import { watch, type FSWatcher } from 'node:fs'; import { dirname } from 'node:path'; import { MUSIC_ROOT, reindexFolder } from './indexer'; // Recursive file watcher on ~/Music (chokidar-style). Any change — add / modify (mtime delta) / delete — // is logged and, after a short debounce, triggers a LOCALIZED reindex of just the affected folder(s) // (reindexFolder recomputes that album/artist's cache entry; its mtime-based signature decides whether a // rebuild is actually needed, so irrelevant touches are cheap no-ops). Full correctness is backstopped by // the nightly full reindex — a new folder whose files land after the debounce is caught by that. // // Notes: max_user_watches is ~483k here (>> the folder count), and Bun's recursive watch fires through // the ~/Music symlink and for new-dir creation (so reindexFolder reads the new folder's contents). const DEBOUNCE_MS = 3000; let watcher: FSWatcher | null = null; let flushTimer: ReturnType | null = null; const pending = new Map(); // music-relative folder → reindex recursively? function enqueue(rel: string, recursive: boolean): void { pending.set(rel, (pending.get(rel) ?? false) || recursive); } function scheduleFlush(): void { if (flushTimer) clearTimeout(flushTimer); flushTimer = setTimeout(() => void flush(), DEBOUNCE_MS); } async function flush(): Promise { flushTimer = null; const folders = [...pending]; pending.clear(); console.log(`[music] watch: reindexing ${folders.length} changed folder(s)`); for (const [rel, recursive] of folders) { try { await reindexFolder(rel, { recursive }); } catch (err) { console.error(`[music] watch: reindex "${rel}" failed:`, err instanceof Error ? err.message : err); } } } export function startMusicWatcher(): void { try { watcher = watch(MUSIC_ROOT, { recursive: true, persistent: true }, (eventType, filename) => { if (!filename) return; const rel = filename.toString(); // path relative to MUSIC_ROOT (posix) console.log(`[music] watch: ${eventType} — ${rel}`); // Two enqueues: the containing folder SHALLOW — rebuilds that album on a file change, and prunes any // renamed/removed-away child it no longer sees (e.g. an old-cased artist dir); and the path itself // RECURSIVE — so if it's a new or renamed-in container, its album children get indexed. A recursive // reindex of a plain file or leaf album is a cheap no-op / single-folder rebuild. const folder = dirname(rel); enqueue(folder === '.' ? '' : folder, false); enqueue(rel, true); scheduleFlush(); }); console.log(`[music] watching ${MUSIC_ROOT} (recursive) for changes`); } catch (err) { console.error('[music] failed to start file watcher:', err instanceof Error ? err.message : err); } } export function stopMusicWatcher(): void { if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } if (watcher) { watcher.close(); watcher = null; } pending.clear(); }