Play albums by their Music Assistant URI

Names can match near-duplicate library albums that differ only in capitals (Let the/The Tribe Increase), and Music Assistant picked the unplayable one. Assignments now store the album's library URI, scans return it after checking it's still in the library (re-finding it by name, exact spelling first, if not), and HA plays by it. sync-library (formerly fetch-covers) links existing assignments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 00:23:54 +00:00
co-authored by Claude Opus 5.5
parent 91e4519503
commit a2f421c272
8 changed files with 217 additions and 73 deletions
+63 -22
View File
@@ -3,7 +3,8 @@ import sqlite3
from conftest import AUTH
from tag_albums.cli import fetch_covers, import_yaml
from tag_albums.cli import import_yaml, sync_library
from tag_albums.library import Album
from tag_albums.store import Store
@@ -23,7 +24,8 @@ def test_first_scan_creates_unassigned_tag(client, store):
def test_scan_of_assigned_tag_returns_album(client, store):
store.assign("86-2C-1D-BD", "Tool", "Lateralus")
r = client.post("/api/scans", json={"tag_id": "86-2C-1D-BD"}, headers=AUTH)
assert r.json() == {"tag_id": "86-2C-1D-BD", "assigned": True, "artist": "Tool", "album": "Lateralus"}
assert r.json() == {"tag_id": "86-2C-1D-BD", "assigned": True, "artist": "Tool", "album": "Lateralus",
"uri": "library://album/1868"}
assert store.get("86-2C-1D-BD").scan_count == 1
@@ -158,27 +160,19 @@ def test_old_database_is_migrated(tmp_path):
store.close()
def test_fetch_covers_for_imported_assignments(store, library):
store.assign("A", "Tool", "Lateralus") # imported: no image URL
def test_sync_library_links_and_fetches_covers(store, library):
store.assign("A", "Tool", "Lateralus") # imported: names only
store.assign("B", "Amenra", "De Doorn") # not in the library
found, missing = fetch_covers(store, library)
assert found == ["Tool – Lateralus"] and missing == ["Amenra – De Doorn"]
report = sync_library(store, library)
assert report.linked == ["Tool – Lateralus"] and report.unmatched == ["Amenra – De Doorn"]
assert report.covers == ["Tool – Lateralus"] and report.coverless == ["Amenra – De Doorn"]
tag = store.get("A")
assert tag.has_cover and tag.image == "http://img/1"
assert fetch_covers(store, library) == ([], ["Amenra – De Doorn"]) # already stored: skipped
assert tag.uri == "library://album/1868" and tag.has_cover and tag.image == "http://img/1"
again = sync_library(store, library) # done ones are skipped
assert (again.linked, again.covers) == ([], [])
def test_import_yaml(tmp_path):
src = tmp_path / "tag_albums.yaml"
src.write_text('"29-92-68-C5":\n artist: ONRUST\n album: Van Woede Tot Wanhoop\n')
db = str(tmp_path / "db.sqlite3")
assert import_yaml(str(src), db) == 1
store = Store(db)
assert store.get("29-92-68-C5").album == "Van Woede Tot Wanhoop"
store.close()
def test_fetch_covers_stops_when_music_assistant_is_unreachable(store, library):
def test_sync_library_stops_when_music_assistant_is_unreachable(store, library):
import httpx
import pytest
@@ -186,11 +180,9 @@ def test_fetch_covers_stops_when_music_assistant_is_unreachable(store, library):
raise httpx.ConnectTimeout("timed out")
library.fetch_image = unreachable
library.image_download_url = lambda url: url
store.assign("A", "Tool", "Lateralus")
store.assign("B", "Kiasmos", "Kiasmos")
with pytest.raises(SystemExit, match="Set MA_URL"):
fetch_covers(store, library)
sync_library(store, library)
def test_settings_read_ma_url(monkeypatch):
@@ -201,3 +193,52 @@ def test_settings_read_ma_url(monkeypatch):
assert Settings.from_env().ma_url == "https://ma.example"
monkeypatch.delenv("MA_URL")
assert Settings.from_env().ma_url is None
# --- Playing by URI -----------------------------------------------------------
def _scan(client, tag_id):
return client.post("/api/scans", json={"tag_id": tag_id}, headers=AUTH).json()
def test_assign_from_search_stores_the_uri(client, store):
client.post("/tags/AA-BB/assign", data=_search_form(client))
assert store.get("AA-BB").uri == "library://album/1868"
assert _scan(client, "AA-BB")["uri"] == "library://album/1868"
def test_scan_links_names_only_assignment_to_exact_spelling(client, store, library):
# The case that broke: two library entries differing only in capitals,
# and only the one spelled like the assignment is playable.
library.library = [
Album("The Mob", "Let The Tribe Increase", None, "library://album/1183"),
Album("The Mob", "Let the Tribe Increase", None, "library://album/2846"),
]
store.assign("MOB", "The Mob", "Let the Tribe Increase")
assert _scan(client, "MOB")["uri"] == "library://album/2846"
assert store.get("MOB").uri == "library://album/2846"
def test_scan_replaces_a_uri_the_library_no_longer_has(client, store, library):
store.assign("A", "Tool", "Lateralus", uri="library://album/9999") # before a rebuild
assert _scan(client, "A")["uri"] == "library://album/1868"
assert store.get("A").uri == "library://album/1868"
def test_scan_falls_back_to_names_when_nothing_matches(client, store, library):
store.assign("A", "Amenra", "De Doorn", uri="library://album/9999")
body = _scan(client, "A")
assert body["uri"] is None and body["album"] == "De Doorn"
assert store.get("A").uri == "library://album/9999" # kept, in case it comes back
def test_scan_trusts_stored_uri_when_library_is_unreachable(client, store, library):
store.assign("A", "Tool", "Lateralus", uri="library://album/1868")
library.fail_search = True
assert _scan(client, "A")["uri"] == "library://album/1868"
def test_lookup_returns_stored_uri_without_searching(client, store, library):
store.assign("A", "Tool", "Lateralus", uri="library://album/1868")
r = client.get("/api/tags/A", headers=AUTH).json()
assert r["uri"] == "library://album/1868" and library.queries == []