index-metadata.ts (3263B)
1 import fs from "node:fs"; 2 import path from "node:path"; 3 import readline from "node:readline"; 4 5 import dotenv from "dotenv"; 6 7 import { IA_DRAGON_HOARD_ID, parseTrackLine } from "../server/metadata.js"; 8 import { WritableTrackStore } from "../server/trackStore.js"; 9 10 const PROJECT_ROOT = process.cwd(); 11 dotenv.config({ path: path.join(PROJECT_ROOT, ".env") }); 12 13 function resolvePath(p: string): string { 14 return path.isAbsolute(p) ? p : path.resolve(PROJECT_ROOT, p); 15 } 16 17 const BUNDLED_METADATA_TSV = path.join(PROJECT_ROOT, "data", "metadata.tsv"); 18 const BUNDLED_TRACKS_DB = path.join(PROJECT_ROOT, "data", "tracks.db"); 19 20 function resolveMetadataTsv(): string { 21 const raw = process.env.METADATA_TSV?.trim(); 22 if (!raw) return BUNDLED_METADATA_TSV; 23 const resolved = resolvePath(raw); 24 if (fs.existsSync(resolved)) return resolved; 25 if (fs.existsSync(BUNDLED_METADATA_TSV)) return BUNDLED_METADATA_TSV; 26 return resolved; 27 } 28 29 function resolveTracksDb(): string { 30 const raw = process.env.TRACKS_DB?.trim(); 31 return raw ? resolvePath(raw) : BUNDLED_TRACKS_DB; 32 } 33 34 function isStale(tsvPath: string, dbPath: string): boolean { 35 if (!fs.existsSync(dbPath)) return true; 36 if (!fs.existsSync(tsvPath)) return false; 37 const tsvMtime = fs.statSync(tsvPath).mtimeMs; 38 const dbMtime = fs.statSync(dbPath).mtimeMs; 39 return tsvMtime > dbMtime; 40 } 41 42 async function indexFromTsv(tsvPath: string, dbPath: string, itemId: string): Promise<number> { 43 const store = new WritableTrackStore(dbPath); 44 store.open(); 45 store.clearTracks(); 46 47 const BATCH = 5000; 48 let batch: ReturnType<typeof parseTrackLine>[] = []; 49 let total = 0; 50 51 const rl = readline.createInterface({ 52 input: fs.createReadStream(tsvPath, { encoding: "utf-8" }), 53 crlfDelay: Infinity, 54 }); 55 56 for await (const line of rl) { 57 const track = parseTrackLine(line, itemId); 58 if (!track) continue; 59 batch.push(track); 60 if (batch.length >= BATCH) { 61 store.insertBatch(batch.filter(Boolean) as NonNullable<typeof track>[]); 62 total += batch.length; 63 batch = []; 64 if (total % 50_000 === 0) console.info(`MyMusics index: ${total} tracks…`); 65 } 66 } 67 if (batch.length > 0) { 68 store.insertBatch(batch as NonNullable<(typeof batch)[0]>[]); 69 total += batch.length; 70 } 71 72 console.info("MyMusics index: rebuilding FTS…"); 73 store.finishIndex(); 74 store.close(); 75 return total; 76 } 77 78 async function main() { 79 const args = process.argv.slice(2); 80 const ifStale = args.includes("--if-stale"); 81 const force = args.includes("--force"); 82 83 const tsvPath = resolveMetadataTsv(); 84 const dbPath = resolveTracksDb(); 85 const itemId = process.env.IA_ITEM_ID?.trim() || IA_DRAGON_HOARD_ID; 86 87 if (!fs.existsSync(tsvPath)) { 88 console.error(`METADATA_TSV not found: ${tsvPath}`); 89 process.exit(1); 90 } 91 92 if (ifStale && !force && !isStale(tsvPath, dbPath)) { 93 console.info(`MyMusics index: ${dbPath} is up to date (use --force to rebuild).`); 94 return; 95 } 96 97 const t0 = Date.now(); 98 console.info(`MyMusics index: ${tsvPath} → ${dbPath}`); 99 const count = await indexFromTsv(tsvPath, dbPath, itemId); 100 console.info(`MyMusics index: ${count} tracks in ${((Date.now() - t0) / 1000).toFixed(1)}s`); 101 } 102 103 main().catch((e) => { 104 console.error(e); 105 process.exit(1); 106 });