Backfill YouTube titles through the queue

This commit is contained in:
Fisto
2026-09-17 13:19:22 +02:00
parent 557ba5ca7e
commit 774183b6fa
11 changed files with 295 additions and 21 deletions
+59 -1
View File
@@ -171,6 +171,8 @@ export interface MediaRow {
preview_attempts: number;
/** Embed title from Discord (YouTube and friends). */
title: string | null;
title_attempts: number;
title_error: string | null;
/** Lowercased, punctuation-collapsed title for token-prefix search. */
title_key: string | null;
created_at: string;
@@ -224,6 +226,8 @@ CREATE TABLE IF NOT EXISTS media (
preview_attempts INTEGER NOT NULL DEFAULT 0,
title TEXT,
title_key TEXT,
title_attempts INTEGER NOT NULL DEFAULT 0,
title_error TEXT,
created_at TEXT NOT NULL
);
@@ -273,6 +277,10 @@ export class ScrapeStore {
this.db.exec("ALTER TABLE media ADD COLUMN title TEXT");
this.db.exec("ALTER TABLE media ADD COLUMN title_key TEXT");
}
if (!names.has("title_attempts")) {
this.db.exec("ALTER TABLE media ADD COLUMN title_attempts INTEGER NOT NULL DEFAULT 0");
this.db.exec("ALTER TABLE media ADD COLUMN title_error TEXT");
}
}
close(): void {
@@ -434,6 +442,8 @@ export class ScrapeStore {
pending: number;
cached: number;
failed: number;
/** YouTube-like rows still missing a title. */
titlePending: number;
byKind: Record<MediaKind, { total: number; pending: number; cached: number; failed: number }>;
} {
const empty = () => ({ total: 0, pending: 0, cached: 0, failed: 0 });
@@ -477,10 +487,58 @@ export class ScrapeStore {
totals.failed += row.failed ?? 0;
}
return { ...totals, byKind };
const titles = this.db
.prepare(
`SELECT COUNT(*) AS count FROM media
WHERE title IS NULL AND title_error IS NULL
AND (lower(COALESCE(provider, '')) LIKE '%youtube%'
OR lower(url) LIKE '%youtube.com/%'
OR lower(url) LIKE '%youtu.be/%')`
)
.get() as { count: number | null } | undefined;
return { ...totals, titlePending: titles?.count ?? 0, byKind };
}
/** Count one transient preview failure and return the new total. */
/**
* YouTube-like rows whose title Discord did not provide, oldest first. Resolved through the
* provider oEmbed in the backfill queue.
*/
pendingTitles(limit = 500): MediaRow[] {
return this.db
.prepare(
`SELECT * FROM media
WHERE title IS NULL AND title_error IS NULL
AND (lower(COALESCE(provider, '')) LIKE '%youtube%'
OR lower(url) LIKE '%youtube.com/%'
OR lower(url) LIKE '%youtu.be/%')
ORDER BY id LIMIT ?`
)
.all(limit) as unknown as MediaRow[];
}
/** Store a resolved title and its search key. */
markTitle(id: number, title: string): void {
this.db
.prepare("UPDATE media SET title = ?, title_key = ?, title_error = NULL WHERE id = ?")
.run(title.slice(0, 300), normalizeTitleKey(title), id);
}
/** Count one transient title failure and return the new total. */
bumpTitleAttempt(id: number): number {
this.db.prepare("UPDATE media SET title_attempts = title_attempts + 1 WHERE id = ?").run(id);
const row = this.db.prepare("SELECT title_attempts FROM media WHERE id = ?").get(id) as
| { title_attempts: number | null }
| undefined;
return row?.title_attempts ?? 0;
}
markTitleError(id: number, error: string): void {
this.db.prepare("UPDATE media SET title_error = ? WHERE id = ?").run(error.slice(0, 200), id);
}
/** Count one transient preview failure and return the new total. */
bumpPreviewAttempt(id: number): number {
this.db.prepare("UPDATE media SET preview_attempts = preview_attempts + 1 WHERE id = ?").run(id);