From 4e60b61edbaee47339c303102f8f6f449c03039a Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sun, 26 Jul 2026 19:22:02 +0700 Subject: [PATCH] fix(scripts): count a Bunny id referenced by url, not only by column A Bunny guid is stored twice per row: in `VideoVersion.videoId` and `VideoAsset.providerVideoId` on its own, and inside `originalUrl` / `sourceUrl` as `https://iframe.mediadelivery.net/embed//`. The lookup read only the id columns. They are written together so they normally agree, but this query decides what gets deleted. A row whose id column was left empty or drifted while its url still carried the guid would present a live video as an orphan, and the script would delete media the product is still serving. Reading both makes a disagreement harmless instead of destructive. Bunny rows are now read in one pass rather than filtered per candidate id: the url match is a substring test, and there are only as many of these rows as there are Bunny videos in the product, so one small scan beats a LIKE per id. --- scripts/bunny-orphan-cleanup.ts | 53 +++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/scripts/bunny-orphan-cleanup.ts b/scripts/bunny-orphan-cleanup.ts index 7f7c5aa..0a44248 100644 --- a/scripts/bunny-orphan-cleanup.ts +++ b/scripts/bunny-orphan-cleanup.ts @@ -166,32 +166,53 @@ function chunk(items: T[], size: number): T[][] { return out; } +/** + * Every Bunny id the database still points at. + * + * A Bunny id is stored in two places per row, and both are checked. `VideoVersion.videoId` + * and `VideoAsset.providerVideoId` hold the guid on its own, while `originalUrl` and + * `sourceUrl` embed the same guid inside + * `https://iframe.mediadelivery.net/embed//`. They are written together, so + * they normally agree, but this decides what gets deleted: a row whose id column was left + * empty or drifted while its url still carried the guid would make a live video look + * orphaned. Reading both makes disagreement harmless rather than destructive. + */ async function findReferencedVideoIds(videoIds: string[]): Promise> { const referenced = new Set(); for (const group of chunk(videoIds, CHUNK_SIZE)) { const [versionRows, assetRows] = await Promise.all([ db.videoVersion.findMany({ - where: { - providerId: 'bunny', - videoId: { in: group }, - }, - select: { videoId: true }, + where: { providerId: 'bunny' }, + select: { videoId: true, originalUrl: true }, }), db.videoAsset.findMany({ - where: { - provider: 'BUNNY', - providerVideoId: { in: group }, - }, - select: { providerVideoId: true }, + where: { provider: 'BUNNY' }, + select: { providerVideoId: true, sourceUrl: true }, }), ]); - versionRows.forEach((row) => { - if (row.videoId) referenced.add(row.videoId); - }); - assetRows.forEach((row) => { - if (row.providerVideoId) referenced.add(row.providerVideoId); - }); + + // Every Bunny row is read rather than filtered by id, because the url has to be + // searched for a guid it merely contains. There are as many of these rows as there + // are Bunny videos in the product, so this is one small scan instead of one LIKE per + // candidate id. + const ids = new Set(group); + const urls: string[] = []; + + for (const row of versionRows) { + if (row.videoId && ids.has(row.videoId)) referenced.add(row.videoId); + if (row.originalUrl) urls.push(row.originalUrl); + } + for (const row of assetRows) { + if (row.providerVideoId && ids.has(row.providerVideoId)) referenced.add(row.providerVideoId); + if (row.sourceUrl) urls.push(row.sourceUrl); + } + + for (const url of urls) { + for (const id of group) { + if (!referenced.has(id) && url.includes(id)) referenced.add(id); + } + } } return referenced;