164 lines
5.5 KiB
TypeScript
164 lines
5.5 KiB
TypeScript
/**
|
||
* 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();
|