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 <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-01 00:10:26 +02:00
parent 38a779c5f8
commit 7e624dd452
2 changed files with 93 additions and 75 deletions

View file

@ -79,7 +79,12 @@ fun BrowseScreen(
val page = api.browse(catalogId, skip = skip, take = 60) val page = api.browse(catalogId, skip = skip, take = 60)
title = page.title.ifBlank { titleHint ?: catalogId } title = page.title.ifBlank { titleHint ?: catalogId }
total = page.total 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) { } catch (e: Exception) {
error = e.message error = e.message
} finally { } finally {

View file

@ -328,7 +328,16 @@ export class ViewerService {
title: string; title: string;
kind: "MOVIE" | "SERIES"; kind: "MOVIE" | "SERIES";
slug: string; slug: string;
items: Awaited<ReturnType<ViewerService["shelfItems"]>>; 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) { if (recentMovies.length) {
@ -567,80 +576,79 @@ export class ViewerService {
} }
async shelfItems(shelfId: string, kind: "MOVIE" | "SERIES", take: number) { 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") { if (kind === "MOVIE") {
const files = await prisma.mediaFile.findMany({ const where = {
where: { shelfId, available: true, movieId: { not: null } }, mediaFiles: { some: { shelfId, available: true } },
orderBy: { createdAt: "desc" }, };
take: Math.max(take * 4, take), const [total, movies] = await Promise.all([
include: { movie: true }, prisma.movie.count({ where }),
}); prisma.movie.findMany({
const seen = new Set<string>(); where,
const out: Array<{ orderBy: [{ title: "asc" }, { year: "asc" }],
type: "movie"; skip,
id: string; take,
title: string; include: {
year: number | null; mediaFiles: {
posterUrl: string | null; where: { shelfId, available: true },
backdropUrl: string | null; orderBy: { createdAt: "desc" },
mediaFileId: string | null; take: 1,
}> = []; },
for (const f of files) { },
if (!f.movieId || !f.movie || seen.has(f.movieId)) continue; }),
seen.add(f.movieId); ]);
out.push({ return {
type: "movie", total,
id: f.movie.id, items: movies.map((m) => ({
title: f.movie.title, type: "movie" as const,
year: f.movie.year, id: m.id,
posterUrl: f.movie.posterUrl, title: m.title,
backdropUrl: f.movie.backdropUrl, year: m.year,
mediaFileId: f.id, posterUrl: m.posterUrl,
}); backdropUrl: m.backdropUrl,
if (out.length >= take) break; mediaFileId: m.mediaFiles[0]?.id ?? null,
} })),
return out; };
} }
const files = await prisma.mediaFile.findMany({ const where = {
where: { episodes: {
shelfId, some: {
available: true, mediaFiles: { some: { shelfId, available: true } },
episodeId: { not: null }, },
}, },
orderBy: { createdAt: "desc" }, };
take: Math.max(take * 6, take), const [total, series] = await Promise.all([
include: { prisma.series.count({ where }),
episode: { include: { series: true } }, prisma.series.findMany({
}, where,
}); orderBy: [{ title: "asc" }, { year: "asc" }],
const seen = new Set<string>(); skip,
const out: Array<{ take,
type: "series"; }),
id: string; ]);
title: string; return {
year: number | null; total,
posterUrl: string | null; items: series.map((s) => ({
backdropUrl: string | null; type: "series" as const,
mediaFileId: string | null; id: s.id,
}> = []; title: s.title,
for (const f of files) { year: s.year,
const series = f.episode?.series; posterUrl: s.posterUrl,
const seriesId = f.episode?.seriesId; backdropUrl: s.backdropUrl,
if (!series || !seriesId || seen.has(seriesId)) continue; mediaFileId: null as string | null,
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;
} }
async continueWatching(viewerId: string) { async continueWatching(viewerId: string) {
@ -990,13 +998,18 @@ export class ViewerService {
})) || null; })) || null;
if (!shelf) throw new AppError("NOT_FOUND", "Catalogus niet gevonden", 404); 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 { return {
id: shelf.slug, id: shelf.slug,
title: shelf.name, title: shelf.name,
kind: shelf.kind, kind: shelf.kind,
total: items.length + skip, total: page.total,
items: items.slice(skip, skip + take), items: page.items,
}; };
} }