the previous commit read the duration from meta.json instead of spawning ffprobe. correct for an indexed album and wrong everywhere else: /stream serves any home-relative path and never required an index, so a library that has not been indexed got no header at all. alpha is exactly that case — its music cache is empty — which is how this was caught rather than shipped. "always present" replaced with "present if indexed" is the same regression as removing the header, one layer down. so both, in order: meta.json when it can answer, ffprobe when it cannot, each cached. verified on an indexed album (247 from meta, 0.3ms warm) and on the same file with its index deleted (7.03 from ffprobe, 83ms cold, 0.1ms warm).
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-Durationon 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.txtcontaining[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. - 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.
ffprobeandffmpegon the host. Both come from theffmpegpackage on apt, pacman, dnf and brew. The installer probes for them and offers to install the package if either is missing.- Music under
~/Musicin 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
/musicstops 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/musicreloads 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
filespermission.filesis confined: it means nothing for an account without its own Linux user. An account grantedmusicbut notfilesgets 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
/musicscreen does not yet show or edit playlists. - Videos are indexed but not played in the browser. The index records them,
/streamserves 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.
/queueis 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.tslooks for the sidecar port in the wrong place. It readsDATA_PATH/music/.server, falling back to./data, but the platform stopped using aDATA_PATHenvironment variable and now derives the data directory as a sibling of the repo. Run it withDATA_PATHset explicitly, or trigger a rebuild from the UI instead.- cliamp is parked. A second playback path — the
cliampTUI running on the server with its audio piped to the browser — ships incliamp/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.