173 lines
5.1 KiB
JavaScript
173 lines
5.1 KiB
JavaScript
|
|
|
|
/**
|
|
* 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();
|
|
|