/** * Migration Script: V1 → V2 Database * ----------------------------------- * This script migrates VTuber and VOD data from an old Postgres database (V1) into the new Prisma-backed database (V2). * * 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. * * What it does: * 1. Migrates VTubers: * - Reads all rows from `vtubers` in V1. * - Inserts each into V2’s `vtuber` table using Prisma. * - Maps all known fields (social links, images, themeColor, etc.). * - Combines `description_1` and `description_2` into a single `description` field. * - Assigns the `DEFAULT_UPLOADER_ID` to each migrated VTuber. * * 2. Migrates VODs: * - Reads all rows from `vods` in V1. * - Resolves associated VTubers via `vods_vtuber_links` → `vtubers.slug`. * - Finds related thumbnails and source video links via `vods_thumbnail_links` and `vods_video_src_b_2_links`. * - Inserts each VOD into V2’s `vod` table, connecting it to the corresponding VTubers by slug. * - Assigns the `DEFAULT_UPLOADER_ID` to each migrated VOD. * * Notes: * - This script assumes schema compatibility between V1 and V2 (field names may differ slightly). * - Thumbnails and video source links fall back to `cdn_url` or `url` if available. * - Any V1 records with missing or null values are gracefully handled with `null` fallbacks. * - Run this script once; re-running may cause duplicate records unless unique constraints prevent it. * * Execution: * Run with Node.js: * $ npx @dotenvx/dotenvx run -f ./.env -- tsx ./migrate.ts * * Cleanup: * - Connections to both the V1 database (pg.Pool) and Prisma client are properly closed at the end of execution. */ import { PrismaClient } from '../generated/prisma'; import pg from 'pg'; const prisma = new PrismaClient(); 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' }); // Set this to an existing user ID in v2 const DEFAULT_UPLOADER_ID = process.env.DEFAULT_UPLOADER_ID || 'REPLACE_WITH_V2_USER_ID'; async function migrateVtubers() { console.log('Migrating vtubers...'); const res = await v1.query(`SELECT * FROM vtubers`); for (const vt of res.rows) { await prisma.vtuber.create({ data: { slug: vt.slug, image: vt.image, 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, uploaderId: DEFAULT_UPLOADER_ID } }); } console.log(`Migrated ${res.rows.length} vtubers`); } async function migrateVods() { console.log('Migrating vods...'); const vods = await v1.query(`SELECT * FROM vods`); for (const vod of vods.rows) { // Get linked vtubers const vtuberLinks = await v1.query( `SELECT vtuber_id FROM vods_vtuber_links WHERE vod_id = $1`, [vod.id] ); let vtuberSlugs: string[] = []; if (vtuberLinks.rows.length > 0) { const vtuberRes = await v1.query( `SELECT slug FROM vtubers WHERE id = ANY($1)`, [vtuberLinks.rows.map(r => r.vtuber_id)] ); vtuberSlugs = vtuberRes.rows.map(v => v.slug).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] ); await prisma.vod.create({ data: { uploaderId: DEFAULT_UPLOADER_ID, streamDate: vod.date ?? new Date(), notes: vod.note, sourceVideo: videoSrcLink.rows[0]?.cdn_url || videoSrcLink.rows[0]?.url || null, thumbnail: thumbLink.rows[0]?.cdn_url || thumbLink.rows[0]?.url || null, vtubers: { connect: vtuberSlugs.map(slug => ({ slug })) } } }); } console.log(`Migrated ${vods.rows.length} vods`); } async function main() { try { await migrateVtubers(); await migrateVods(); } catch (err) { console.error(err); } finally { await v1.end(); await prisma.$disconnect(); } } main();