diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 17:12:36 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 17:12:36 +0200 |
| commit | 941d1a135dd7b03834576855e8e9fdaa24c4e406 (patch) | |
| tree | 5d7c27d45a3f1e77320e089f6a4522b8383cbd45 /packages/meshbay-node/tests/test_media_cache.py | |
| parent | 16bc07acf053d7d14f8182f5523da1d179154a15 (diff) | |
| download | meshbay-941d1a135dd7b03834576855e8e9fdaa24c4e406.tar.gz | |
feat(node): Music app node-side — indexing, MusicBrainz enrichment, protocol
Implements the node half of docs/musicbay.md against MNP 0.8:
- IndexEntry gains artist/album/track_no (reuses duration/thumb_hash/
display_title, already generic). New musicbrainz_config/_enabled and
music_meta_req/_resp message pairs, mirroring the TMDB shape.
- title_parse.parse_track_filename: track-number-prefix + title parsing,
fallback-only (embedded tags are the primary source, unlike Videos).
- indexer.enrich_audio.AudioEnricher: mutagen-based tag/embedded-cover
extraction through its own bounded pool (asyncio.to_thread, no
subprocess — no ffmpeg-shaped deadlock risk). Gated on "music" in a
group's enabled_apps rather than a video_root-style scoped folder.
- musicbrainz.py: MusicBrainzClient — no API key (unlike TMDB), just a
self-imposed ~1 req/s pace and a configurable, non-default User-Agent
contact string; inert (no calls at all) when no contact is configured,
never sends an unidentified client.
- media_cache.py: file_mbid/mbid_meta tables alongside the existing TMDB
ones, cover art reusing the thumbs table via a synthetic
musicbrainz:{mbid} id, pruned on file deletion.
- roster.py/ops.py/webrtc_server.py: musicbrainz_contact (node-wide) and
musicbrainz_enabled (per-group, from the start) as signed operator
settings, ALLOWED_APPS gains "music", _do_music_meta_request resolves
and caches a release-level MusicBrainz match per (artist, album).
- daemon.py: AudioEnricher/MusicBrainzClient wired alongside the video
ones; a group's existing library is swept when "music" is newly
enabled (no video_root equivalent — see musicbay.md §2.1).
41 new tests (musicbrainz.py against a mocked transport, admin-op policy
for both new settings, media_cache round-trip/pruning, enrich_audio
end-to-end against real ffmpeg-generated MP3s). Full suite (common +
node + hub): 1116 passed, no regressions.
Client-side (music-app.js, persistent player bar) not started yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
Diffstat (limited to 'packages/meshbay-node/tests/test_media_cache.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_media_cache.py | 53 |
1 files changed, 51 insertions, 2 deletions
diff --git a/packages/meshbay-node/tests/test_media_cache.py b/packages/meshbay-node/tests/test_media_cache.py index b66c448..e70ef43 100644 --- a/packages/meshbay-node/tests/test_media_cache.py +++ b/packages/meshbay-node/tests/test_media_cache.py @@ -1,10 +1,10 @@ -"""Tests for media_cache.py — TMDB/thumbnail cache and its pruning obligation.""" +"""Tests for media_cache.py — TMDB/MusicBrainz/thumbnail cache and its pruning obligation.""" import time import pytest -from meshbay_node.media_cache import MediaCache, TMDB_META_TTL_SECS +from meshbay_node.media_cache import MediaCache, TMDB_META_TTL_SECS, MUSICBRAINZ_META_TTL_SECS @pytest.fixture @@ -69,3 +69,52 @@ async def test_prune_file_removes_thumb_and_mapping_but_not_shared_meta(cache): # ep2's own mapping and the shared show metadata both survive assert await cache.get_file_tmdb("ep2") == ("555", "tv") assert await cache.get_tmdb_meta("555", "tv") == {"name": "A Show"} + + +# ── Music app (docs/musicbay.md §6) — file_mbid/mbid_meta ──────────────────── + +@pytest.mark.asyncio +async def test_file_mbid_round_trip(cache): + assert await cache.get_file_mbid("file1") is None + + await cache.set_file_mbid("file1", "release-mbid-1") + + assert await cache.get_file_mbid("file1") == "release-mbid-1" + + +@pytest.mark.asyncio +async def test_mbid_meta_round_trip(cache): + assert await cache.get_mbid_meta("release-mbid-1") is None + + await cache.set_mbid_meta("release-mbid-1", {"artist": "Some Artist", "album": "An Album"}) + + meta = await cache.get_mbid_meta("release-mbid-1") + assert meta == {"artist": "Some Artist", "album": "An Album"} + + +@pytest.mark.asyncio +async def test_mbid_meta_expires_after_ttl(cache): + await cache._db.execute( + "INSERT INTO mbid_meta (mbid, json, fetched_at) VALUES (?, ?, ?)", + ("old-mbid", '{"album": "Old"}', time.time() - MUSICBRAINZ_META_TTL_SECS - 1), + ) + await cache._db.commit() + + assert await cache.get_mbid_meta("old-mbid") is None + + +@pytest.mark.asyncio +async def test_prune_file_removes_mbid_mapping_but_not_shared_meta(cache): + # Two tracks of the same release share one mbid_meta row. + await cache.set_file_mbid("track1", "release-mbid-1") + await cache.set_file_mbid("track2", "release-mbid-1") + await cache.set_mbid_meta("release-mbid-1", {"album": "An Album"}) + await cache.put_thumb("cover-hash", "track1", b"cover-bytes") + + await cache.prune_file("track1") + + assert await cache.get_file_mbid("track1") is None + assert await cache.get_thumb("cover-hash") is None + # track2's own mapping and the shared release metadata both survive + assert await cache.get_file_mbid("track2") == "release-mbid-1" + assert await cache.get_mbid_meta("release-mbid-1") == {"album": "An Album"} |