/** * Migration Script: V1 → json * ----------------------------------- * This script migrates VTuber and VOD data from an old Postgres database (V1) into JSON files * One JSON file per collection, suitable for importing using pocketbase import script * * Usage: * - Ensure environment variables are configured for both databases: * V1_DB_HOST Hostname of the V1 database (default: "localhost") * V1_DB_PORT Port of the V1 database (default: "5444") * V1_DB_USER Username for the V1 database (default: "postgres") * V1_DB_PASS Password for the V1 database (default: "password") * V1_DB_NAME Database name for V1 (default: "restoredb") * DEFAULT_UPLOADER_ID * An existing user ID in the V2 database that will be set as the uploader for all migrated records. * * Usage: * Run with Node.js: * $ npx @dotenvx/dotenvx run -f ./.env.local -- node ./2025-10-30-v1-export.js ../pb_data/ * */ import pg from 'pg'; import fs from 'fs'; import path from 'path'; const dataDir = process.argv[2]; // e.g., "../../pb_data/" if (!dataDir) { console.error("Please provide the output data directory as the first argument."); process.exit(1); } const v1 = new pg.Pool({ host: process.env.V1_DB_HOST || 'localhost', port: +(process.env.V1_DB_PORT || '5444'), user: process.env.V1_DB_USER || 'postgres', password: process.env.V1_DB_PASS || 'password', database: process.env.V1_DB_NAME || 'restoredb' }); async function migrateVtubers() { console.log('Migrating vtubers...'); const res = await v1.query(`SELECT * FROM vtubers`); const vtubers = res.rows.map(vt => ({ slug: vt.slug, displayName: vt.display_name, chaturbate: vt.chaturbate, twitter: vt.twitter, patreon: vt.patreon, twitch: vt.twitch, tiktok: vt.tiktok, onlyfans: vt.onlyfans, youtube: vt.youtube, linktree: vt.linktree, carrd: vt.carrd, fansly: vt.fansly, pornhub: vt.pornhub, discord: vt.discord, reddit: vt.reddit, throne: vt.throne, instagram: vt.instagram, facebook: vt.facebook, merch: vt.merch, description: `${vt.description_1 ?? ''}\n${vt.description_2 ?? ''}`.trim() || null, themeColor: vt.theme_color, })); console.log('vtubers sample', vtubers.at(0)) const outPath = path.join(dataDir, 'vtubers.json'); fs.writeFileSync(outPath, JSON.stringify(vtubers, null, 2), 'utf-8'); console.log(`Migrated ${res.rows.length} vtubers → ${outPath}`); } async function migrateVods() { console.log('Migrating vods...'); const res = await v1.query(`SELECT * FROM vods`); const vods = []; for (const vod of res.rows) { // Get linked vtubers const vtuberLinks = await v1.query( `SELECT vtuber_id FROM vods_vtuber_links WHERE vod_id = $1`, [vod.id] ); let vtuberIds = []; if (vtuberLinks.rows.length > 0) { const vtuberRes = await v1.query( `SELECT id FROM vtubers WHERE id = ANY($1)`, [vtuberLinks.rows.map(r => r.vtuber_id)] ); vtuberIds = vtuberRes.rows.map(v => v.id).filter(Boolean); } // Get thumbnail const thumbLink = await v1.query( `SELECT b2.cdn_url, b2.url FROM vods_thumbnail_links vtl JOIN b2_files b2 ON vtl.b_2_file_id = b2.id WHERE vtl.vod_id = $1 LIMIT 1`, [vod.id] ); // Get source video const videoSrcLink = await v1.query( `SELECT b2.cdn_url, b2.url FROM vods_video_src_b_2_links vsl JOIN b2_files b2 ON vsl.b_2_file_id = b2.id WHERE vsl.vod_id = $1 LIMIT 1`, [vod.id] ); // Get Mux asset and playback ID const muxLink = await v1.query( `SELECT m.asset_id, m.playback_id FROM vods_mux_asset_links vmal JOIN mux_assets m ON vmal.mux_asset_id = m.id WHERE vmal.vod_id = $1 LIMIT 1`, [vod.id] ); const muxAssetId = muxLink.rows[0]?.asset_id || null; const muxPlaybackId = muxLink.rows[0]?.playback_id || null; // @chatgpt we need one more // we need to populate videoSrcB2 // we want the cdn_url from b2_files // we have to get the link from the vods_video_src_b_2_links table vods.push({ streamDate: vod.date ?? new Date(), notes: vod.note, sourceVideo: (() => { const u = videoSrcLink.rows[0]?.cdn_url || videoSrcLink.rows[0]?.url || null; return u ? 'content' + new URL(u).pathname : null; })(), thumbnail: (() => { const u = thumbLink.rows[0]?.cdn_url || thumbLink.rows[0]?.url || null; return u ? 'content' + new URL(u).pathname : null; })(), ipfsCid: vod.video_src_hash, videoSrcB2: videoSrcLink.rows[0]?.cdn_url || null, muxAssetId, muxPlaybackId, }); } console.log(vods.at(0)) const outPath = path.join(dataDir, 'vods.json'); fs.writeFileSync(outPath, JSON.stringify(vods, null, 2), 'utf-8'); console.log(`Migrated ${res.rows.length} vods → ${outPath}`); } async function main() { try { await migrateVtubers(); await migrateVods(); } catch (err) { console.error(err); } finally { await v1.end(); } } main();