Lightweight MPD-backed music player
Check config.env (copied from config.env.example)
Ensure mpd running and has indexed music (use mpc) For dev: brew services start mpd
indexer/.venv/bin/hifi-index
cd service .venv/bin/hifi-service (with sudo for root)
cd frontend yarn dev
@spec-frontend.md @spec-indexer.md @spec-web-service.md
fixtures/ committed test fixture tree + bootstrap script
build.sh run by hand to (re)build music/ — not run by tests
music/ synthetic library (mp3/m4a, embedded + folder art, multi-disc, untagged)
expected/ committed snapshot of indexer output
indexer/ Python indexer package
hifi_indexer/ source
tests/ end-to-end snapshot test (spins up a real mpd)
Reads tags from MPD over a Unix socket, walks album directories for artwork, and emits:
index.json— client-facing artist/album/track tables (no file paths)paths.json— backend-only{trackId: relative/path}map for playbackart/<albumId>/{thumb,large}.webp— 300px and 800px WebP, q=80
Album id is sha256("<AlbumArtist||Artist>|<Album>")[:12], which is also
the artwork hash — so artwork URLs are /art/<album.i>/{thumb,large}.webp
and stable across rebuilds.
Requires python>=3.10, ffmpeg, and mpd on PATH.
cd indexer
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'The indexer talks to a running MPD via its Unix socket. By default it
asks MPD for music_directory; pass --music-dir to override.
.venv/bin/hifi-index --mpd-socket /run/mpd/socketOutput goes to $XDG_DATA_HOME/hifi/ (or ~/.local/share/hifi/).
Override with --out.
| flag | default | notes |
|---|---|---|
--mpd-socket PATH |
$MPD_HOST or /run/mpd/socket |
MPD Unix socket |
--music-dir PATH |
(read from MPD) | override MPD's music_directory |
--out DIR |
$XDG_DATA_HOME/hifi |
output directory |
--jobs N |
os.cpu_count() |
parallel ffmpeg workers |
--force |
off | rebuild artwork even if cached |
--skip-mpd-update |
off | skip the update + wait before reading tags |
index.json and paths.json are always rebuilt from scratch and written
atomically. Artwork is cached per album id — existing
art/<id>/{thumb,large}.webp is left alone unless --force.
rsync ... /path/to/music/
hifi-index --mpd-socket /run/mpd/socketThe indexer issues update to MPD and polls status until
updating_db clears before reading tags.
A track missing both Album and (AlbumArtist or Artist) is skipped
and logged to stderr; a summary count is printed at the end.
FastAPI app that serves index.json, album art, and proxies playback
control to MPD over its Unix socket. Listens on port 80 (hard-coded
— needs root or an equivalent capability/port-forward).
cd service
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'Assumes the indexer has already populated $HIFI_DATA_DIR (default
/var/lib/hifi) and MPD is reachable on its Unix socket (default
/tmp/mpd.sock, override with MPD_SOCKET).
.venv/bin/hifi-service
use sudo if running on port 80
End-to-end: the test fixture spawns a real mpd against
fixtures/music/, runs the indexer, and asserts equality with
fixtures/expected/{index,paths}.json plus presence of every artwork
file.
cd indexer
.venv/bin/pytest
.venv/bin/ruff check .
.venv/bin/ruff format --check .fixtures/music/ and fixtures/expected/ are committed. Don't
regenerate casually. If a code change forces a snapshot update, call it
out explicitly.
cd fixtures && ./build.sh # regenerate the synthetic library
# then re-run the indexer against it and copy index.json / paths.json
# (and art/) into fixtures/expected/