Process all pending movies in IMDb backfill instead of capping at 200.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-08-25 11:41:56 +02:00
parent a9df10ed7c
commit fc842aa145
5 changed files with 45 additions and 36 deletions

View file

@ -70,7 +70,7 @@ export default function DashboardPage() {
async function backfillImdb() { async function backfillImdb() {
if ( if (
!confirm( !confirm(
"IMDb-IDs ophalen via TMDB voor films die die nog missen? Nodig zodat Cinemeta jouw lokale bronnen toont." "IMDb-IDs ophalen via TMDB voor ALLE films die die nog missen? Dit kan enkele minuten duren bij een grote bibliotheek. Nodig zodat Cinemeta jouw lokale bronnen toont."
) )
) { ) {
return; return;
@ -86,7 +86,7 @@ export default function DashboardPage() {
return; return;
} }
setImdbMsg( setImdbMsg(
`Gecontroleerd: ${json.checked ?? 0}, bijgewerkt: ${json.updated ?? 0}, nog open: ${json.skipped ?? 0}` `Gecontroleerd: ${json.checked ?? 0}, bijgewerkt: ${json.updated ?? 0}, nog open: ${json.remaining ?? json.skipped ?? 0}, mislukt: ${json.failed ?? 0}`
); );
load(); load();
} }

View file

@ -445,7 +445,8 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config)
}); });
app.post("/api/v1/admin/library/backfill-imdb", { preHandler: requireAdmin }, async () => { app.post("/api/v1/admin/library/backfill-imdb", { preHandler: requireAdmin }, async () => {
const result = await metadata.backfillMovieImdbIds({ delayMs: 150, limit: 500 }); // Process ALL pending movies (batched). Can take a few minutes for large libraries.
const result = await metadata.backfillMovieImdbIds({ delayMs: 120 });
return result; return result;
}); });

View file

@ -126,16 +126,16 @@ async function main() {
await app.listen({ port: config.PORT, host: config.HOST }); await app.listen({ port: config.PORT, host: config.HOST });
console.log(`Master API listening on ${config.HOST}:${config.PORT}`); console.log(`Master API listening on ${config.HOST}:${config.PORT}`);
// Background: fill missing movie imdbIds so Cinemeta detail pages can resolve local streams. // Background: fill ALL missing movie imdbIds (batched) so Cinemeta can resolve local streams.
if (config.TMDB_API_KEY) { if (config.TMDB_API_KEY) {
const metadata = new MetadataService(config.TMDB_API_KEY); const metadata = new MetadataService(config.TMDB_API_KEY);
setTimeout(() => { setTimeout(() => {
void metadata void metadata
.backfillMovieImdbIds({ delayMs: 200, limit: 200 }) .backfillMovieImdbIds({ delayMs: 120 })
.then((r) => { .then((r) => {
if (r.checked > 0) { if (r.checked > 0) {
console.log( console.log(
`IMDb backfill: checked=${r.checked} updated=${r.updated} remaining=${r.skipped}` `IMDb backfill: checked=${r.checked} updated=${r.updated} remaining=${r.remaining} failed=${r.failed}`
); );
} }
}) })

View file

@ -666,50 +666,58 @@ export class MetadataService {
} }
/** /**
* Fill missing imdbId for all movies that have a TMDB id. * Fill missing imdbId for movies that have a TMDB id.
* Processes in batches until none remain (or `limit` is hit).
* Rate-limited to avoid TMDB bans. Safe for series (movies only). * Rate-limited to avoid TMDB bans. Safe for series (movies only).
*/ */
async backfillMovieImdbIds(options?: { async backfillMovieImdbIds(options?: {
delayMs?: number; delayMs?: number;
/** Max movies to process this run. Omit / 0 = all pending. */
limit?: number; limit?: number;
}): Promise<{ checked: number; updated: number; skipped: number; failed: number }> { batchSize?: number;
const delayMs = options?.delayMs ?? 200; }): Promise<{ checked: number; updated: number; remaining: number; failed: number }> {
const limit = options?.limit ?? 500; const delayMs = options?.delayMs ?? 150;
const batchSize = Math.min(Math.max(options?.batchSize ?? 100, 1), 500);
const maxTotal = options?.limit && options.limit > 0 ? options.limit : Number.POSITIVE_INFINITY;
if (!this.tmdbApiKey) { if (!this.tmdbApiKey) {
return { checked: 0, updated: 0, skipped: 0, failed: 0 }; return { checked: 0, updated: 0, remaining: 0, failed: 0 };
} }
const pending = await prisma.movie.findMany({ let checked = 0;
where: { tmdbId: { not: null }, imdbId: null },
select: { id: true, title: true, tmdbId: true },
take: limit,
orderBy: { updatedAt: "asc" },
});
let updated = 0; let updated = 0;
let failed = 0; let failed = 0;
for (const row of pending) {
try { while (checked < maxTotal) {
const ok = await this.ensureMovieImdbId(row.id); const take = Math.min(batchSize, maxTotal - checked);
if (ok) updated += 1; const pending = await prisma.movie.findMany({
else failed += 1; where: { tmdbId: { not: null }, imdbId: null },
} catch { select: { id: true },
failed += 1; take,
} orderBy: { updatedAt: "asc" },
if (delayMs > 0) { });
await new Promise((r) => setTimeout(r, delayMs)); if (pending.length === 0) break;
for (const row of pending) {
try {
const ok = await this.ensureMovieImdbId(row.id);
if (ok) updated += 1;
else failed += 1;
} catch {
failed += 1;
}
checked += 1;
if (delayMs > 0) {
await new Promise((r) => setTimeout(r, delayMs));
}
} }
if (pending.length < take) break;
} }
const stillMissing = await prisma.movie.count({ const remaining = await prisma.movie.count({
where: { tmdbId: { not: null }, imdbId: null }, where: { tmdbId: { not: null }, imdbId: null },
}); });
return { return { checked, updated, remaining, failed };
checked: pending.length,
updated,
skipped: stillMissing,
failed,
};
} }
private async absorbOrphanMovies( private async absorbOrphanMovies(

View file

@ -603,7 +603,7 @@ async function getStreams(
); );
// Do not block this request — fill IMDb ids in background for next try. // Do not block this request — fill IMDb ids in background for next try.
if (isImdb && missingImdb > 0) { if (isImdb && missingImdb > 0) {
void metadata.backfillMovieImdbIds({ delayMs: 150, limit: 80 }).catch(() => undefined); void metadata.backfillMovieImdbIds({ delayMs: 120, limit: 30 }).catch(() => undefined);
} }
return []; return [];
} }