Files
tag-albums/README.md
T
twislaandClaude Opus 5.5 5878adb756 Let a tag play several albums in order
An assignment is now an ordered list of albums (e.g. one vinyl compiling two EPs that are separate releases in the library). Albums move to their own table, with a migration for existing databases. The scan and lookup responses return an albums list instead of the single artist/album/uri fields. The tag page lists its albums with up, down and remove, and search results get an Add button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 11:44:44 +00:00

7.7 KiB

Tag albums

Tells Home Assistant which album to play when an RFID card (a Tag) is scanned, and provides a web page to give new cards an album (an Assignment). See CONTEXT.md for the vocabulary.

How it fits together:

  1. The ESPHome RFID reader sends tag_scanned to Home Assistant, as before.
  2. HA's Play album by tag script calls POST /api/scans. The service records the Scan and answers with the album, or assigned: false.
  3. A card the service hasn't seen before becomes an Unassigned tag. It appears on the web page within a few seconds, where you search the Music Assistant library and assign an album.
  4. When a card is removed, HA's Stop album by tag script calls GET /api/tags/{tag_id}. It stops the player only if that tag has an album.

Demo

A card is scanned while the page is open, shows up under Unassigned tags, and gets an album from a live search of the Music Assistant library:

Scanning a new card and assigning it an album

API

Both endpoints need Authorization: Bearer <API_TOKEN>.

Endpoint Purpose
POST /api/scans {"tag_id": "53-13-0B-2A"} Record a Scan; creates the Tag on first sight. Returns {"tag_id", "assigned", "albums": [{"artist", "album", "uri"}, …]}, the albums in play order (empty when unassigned). Each uri is the album's Music Assistant URI (e.g. library://album/2846), checked against the library on every scan; null means play by name.
GET /api/tags/{tag_id} Read-only lookup, same response, with the stored URIs unchecked. 404 if the tag has never been seen.

Tag IDs are normalised to upper case.

A tag usually plays one album, but can play several in order, e.g. a vinyl that compiles two EPs the library has as separate releases. HA plays the first album (replacing the queue) and queues the others after it. On the web page, Add appends an album to a tag and Assign replaces all of them; the tag's page reorders and removes them.

HA plays by uri when there is one, because a library can hold near-duplicate albums whose names differ only in capitals, and only one of them may be playable. On each scan the service checks the stored URI is still in the library. If it's gone (e.g. after a library rebuild), it finds the album by name again, preferring the exact spelling, and stores the new URI.

The web page (/) has no login: keep the service on the LAN.

Configuration

Environment variables:

Variable Meaning
API_TOKEN Shared secret Home Assistant sends in the Authorization header
HA_URL Home Assistant base URL, e.g. https://ha.home.knbg
HA_TOKEN HA long-lived access token, used to search the Music Assistant library
DB_PATH SQLite file (default tag_albums.sqlite3); its directory must be writable
MA_URL Optional. Address to download Music Assistant cover images from, e.g. https://ma.home.knbg, when Music Assistant's own address (http://<host>:8095) isn't reachable from the service. See Album covers.

HTTPS to HA is verified against the system trust store, so a home CA must be installed on the host. If it isn't, point SSL_CERT_FILE at the CA bundle.

Development

uv sync
uv run pytest
API_TOKEN=dev HA_URL=https://ha.home.knbg HA_TOKEN=... uv run tag-albums serve --port 8087

Running with Docker

For local testing. The container has to trust the home CA to reach HA, so mount the CA certificate and point SSL_CERT_FILE at it:

docker build -t tag-albums .
docker run --rm -p 8087:8087 -v tag-albums-data:/data \
  -e API_TOKEN=dev -e HA_URL=https://ha.home.knbg -e HA_TOKEN=... \
  -v /usr/local/share/ca-certificates/KNBG_Root_CA_306526670621344964268144771797781855543.crt:/ca.crt:ro \
  -e SSL_CERT_FILE=/ca.crt \
  tag-albums

To import assignments, run tag-albums import-yaml inside the container, e.g. docker run --rm -v tag-albums-data:/data -v $PWD/tag_albums.yaml:/import.yaml:ro tag-albums tag-albums import-yaml /import.yaml.

Deploying in a FreeBSD jail

pkg install python313 py313-sqlite3 py313-uv git rust   # any Python >= 3.11; sqlite3 is a separate package
# uv compiles pydantic-core (Rust) and a few C extensions from source, since
# FreeBSD has no prebuilt wheels. The jail needs the base system's development
# files for that (e.g. /usr/lib/Scrt1.o, crti.o, libc): on a pkgbase jail,
# install the FreeBSD-*-dev packages (or the devel set) from the FreeBSD-base repo.
pw useradd tagalbums -d /nonexistent -s /usr/sbin/nologin
git clone <repo> /usr/local/tag-albums && cd /usr/local/tag-albums && uv sync --no-dev
install -d -o tagalbums /var/db/tag_albums

cat > /usr/local/etc/tag_albums.env <<'EOF'
API_TOKEN=<random, e.g. openssl rand -hex 32>
HA_URL=https://ha.home.knbg
HA_TOKEN=<HA long-lived token>
DB_PATH=/var/db/tag_albums/tag_albums.sqlite3
EOF
chmod 600 /usr/local/etc/tag_albums.env

install -m 755 deploy/rc.d/tag_albums /usr/local/etc/rc.d/tag_albums
sysrc tag_albums_enable=YES
service tag_albums start

Logs go to syslog with the tag tag_albums. The port defaults to 8087 (sysrc tag_albums_port=...).

Importing the old tag_albums.yaml

One time, before switching Home Assistant over:

cd /usr/local/tag-albums
env DB_PATH=/var/db/tag_albums/tag_albums.sqlite3 \
  su -m tagalbums -c '.venv/bin/tag-albums import-yaml /path/to/tag_albums.yaml'

Linking assignments to the library

Albums assigned from the web page store their library URI and cover straight away. Imported assignments only have names, so link them once. This finds each album (exact spelling first), stores its URI and downloads its cover. It reads the env file for HA access:

cd /usr/local/tag-albums
su -m tagalbums -c 'set -a; . /usr/local/etc/tag_albums.env; set +a; .venv/bin/tag-albums sync-library'

It lists albums it couldn't find (those keep playing by name) and albums with no cover in Music Assistant. Running it again only looks at assignments still missing something. fetch-covers is an older name for the same command. Scanning an assignment without a URI also links it.

Album covers

The service stores each assignment's cover and serves it itself. Music Assistant's image URLs are plain http on another host, which browsers block on an https page. sync-library (above) fetches covers for imported assignments; albums with no cover in Music Assistant keep the grey placeholder.

The service downloads covers from the address Music Assistant reports: its own host and port 8095, over plain http. If the jail can't reach that, put Music Assistant's image proxy behind the reverse proxy and set MA_URL to it. For example, in Caddy (use the same TLS setup as your other sites):

ma.home.knbg {
	@images path /imageproxy/*
	handle @images {
		reverse_proxy 172.16.40.250:8095
	}
	respond 404
}

Then add MA_URL=https://ma.home.knbg to the env file and restart the service. Only /imageproxy/* is exposed, not the Music Assistant interface. If sync-library can't connect, it stops at the first cover and says so.

Updating

cd /usr/local/tag-albums && git pull && uv sync --no-dev
service tag_albums restart

Database changes are applied automatically at startup.

Home Assistant side

These pieces live in the home-assistant-config repo:

  • configuration.yaml: two rest_commands, tag_albums_scan and tag_albums_lookup, pointing at https://tag-albums.home.knbg (a reverse proxy in front of port 8087). They set verify_ssl: false, because the certificate comes from the home CA and HA only trusts its built-in list of public CAs.
  • secrets.yaml: tag_albums_auth: "Bearer <API_TOKEN>".
  • scripts.yaml: play_album_by_tag and stop_album_by_tag call those commands.

If the service is down, a scan plays nothing and play_album_by_tag writes a warning to the HA log.