diff --git a/bunfig.toml b/bunfig.toml index a541ec98..30ea0dc1 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -22,4 +22,13 @@ env = "BUN_PUBLIC_*" coverage = true coverageDir = "coverage" preload = ["./test-setup.ts"] -root = "./src" +# The repo, not just `src` — a plugin's tests are the platform's tests. +# +# This was "./src" until 2026-08-15, when music became `plugins/music/` and took `lyrics.test.ts` with +# it. `bun test` then stopped running it and said nothing: the count fell by nine and the suite still +# read green-ish. A test that quietly stops running is worse than one that fails, and every future +# extraction would have taken its tests out of the suite the same way. +# +# Positional filters do not help — `bun test plugins` matches paths UNDER root, so it finds +# `src/servers/plugins/` and not `plugins/`. Root is the only lever. +root = "." diff --git a/MUSIC_API.md b/plugins/music/MUSIC_API.md similarity index 72% rename from MUSIC_API.md rename to plugins/music/MUSIC_API.md index d6431fd6..2b1f6172 100644 --- a/MUSIC_API.md +++ b/plugins/music/MUSIC_API.md @@ -28,13 +28,13 @@ GET /api/music/stream?path=&token= Byte-range streaming so the player can **seek without downloading the whole file**. -| Case | Status | Headers | -|---|---|---| -| No `Range` | `200` | `Content-Type`, `Content-Length`, `Accept-Ranges: bytes`, `X-Audio-Duration` | -| With `Range: bytes=…` | `206` | `Content-Range`, `Content-Length`, `Accept-Ranges: bytes`, `Content-Type`, `X-Audio-Duration` | +| Case | Status | Headers | +| --------------------- | ------ | --------------------------------------------------------------------------------------------- | +| No `Range` | `200` | `Content-Type`, `Content-Length`, `Accept-Ranges: bytes`, `X-Audio-Duration` | +| With `Range: bytes=…` | `206` | `Content-Range`, `Content-Length`, `Accept-Ranges: bytes`, `Content-Type`, `X-Audio-Duration` | - **`X-Audio-Duration`**: track duration in **seconds** (ffprobe-derived). Read this to set the player's - duration up front — it's the fix for AVPlayer reporting an *indefinite* duration on progressively-streamed + duration up front — it's the fix for AVPlayer reporting an _indefinite_ duration on progressively-streamed VBR MP3s. No need to scan the file. - Errors: `400` invalid/missing path · `404` not found · `416` bad range. @@ -51,26 +51,28 @@ The server maintains a cache tree that **mirrors the library**, one entry per al this instead of walking + ID3-parsing the library itself. Each album has a **version stamp `v`** (hash of the album's source files' names/sizes/mtimes + its cover). -`v` changes **iff the album's content changed** → it's the whole basis of the diff: *unchanged `v` ⇒ skip*. +`v` changes **iff the album's content changed** → it's the whole basis of the diff: _unchanged `v` ⇒ skip_. ### 2.1 Manifest — one call, whole library ``` GET /api/music/manifest ``` + ```jsonc { "version": 1, - "generatedAt": 1785034701973, // ms; when the index was last built + "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/Metallica/[1989] Live Shit": { "v": "beefbeefbeefbee", "cover": true, "tracks": 0, "videos": 2 }, - "Albums/AC-DC": { "v": "c0ffee1234567890", "cover": true, "tracks": 0, "disco": true } + "Albums/AC-DC": { "v": "c0ffee1234567890", "cover": true, "tracks": 0, "disco": true }, // … - } + }, } ``` + `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). **`videos: N`** (optional) counts video files (concerts, clips) that live @@ -82,14 +84,16 @@ may have any mix of `tracks`, `videos`, and `disco`. ``` GET /api/music/meta?path= ``` + Returns the album's `meta.json`. Sends `ETag: `; a request with `If-None-Match: ` returns `304`. + ```jsonc { "path": "Albums/AC-DC/[1980] Back in Black", - "cover": "cover.jpg", // present only if a cover exists + "cover": "cover.jpg", // present only if a cover exists "tracks": [ { - "file": "01 Hells Bells.mp3", // filename within the album folder + "file": "01 Hells Bells.mp3", // filename within the album folder "title": "Hells Bells", "artist": "AC/DC", "albumArtist": "AC/DC", @@ -97,23 +101,25 @@ Returns the album's `meta.json`. Sends `ETag: `; a request with `If-None-Matc "track": "1", "year": "1980", "durationSec": 312, - "lyrics": "lrc" // present if lyrics exist: "lrc" = synced, "txt" = plain (see §2.3.2) - } + "lyrics": "lrc", // present if lyrics exist: "lrc" = synced, "txt" = plain (see §2.3.2) + }, // … ], - "videos": [ // present only for folders that contain video files + "videos": [ + // present only for folders that contain video files { - "file": "1989 - Seattle.mp4", // filename within the folder - "title": "Live Shit: Seattle", // from the container title tag, if any + "file": "1989 - Seattle.mp4", // filename within the folder + "title": "Live Shit: Seattle", // from the container title tag, if any "durationSec": 8130, "width": 1280, "height": 720, - "poster": "posters/1989 - Seattle.mp4.jpg" // present when a poster was generated (see §2.3.1) - } + "poster": "posters/1989 - Seattle.mp4.jpg", // present when a poster was generated (see §2.3.1) + }, // … - ] + ], } ``` + All track/video fields except `file` are optional (absent when the tag/stream info is missing). `videos` is omitted entirely when the folder has none. To stream a track or video: `GET /api/music/stream?path=Music//` (byte-range; works for `.mp4`). @@ -123,6 +129,7 @@ To stream a track or video: `GET /api/music/stream?path=Music//` (byt ``` GET /api/music/cover?path= ``` + Compressed JPEG (≤600px on the long edge, ~30–80 KB). Sends `ETag: `; `If-None-Match: ` → `304`. Only meaningful when the manifest entry has `"cover": true`. @@ -131,6 +138,7 @@ Only meaningful when the manifest entry has `"cover": true`. ``` GET /api/music/poster?path=&file=