From 7e624dd452b2599e0b75fb9fa94cba0aacf6fa08 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Tue, 1 Sep 2026 00:10:26 +0200 Subject: [PATCH] Fix shelf catalog pagination so browse shows all titles. Shelf endpoints now return a real total and page by skip/take instead of capping around 60. Co-authored-by: Cursor --- .../vonas/mediacluster/tv/ui/BrowseScreen.kt | 7 +- apps/master-api/src/viewer/service.ts | 161 ++++++++++-------- 2 files changed, 93 insertions(+), 75 deletions(-) diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt index a0bb4ed..9b6ba9d 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt @@ -79,7 +79,12 @@ fun BrowseScreen( val page = api.browse(catalogId, skip = skip, take = 60) title = page.title.ifBlank { titleHint ?: catalogId } total = page.total - items = if (reset) page.items else items + page.items.filter { n -> items.none { it.id == n.id } } + items = if (reset) { + page.items + } else { + val existing = items.map { "${it.type}:${it.id}" }.toSet() + items + page.items.filter { "${it.type}:${it.id}" !in existing } + } } catch (e: Exception) { error = e.message } finally { diff --git a/apps/master-api/src/viewer/service.ts b/apps/master-api/src/viewer/service.ts index f559672..66c999b 100644 --- a/apps/master-api/src/viewer/service.ts +++ b/apps/master-api/src/viewer/service.ts @@ -328,7 +328,16 @@ export class ViewerService { title: string; kind: "MOVIE" | "SERIES"; slug: string; - items: Awaited>; + items: Array<{ + type: "movie" | "series"; + id: string; + title: string; + year: number | null; + posterUrl: string | null; + backdropUrl: string | null; + mediaFileId: string | null; + badge?: string; + }>; }> = []; if (recentMovies.length) { @@ -567,80 +576,79 @@ export class ViewerService { } async shelfItems(shelfId: string, kind: "MOVIE" | "SERIES", take: number) { - // Sorteer op laatst toegevoegd mediabestand (niet op metadata-aanmaakdatum) + const page = await this.shelfItemsPage(shelfId, kind, 0, take); + return page.items; + } + + /** Volledige plank met echte total + skip/take (voor browse/Alles). */ + async shelfItemsPage( + shelfId: string, + kind: "MOVIE" | "SERIES", + skip: number, + take: number + ) { if (kind === "MOVIE") { - const files = await prisma.mediaFile.findMany({ - where: { shelfId, available: true, movieId: { not: null } }, - orderBy: { createdAt: "desc" }, - take: Math.max(take * 4, take), - include: { movie: true }, - }); - const seen = new Set(); - const out: Array<{ - type: "movie"; - id: string; - title: string; - year: number | null; - posterUrl: string | null; - backdropUrl: string | null; - mediaFileId: string | null; - }> = []; - for (const f of files) { - if (!f.movieId || !f.movie || seen.has(f.movieId)) continue; - seen.add(f.movieId); - out.push({ - type: "movie", - id: f.movie.id, - title: f.movie.title, - year: f.movie.year, - posterUrl: f.movie.posterUrl, - backdropUrl: f.movie.backdropUrl, - mediaFileId: f.id, - }); - if (out.length >= take) break; - } - return out; + const where = { + mediaFiles: { some: { shelfId, available: true } }, + }; + const [total, movies] = await Promise.all([ + prisma.movie.count({ where }), + prisma.movie.findMany({ + where, + orderBy: [{ title: "asc" }, { year: "asc" }], + skip, + take, + include: { + mediaFiles: { + where: { shelfId, available: true }, + orderBy: { createdAt: "desc" }, + take: 1, + }, + }, + }), + ]); + return { + total, + items: movies.map((m) => ({ + type: "movie" as const, + id: m.id, + title: m.title, + year: m.year, + posterUrl: m.posterUrl, + backdropUrl: m.backdropUrl, + mediaFileId: m.mediaFiles[0]?.id ?? null, + })), + }; } - const files = await prisma.mediaFile.findMany({ - where: { - shelfId, - available: true, - episodeId: { not: null }, + const where = { + episodes: { + some: { + mediaFiles: { some: { shelfId, available: true } }, + }, }, - orderBy: { createdAt: "desc" }, - take: Math.max(take * 6, take), - include: { - episode: { include: { series: true } }, - }, - }); - const seen = new Set(); - const out: Array<{ - type: "series"; - id: string; - title: string; - year: number | null; - posterUrl: string | null; - backdropUrl: string | null; - mediaFileId: string | null; - }> = []; - for (const f of files) { - const series = f.episode?.series; - const seriesId = f.episode?.seriesId; - if (!series || !seriesId || seen.has(seriesId)) continue; - seen.add(seriesId); - out.push({ - type: "series", - id: series.id, - title: series.title, - year: series.year, - posterUrl: series.posterUrl, - backdropUrl: series.backdropUrl, - mediaFileId: null, - }); - if (out.length >= take) break; - } - return out; + }; + const [total, series] = await Promise.all([ + prisma.series.count({ where }), + prisma.series.findMany({ + where, + orderBy: [{ title: "asc" }, { year: "asc" }], + skip, + take, + }), + ]); + return { + total, + items: series.map((s) => ({ + type: "series" as const, + id: s.id, + title: s.title, + year: s.year, + posterUrl: s.posterUrl, + backdropUrl: s.backdropUrl, + mediaFileId: null as string | null, + })), + }; } async continueWatching(viewerId: string) { @@ -990,13 +998,18 @@ export class ViewerService { })) || null; if (!shelf) throw new AppError("NOT_FOUND", "Catalogus niet gevonden", 404); - const items = await this.shelfItems(shelf.id, shelf.kind, skip + take); + const page = await this.shelfItemsPage( + shelf.id, + shelf.kind as "MOVIE" | "SERIES", + skip, + take + ); return { id: shelf.slug, title: shelf.name, kind: shelf.kind, - total: items.length + skip, - items: items.slice(skip, skip + take), + total: page.total, + items: page.items, }; }