mymusics

retro MySpace-style music player
Log | Files | Refs | README

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 });