#!/usr/bin/env bun /** * Trigger the music library index on the officer-music sidecar and follow its progress live, * ending with a summary report. Run from the platform repo: bun scripts/reindex-music.ts * * It reads the sidecar's port from DATA_PATH/music/.server (written by the sidecar on startup) and * consumes its /reindex/stream SSE endpoint — the same stream the app subscribes to. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data'); const PORT_FILE = join(DATA_PATH, 'music', '.server'); type Progress = { running: boolean; foldersScanned: number; albumsBuilt: number; albumsSkipped: number; tracksIndexed: number; coversSaved: number; currentPath: string; }; type Report = { albums: number; built: number; skipped: number; foldersScanned: number; tracksIndexed: number; coversSaved: number; discographies: number; elapsedSec: number; error: string | null; }; function readPort(): number { try { const p = parseInt(readFileSync(PORT_FILE, 'utf8').trim(), 10); if (Number.isInteger(p) && p > 0) return p; } catch { /* fall through */ } console.error(`✗ Could not read the sidecar port from ${PORT_FILE}.`); console.error(' Is officer-music running? pm2 restart officer-music'); process.exit(1); } const isTTY = Boolean(process.stdout.isTTY); const cols = () => (process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 100); function printProgress(p: Progress): void { const line = `♪ indexing… folders ${p.foldersScanned} · tracks ${p.tracksIndexed} · ` + `built ${p.albumsBuilt} · skipped ${p.albumsSkipped} · covers ${p.coversSaved}` + (p.currentPath ? ` · ${p.currentPath}` : ''); if (isTTY) { const clipped = line.length > cols() - 1 ? line.slice(0, cols() - 2) + '…' : line; process.stdout.write('\r\x1b[2K' + clipped); } else { process.stdout.write(line + '\n'); } } function printReport(r: Report): void { if (isTTY) process.stdout.write('\r\x1b[2K'); const l = (k: string, v: string | number) => console.log(` ${k.padEnd(9)} ${v}`); console.log('\n─── Music index complete ───'); l('Albums:', `${r.albums} (${r.built} built, ${r.skipped} unchanged)`); l('Tracks:', `${r.tracksIndexed} indexed`); l('Covers:', `${r.coversSaved} compressed`); l('Discogs:', `${r.discographies} artist${r.discographies === 1 ? '' : 's'}`); l('Folders:', `${r.foldersScanned} scanned`); l('Elapsed:', `${r.elapsedSec}s`); if (r.error) l('Error:', r.error); console.log('────────────────────────────\n'); } async function main(): Promise { const port = readPort(); const url = `http://127.0.0.1:${port}/reindex/stream`; console.log(`Triggering music index via ${url}\n`); let res: Response; try { res = await fetch(url); } catch (err) { console.error(`✗ Could not reach the sidecar at 127.0.0.1:${port}: ${String(err)}`); process.exit(1); } if (!res.ok || !res.body) { console.error(`✗ Sidecar returned ${res.status}`); process.exit(1); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); let sep: number; while ((sep = buf.indexOf('\n\n')) >= 0) { const frame = buf.slice(0, sep); buf = buf.slice(sep + 2); let event = 'message'; let data = ''; for (const line of frame.split('\n')) { if (line.startsWith('event:')) event = line.slice(6).trim(); else if (line.startsWith('data:')) data += line.slice(5).trim(); } if (!data) continue; if (event === 'progress') printProgress(JSON.parse(data) as Progress); else if (event === 'done') printReport(JSON.parse(data) as Report); } } } main();