Tag albums service: album lookup API for Home Assistant and assignment web page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 16:56:21 +00:00
co-authored by Claude Opus 5.5
commit eaf77dd0de
20 changed files with 1358 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import pytest
from fastapi.testclient import TestClient
from tag_albums.app import create_app
from tag_albums.config import Settings
from tag_albums.library import Album
from tag_albums.store import Store
TOKEN = "test-token"
AUTH = {"Authorization": f"Bearer {TOKEN}"}
class FakeLibrary:
def __init__(self):
self.queries = []
def search_albums(self, query, limit=12):
self.queries.append(query)
return [Album(artist="Tool", album="Lateralus", image="http://img/1")]
@pytest.fixture
def store(tmp_path):
s = Store(str(tmp_path / "test.sqlite3"))
yield s
s.close()
@pytest.fixture
def library():
return FakeLibrary()
@pytest.fixture
def client(store, library):
settings = Settings(db_path=":unused:", api_token=TOKEN, ha_url="http://ha", ha_token="x")
return TestClient(create_app(settings, store=store, library=library))
+79
View File
@@ -0,0 +1,79 @@
from conftest import AUTH
from tag_albums.cli import import_yaml
from tag_albums.store import Store
def test_api_requires_token(client):
assert client.post("/api/scans", json={"tag_id": "AA"}).status_code == 401
assert client.get("/api/tags/AA", headers={"Authorization": "Bearer wrong"}).status_code == 401
def test_first_scan_creates_unassigned_tag(client, store):
r = client.post("/api/scans", json={"tag_id": "53-13-0b-2a"}, headers=AUTH)
assert r.status_code == 200
assert r.json() == {"tag_id": "53-13-0B-2A", "assigned": False}
tag = store.get("53-13-0B-2A")
assert tag.scan_count == 1 and not tag.assigned
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 store.get("86-2C-1D-BD").scan_count == 1
def test_empty_tag_id_rejected(client):
assert client.post("/api/scans", json={"tag_id": " "}, headers=AUTH).status_code == 422
def test_lookup_is_read_only(client, store):
assert client.get("/api/tags/AA-BB", headers=AUTH).status_code == 404
assert store.get("AA-BB") is None
store.record_scan("AA-BB")
r = client.get("/api/tags/aa-bb", headers=AUTH)
assert r.json() == {"tag_id": "AA-BB", "assigned": False}
assert store.get("AA-BB").scan_count == 1
def test_assign_unassign_delete_from_web_page(client, store):
store.record_scan("AA-BB")
r = client.post("/tags/AA-BB/assign", data={"artist": "Tool", "album": "Lateralus", "image": ""},
follow_redirects=False)
assert r.status_code == 303
tag = store.get("AA-BB")
assert (tag.artist, tag.album, tag.image) == ("Tool", "Lateralus", None)
client.post("/tags/AA-BB/unassign")
assert not store.get("AA-BB").assigned
client.post("/tags/AA-BB/delete")
assert store.get("AA-BB") is None
def test_pages_render(client, store):
store.record_scan("AA-BB")
store.assign("CC-DD", "Tool", "Lateralus")
home = client.get("/")
assert "AA-BB" in home.text and "Lateralus" in home.text
assert "AA-BB" in client.get("/unassigned").text
assert client.get("/tags/AA-BB").status_code == 200
assert client.get("/tags/ZZ").status_code == 404
def test_search_results(client, library):
r = client.get("/tags/AA-BB/search", params={"q": " lateralus "})
assert library.queries == ["lateralus"]
assert "Lateralus" in r.text and 'action="/tags/AA-BB/assign"' in r.text
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()
+32
View File
@@ -0,0 +1,32 @@
import json
import httpx
from tag_albums.library import MusicLibrary
def test_search_albums_parses_music_assistant_response():
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(request)
if request.url.path == "/api/config/config_entries/entry":
return httpx.Response(200, json=[{"entry_id": "ma-entry"}])
body = json.loads(request.content)
assert body == {"config_entry_id": "ma-entry", "name": "lateralus", "media_type": ["album"], "limit": 12}
return httpx.Response(200, json={"service_response": {"albums": [
{"name": "Lateralus", "artists": [{"name": "Tool"}, {"name": "Guest"}], "image": "http://img/1"},
{"name": "No Artist", "artists": []},
]}})
client = httpx.Client(base_url="http://ha", transport=httpx.MockTransport(handler))
library = MusicLibrary("http://ha", "token", client=client)
albums = library.search_albums("lateralus")
assert [(a.artist, a.album, a.image) for a in albums] == [
("Tool", "Lateralus", "http://img/1"),
("", "No Artist", None),
]
library.search_albums("lateralus")
# The Music Assistant config entry is looked up once, then cached.
assert sum(c.url.path == "/api/config/config_entries/entry" for c in calls) == 1