music: index artist discographies (album → release type) for the app
Each Albums/<Artist>/_discography.md (author-maintained source of truth, never modified) is compiled into a per-artist discography.json in the cache = album folder → normalized release type (Studio/Live/Compilation/Single/EP/…), so the player can split an artist's album list into sections. - indexer.ts: parse the md table, normalize the Type (EP?→EP, Compilation (VA)→ Compilation, …), write discography.json. The artist folder's `v` now includes _discography.md so regenerating it re-syncs just that small JSON (isolated from the albums' meta/cover). Manifest gains `disco: true` on such entries. Also fixed the skip check to require all expected outputs to exist, so artist/ cover-only folders no longer rebuild every run. New `discographies` counter. - sidecar: GET /discography?path=<artist rel> (ETag/304), documented in the contract header. - MUSIC_API.md: §2.4 + manifest disco flag + resync algorithm updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
albumVersion,
|
||||
metaFilePath,
|
||||
coverFilePath,
|
||||
discographyFilePath,
|
||||
onIndexProgress,
|
||||
buildReport,
|
||||
} from './indexer';
|
||||
@@ -30,9 +31,14 @@ const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
|
||||
//
|
||||
// GET /stream?path=<home-relative> audio with Range→206 (Content-Range/Length/Accept-Ranges)
|
||||
// + `X-Audio-Duration` (seconds, ffprobe). 400/404/416.
|
||||
// GET /manifest { version, generatedAt, albums: { "<rel>": { v, cover, tracks } } }
|
||||
// GET /manifest { version, generatedAt, albums: { "<rel>": { v, cover, tracks, disco? } } }
|
||||
// GET /meta?path=<rel> album meta.json (IndexMeta). ETag: <v>; If-None-Match → 304.
|
||||
// GET /cover?path=<rel> compressed cover.jpg. ETag: <v>; If-None-Match → 304.
|
||||
// GET /discography?path=<artist rel> artist discography.json (only where manifest entry has
|
||||
// disco:true) = { artist, albums: { "<[year] album folder>":
|
||||
// "<Type>" } }, Type ∈ Studio/Live/Compilation/Single/EP/…
|
||||
// ETag: <v>; If-None-Match → 304. Source: each artist folder's
|
||||
// _discography.md (normalized; the md itself is never modified).
|
||||
// POST /reindex start an async build; returns IndexStatus (running: true).
|
||||
// GET /reindex/status IndexStatus snapshot.
|
||||
// GET /reindex/stream SSE. Triggers a build if idle (`?trigger=0` = watch-only).
|
||||
@@ -135,21 +141,21 @@ const server = Bun.serve({
|
||||
// ── Sync surface ──
|
||||
if (url.pathname === '/manifest') return json(await getManifest());
|
||||
|
||||
if (url.pathname === '/meta' || url.pathname === '/cover') {
|
||||
if (url.pathname === '/meta' || url.pathname === '/cover' || url.pathname === '/discography') {
|
||||
const rel = url.searchParams.get('path');
|
||||
if (rel === null) return new Response('path is required', { status: 400 });
|
||||
const isMeta = url.pathname === '/meta';
|
||||
const file = isMeta ? metaFilePath(rel) : coverFilePath(rel);
|
||||
if (!file) return new Response('Invalid path', { status: 400 });
|
||||
if (!(await Bun.file(file).exists())) return new Response('Not found', { status: 404 });
|
||||
const spec = {
|
||||
'/meta': { file: metaFilePath(rel), type: 'application/json' },
|
||||
'/cover': { file: coverFilePath(rel), type: 'image/jpeg' },
|
||||
'/discography': { file: discographyFilePath(rel), type: 'application/json' },
|
||||
}[url.pathname]!;
|
||||
if (!spec.file) return new Response('Invalid path', { status: 400 });
|
||||
if (!(await Bun.file(spec.file).exists())) return new Response('Not found', { status: 404 });
|
||||
|
||||
const v = await albumVersion(rel);
|
||||
if (v && req.headers.get('if-none-match') === v) return new Response(null, { status: 304 });
|
||||
return new Response(Bun.file(file), {
|
||||
headers: {
|
||||
'Content-Type': isMeta ? 'application/json' : 'image/jpeg',
|
||||
...(v ? { ETag: v } : {}),
|
||||
},
|
||||
return new Response(Bun.file(spec.file), {
|
||||
headers: { 'Content-Type': spec.type, ...(v ? { ETag: v } : {}) },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user