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:
2026-07-26 09:39:11 +00:00
co-authored by Claude Opus 4.8
parent 592cc72f85
commit 946da85e4c
3 changed files with 134 additions and 23 deletions
+38 -5
View File
@@ -64,12 +64,15 @@ GET /api/music/manifest
"generatedAt": 1785034701973, // ms; when the index was last built
"albums": {
"Albums/AC-DC/[1980] Back in Black": { "v": "50856380f1ca8f9", "cover": true, "tracks": 10 },
"DJ Sets/Dave Clarke": { "v": "a1b2c3d4e5f6a7b", "cover": false, "tracks": 3 }
"DJ Sets/Dave Clarke": { "v": "a1b2c3d4e5f6a7b", "cover": false, "tracks": 3 },
"Albums/AC-DC": { "v": "c0ffee1234567890", "cover": true, "tracks": 0, "disco": true }
// …
}
}
```
`404` if the index has never been built (see §3).
`404` if the index has never been built (see §3). Entries with **`tracks: 0`** are container folders (e.g. an
**artist** folder). An entry with **`disco: true`** is an artist folder that has a discography — fetch its
grouping via `/discography` (§2.4).
### 2.2 Album metadata
@@ -107,6 +110,36 @@ GET /api/music/cover?path=<rel>
Compressed JPEG (≤600px on the long edge, ~3080 KB). Sends `ETag: <v>`; `If-None-Match: <v>``304`.
Only meaningful when the manifest entry has `"cover": true`.
### 2.4 Discography (artist album grouping)
For artist folders (manifest entry with `"disco": true`), this returns a map of **album folder → release
type**, so the player can split an artist's album list into sections (Studio, Live, Compilation, Single, EP…).
```
GET /api/music/discography?path=<artist rel> e.g. path=Albums/AC-DC
```
Sends `ETag: <v>`; `If-None-Match: <v>``304`.
```jsonc
{
"artist": "Anthrax",
"albums": {
"[1984] Fistful Of Metal": "Studio",
"[1985] Armed And Dangerous": "EP",
"[1994] The Island Years": "Live",
"[1991] Attack Of The Killer B's": "Compilation"
// …
}
}
```
- Keys are **album folder names** (`[year] title`) — they map 1:1 to the artist's album folders, i.e. the
last path segment of that album's manifest `<rel>`. Group the artist's albums by looking each up here.
- **Types** are a normalized set: `Studio`, `Live`, `Compilation`, `Single`, `EP`, `Soundtrack`, `Remix`,
`DJ-Mix`, `Demo`, `Mixtape`, `Bootleg`, `Other` (unknown values pass through as-is). The player defines
section order.
- An album folder **not present** here has no classification → put it in an "Other"/uncategorized section.
- Source of truth is each artist's `_discography.md` (author-maintained); this JSON is derived from it and
re-generated whenever that file changes (its `v` bumps independently of the albums' `meta`/`cover`).
---
## 3. Building / refreshing the index
@@ -127,7 +160,7 @@ GET /api/music/reindex/status → IndexStatus snapshot
"running": true,
"startedAt": 1785034701973, "finishedAt": null,
"foldersScanned": 45, "albumsBuilt": 12, "albumsSkipped": 3,
"tracksIndexed": 320, "coversSaved": 12,
"tracksIndexed": 320, "coversSaved": 12, "discographies": 3,
"currentPath": "Albums/AC-DC/[1980] Back in Black",
"error": null
}
@@ -153,7 +186,7 @@ data: {"albums":15,"built":12,"skipped":3,"foldersScanned":45,"tracksIndexed":32
`IndexReport` (the `done` payload):
```jsonc
{ "albums": 15, "built": 12, "skipped": 3, "foldersScanned": 45,
"tracksIndexed": 320, "coversSaved": 12, "elapsedSec": 37.2, "error": null }
"tracksIndexed": 320, "coversSaved": 12, "discographies": 3, "elapsedSec": 37.2, "error": null }
```
> First build of a large library takes a few minutes; re-runs are near-instant (unchanged albums skip via `v`).
@@ -167,7 +200,7 @@ Keep the last `manifest.albums` you synced. On resync:
1. `GET /api/music/manifest`.
2. For each `<rel>` in the new manifest:
- **new**, or **`v` differs** from your stored copy → fetch `GET /meta?path=<rel>` (+ `GET /cover?path=<rel>`
if `cover:true`); store them under your local `<rel>/`.
if `cover:true`, + `GET /discography?path=<rel>` if `disco:true`); store them under your local `<rel>/`.
- **`v` unchanged** → **skip** (no download).
3. For each `<rel>` you have locally that's **absent** from the new manifest → delete it.
4. Save the new manifest as your baseline.