146 lines
9.1 KiB
Markdown
146 lines
9.1 KiB
Markdown
# 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.
|