fp/services/pocketbase/utils/data_migrations/2025-11-07-import-thumbnails.js
CJ_Clippy 6caf2dbcc3
Some checks failed
ci / test (push) Failing after 9m42s
fp/our CI/CD / build (push) Successful in 1m19s
add /vods and /vt/:slug/vods
2025-11-08 12:36:26 -08:00

118 lines
3.8 KiB
JavaScript

import PocketBase from 'pocketbase';
import { readFileSync } from 'node:fs';
import { basename, join } from 'node:path';
import spawn from 'nano-spawn';
import { tmpdir } from 'node:os';
import mime from 'mime';
const pb = new PocketBase(process.env.PB_URL || 'http://127.0.0.1:8090');
if (!process.env.PB_USERNAME) throw new Error('PB_USERNAME missing');
if (!process.env.PB_PASSWORD) throw new Error('PB_PASSWORD missing');
if (!process.env.B2_BUCKET_FROM) throw new Error('B2_BUCKET_FROM missing');
if (!process.env.V1_DATA_FILE) throw new Error('V1_DATA_FILE missing');
const vodsCollectionName = 'pbc_144770472';
// pbc_144770472 is the vods collection
// cv6m31vj98gmtsx is a sample vod id
async function main() {
console.log('Authenticating with PocketBase...');
await pb
.collection("_superusers")
.authWithPassword(process.env.PB_USERNAME, process.env.PB_PASSWORD);
// Use vod.streamDate to find the correct entry
// load v1 datafile
const v1VodDataRaw = readFileSync(process.env.V1_DATA_FILE, { encoding: 'utf-8' });
const v1VodData = JSON.parse(v1VodDataRaw);
// console.log('v1VodData sample', v1VodData[0])
// # BUILD A MANIFEST
// For each vod
// Get the expected pocketbase s3 key format, ex: `pbc_144770472/cv6m31vj98gmtsx/projektmelody-fansly-2025-09-17-thumb.png`
// Get the v1 thumbnail s3 key from the v1 export json
let manifest = [];
const vods = await pb.collection('vods').getFullList();
for (const vod of vods) {
// console.log('v2VodData sample', vod)
// console.log(`for this vod`, vod)
const v1Vod = v1VodData.find((vod1) => new Date(vod1.streamDate).getTime() === new Date(vod.streamDate).getTime());
console.log(`v1vod`, v1Vod)
if (!v1Vod) throw new Error(`failed to find matching v1 data vod for vod`);
// skip if there is no thumbnail in the v1 vod
if (!v1Vod.thumbnail) continue;
const inputS3Key = v1Vod.thumbnail.replace('content/', '');
const outputFile = join(tmpdir(), basename(inputS3Key));
const entry = {
vodId: vod.id,
inputS3Key,
outputFile,
};
manifest.push(entry);
}
const invalidVods = manifest.filter((m) => m.inputS3Key.includes('mp4'))
if (invalidVods.length > 0) {
console.error('invalid thumbnails found', invalidVods)
throw new Error('invalid. mp4s found in thumbnails');
}
console.log('manifest', manifest);
// # ACTUAL WORK
// Copy the file from B2_BUCKET_FROM to tmp file
// Update the pocketbase vod.thumbnail entry
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function retry(fn, retries = 6, delayMs = 500) {
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
console.warn(`Attempt ${attempt} failed: ${err}. Retrying in ${delayMs}ms...`);
if (attempt < retries) await sleep(delayMs);
}
}
throw lastError;
}
for (const [i, m] of manifest.entries()) {
var from = "b2://" + process.env.B2_BUCKET_FROM + "/" + m.inputS3Key;
var to = m.outputFile;
var vodId = m.vodId;
console.log("processing thumbnail " + i + ". " + from + " -> " + to + " (vodId=" + vodId + ")");
// Retry the download
await retry(function () {
return spawn('b2', ['file', 'download', from, to]);
});
// Retry the PocketBase upload
await retry(async function () {
var fileData = readFileSync(m.outputFile);
var mimetype = mime.getType(m.outputFile) || undefined;
var file = new File([fileData], basename(m.outputFile), { type: mimetype });
var form = new FormData();
form.set("thumbnail", file);
return pb.collection('vods').update(vodId, form);
});
}
console.log("All done.");
}
main()