import { mkdirSync, writeFileSync } from 'node:fs'; import { join, basename } from 'node:path'; import type { SidecarCommand, SidecarEvent } from '@@/sidecar/protocol'; import { createSidecarConnector } from '@@/sidecar/connect'; import { streamAudioFile } from './stream-audio'; import { cliampUpgradeData, musicWebsocket } from '@@/sidecar/music/cliamp-ws'; import { ensurePulseAudio } from '@@/sidecar/music/pulse-audio'; import { startNightlyReindex, stopNightlyReindex } from './nightly-reindex'; import { reindexNow, reindexFull, ensureCacheSetup, MUSIC_ROOT, getIndexStatus, getManifest, albumVersion, metaFilePath, coverFilePath, discographyFilePath, posterFilePath, lyricsFilePath, onIndexProgress, buildReport, } from './indexer'; import { getMusicFavorites, addMusicFavorite, removeMusicFavorite, getNowPlaying, setNowPlaying, clearNowPlaying, getPlaylists, getPlaylist, createPlaylist, renamePlaylist, deletePlaylist, addPlaylistItems, setPlaylistItems, type FavoriteKind, } from '../db/queries'; import { DATA_PATH } from '@@/data-path'; import { API_URL } from '@@/officer-url.mjs'; // ── Per-user state validation ── // The authenticated user id arrives in X-Officer-User (the platform proxy injects it after auth; we're // loopback-only so we trust it). Favorite/playlist `key`s are opaque paths we never interpret. const userIdOf = (req: Request): number | null => { const n = Number(req.headers.get('x-officer-user')); return Number.isInteger(n) && n > 0 ? n : null; }; const FAVORITE_KINDS = new Set(['track', 'album', 'artist']); const isKind = (k: unknown): k is FavoriteKind => typeof k === 'string' && FAVORITE_KINDS.has(k as FavoriteKind); const PLAYLIST_NAME_MAX = 200; const cleanName = (v: unknown): string | null => { if (typeof v !== 'string') return null; const n = v.trim(); return n && n.length <= PLAYLIST_NAME_MAX ? n : null; }; const asKeys = (v: unknown): string[] | null => Array.isArray(v) && v.every((k) => typeof k === 'string' && k) ? (v as string[]) : null; // The music sidecar (officer-music). Same philosophy as the other officer-* sidecars: a singleton // process that registers with the API server. It OWNS an audio-streaming HTTP server (all path // resolution + streaming + ffprobe duration happens here); the platform API is just a thin proxy that // authenticates and forwards to us. The server listens on a random loopback port, reported to the API // on connect so it can route `/api/music/*` here. // // ───────────────────────────────────────────────────────────────────────────────────────────────── // HTTP CONTRACT — the full `/api/music/*` surface (this fetch handler is the source of truth; the // platform side is an opaque catch-all proxy). All routes are reached as `/api/music/`, authed // upstream by userMiddleware (Bearer header or `?token=` for media). Data shapes are the exported // `IndexStatus` / `IndexReport` / `IndexMeta` types in indexer.ts. // // GET /stream?path= audio with Range→206 (Content-Range/Length/Accept-Ranges) // + `X-Audio-Duration` (seconds, ffprobe). 400/404/416. // GET /manifest pure read of the last completed index (NO build triggered) — // { version, generatedAt, albums: { "": { v, cover, tracks, videos?, disco? } } } // GET /meta?path= album meta.json (IndexMeta). ETag: ; If-None-Match → 304. // GET /cover?path= compressed cover.jpg. ETag: ; If-None-Match → 304. // GET /poster?path=&file=