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)
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 {

View file

@ -328,7 +328,16 @@ export class ViewerService {
title: string;
kind: "MOVIE" | "SERIES";
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) {
@ -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)
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<string>();
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 page = await this.shelfItemsPage(shelfId, kind, 0, take);
return page.items;
}
const files = await prisma.mediaFile.findMany({
where: {
shelfId,
available: true,
episodeId: { not: null },
},
orderBy: { createdAt: "desc" },
take: Math.max(take * 6, take),
/** 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 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: {
episode: { include: { series: true } },
mediaFiles: {
where: { shelfId, available: true },
orderBy: { createdAt: "desc" },
take: 1,
},
});
const seen = new Set<string>();
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 {
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,
})),
};
}
return out;
const where = {
episodes: {
some: {
mediaFiles: { some: { shelfId, available: true } },
},
},
};
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,
};
}