fp/services/our/scripts/2025-08-12-migrate-from-v1.ts
CJ_Clippy cc0f0a33fa
Some checks failed
rssapp CI/CD / build (push) Successful in 2m2s
ci / test (push) Failing after 1m5s
ci / build (push) Has been cancelled
add rssapp gitea actions builder
2025-09-28 00:55:50 -08:00

164 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 V2s `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 V2s `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();