readme: what music does, what it needs, and what it does not do yet

This commit is contained in:
2026-08-16 04:07:20 +00:00
parent 6e07da7a36
commit 5bd27838bd
+145
View File
@@ -0,0 +1,145 @@
# Music
The music library for an Officer server: browse `~/Music`, play it in the browser, and serve a synced
index to phone and tablet apps.
Music assumes you already have a library on disk — folders of files you organised yourself — and does
not try to become the place you organise it. It indexes what is there, compresses the covers, reads the
tags and lyrics, and serves the result over HTTP. The `/music` screen is one client of that API; the
mobile apps are another. Nothing is imported, moved or renamed, and removing the plugin leaves the
library exactly as it was.
## What you get
- **A library screen** at `/music` — a drill-down list on the left, the detail view on the right. An
album shows its tracklist, an artist shows its albums grouped by release type, anything else shows a
folder grid. Every location is a link, so `?path=` is the address of what you are looking at and
cmd-click works.
- **Gapless playback in the browser.** Web Audio rather than an `<audio>` element: the next track is
decoded ahead of time and scheduled to start at the sample the current one ends, so a continuous mix
has no seam on auto-advance.
- **Byte-range streaming** with `X-Audio-Duration` on the response, so a player knows the length of a
VBR MP3 without scanning it.
- **Lyrics**, synced or plain, in a split pane beside the tracklist. Sources in precedence order: an
external `<track>.lrc`, an external `<track>.txt`, then embedded tags. A `.txt` containing `[mm:ss]`
lines is served as synced.
- **Favourites** — tracks, albums and artists — per account, with their own view.
- **Resume.** The current track and position are saved per account and per device, so a reload lands
back where you were, paused.
- **A server-built index for mobile apps.** One manifest call returns every album with a version stamp;
a client re-fetches only the albums whose stamp changed. The full contract is [`MUSIC_API.md`](./MUSIC_API.md).
- **Playlists**, per account, ordered, over HTTP. There is no playlist UI in the web panels yet — see
the limitations.
## Requirements
- An Officer server, version 1.0.0 or later.
- `ffprobe` and `ffmpeg` on the host. Both come from the `ffmpeg` package on apt, pacman, dnf and brew.
The installer probes for them and offers to install the package if either is missing.
- Music under **`~/Music`** in the server owner's home. The root is not configurable.
Without `ffprobe` the indexer still completes and reports success, but every track carries only its
filename — no title, artist, album, duration or embedded lyrics. Without `ffmpeg` you lose cover
compression and video poster frames. This is why the two are declared separately: they fail differently
and you should be told which one you are missing.
## Install
From the Officer marketplace: find Music, install. That clones the repo into `plugins/music`, checks
the host binaries, applies the four database tables, mounts `/api/music` and `/music`, and starts the
`officer-music` sidecar under PM2.
Uninstalling removes the route and the process. **It never drops the tables**, so favourites, playlists
and resume state survive an uninstall and come back on re-install.
## Permissions
One permission, `music`, grantable per role at read or write like any other.
- **Read** lets an account browse the library, stream, and write its own favourites, playlists and
now-playing state. Those are writes, but only to the caller's own rows, which is what makes them safe
at read level.
- **Write** adds the rest of the surface — in practice, triggering a reindex.
The library index itself is one shared thing for the household. Favourites, playlists and now-playing
are scoped to the caller by the user id the platform proxy injects.
## Configuration
There is nothing to configure, and no environment variables. What the plugin reads off disk instead:
| On disk | What it means |
| -------------------------------------------------- | ----------------------------------------------------------------- |
| `~/Music/<library>/<artist>/<album>/` | The tree. Top-level folders under `Music` are libraries. |
| `folder.jpg`, `cover.jpg`, `folder.png`, `cover.png` | Album cover, first match wins. Served compressed to ≤600px. |
| `[1980] Back in Black` | Album folder naming — the year is parsed out for display. |
| `_discography.md` in an artist folder | Groups that artist's albums into Studio, Live, Compilation, EP… |
| `<track>.lrc` / `<track>.txt` beside a track | External lyrics, preferred over embedded tags. |
Audio: mp3, flac, m4a, aac, ogg, opus, wav, wma. Video: mp4, m4v, mkv, mov, webm, avi.
## How it works
The `officer-music` sidecar is a separate process under PM2 that listens on a loopback port and owns the
whole contract. `/api/music/*` in the platform is a proxy: it authenticates, then forwards. It holds no
music knowledge and no routes of its own.
The sidecar walks `~/Music` and writes a cache tree under the install's `data/music/cache/`, one folder
per album, holding `meta.json`, a compressed `cover.jpg`, video posters and extracted lyrics. Each album
carries a version stamp hashed from its files' names, sizes and mtimes plus its cover, so an incremental
rebuild skips everything that has not changed, and a client diffs the manifest to decide what to fetch.
Rebuilds are triggered, never watched — a recursive `fs.watch` on this library needed one inotify watch
per file and could not establish. The refresh button in the browser runs an incremental build; a full
from-scratch rebuild runs nightly at 3am into a fresh slot and swaps it in atomically only on success.
Four tables in the platform's Postgres, all keyed on the user and cascading from it: `music_favorites`,
`music_playlists`, `music_playlist_items`, `music_now_playing`.
```
manifest.ts identity, one permission, host dependencies
api/router.ts the proxy onto the sidecar
sidecar/ the /api/music contract: indexing, streaming, per-user state
db/ the four tables and their queries
web/ the two panels, the player engine and the lyrics pane
widgets/ a dashboard widget — parked, see below
cliamp/ a second, server-side playback path — parked, see below
scripts/ a CLI that drives a reindex and prints progress
```
`PLUGIN.md` records how this was extracted from the platform and what it cost. It is history, not
documentation; read `MUSIC_API.md` for the HTTP contract.
## Known limitations
Stated plainly rather than omitted.
- **Leaving `/music` stops playback.** The audio engine is mounted inside the library panel, so
navigating away unmounts it. Making audio outlive the route needs either a shell slot a plugin can
contribute to or the engine hoisted to module scope; neither exists yet. Returning to `/music`
reloads the queue from where it was.
- **The library listing goes through the platform's file browser**, not through this plugin, so it sits
behind the separate `files` permission. `files` is confined: it means nothing for an account without
its own Linux user. An account granted `music` but not `files` gets a working player, working
favourites and an **empty library**, with no error on screen. The owner is unaffected. Moving the
listing into the sidecar is the fix and is not done.
- **Playlists have no web UI.** The API is complete and the mobile apps use it; the `/music` screen
does not yet show or edit playlists.
- **Videos are indexed but not played in the browser.** The index records them, `/stream` serves them
and posters are generated, but the web panels ignore them. They are there for the mobile apps.
- **The gapless engine decodes whole tracks to PCM.** That is what buys the sample-accurate seam, and
it costs memory — roughly 200 MB for a ten-minute track, with at most three decoded buffers kept. A
manual skip therefore has a visible decode delay; auto-advance does not, because it was decoded ahead.
- **The index is not live.** Add an album and nothing appears until you press refresh or the nightly
rebuild runs. A targeted per-folder reindex exists in the code but nothing calls it.
- **`/queue` is listed in the permission's paths and no such route exists.** Carried across verbatim
from the platform entry this replaced, kept so no existing grant changed meaning.
- **`scripts/reindex-music.ts` looks for the sidecar port in the wrong place.** It reads
`DATA_PATH/music/.server`, falling back to `./data`, but the platform stopped using a `DATA_PATH`
environment variable and now derives the data directory as a sibling of the repo. Run it with
`DATA_PATH` set explicitly, or trigger a rebuild from the UI instead.
- **cliamp is parked.** A second playback path — the `cliamp` TUI running on the server with its audio
piped to the browser — ships in `cliamp/` but is not wired up. It needs a plugin to be able to own a
websocket, which the platform does not support.
- **The dashboard widget is parked.** `widgets/` holds a working library-browsing widget that nothing
registers, because plugins cannot contribute widgets yet.