"""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, MUSICBRAINZ_META_TTL_SECS @pytest.fixture async def cache(tmp_path): c = MediaCache(db_path=tmp_path / "media_cache.db") await c.open() yield c await c.close() @pytest.mark.asyncio async def test_file_tmdb_round_trip(cache): assert await cache.get_file_tmdb("file1") is None await cache.set_file_tmdb("file1", "12345", "movie") assert await cache.get_file_tmdb("file1") == ("12345", "movie") @pytest.mark.asyncio async def test_tmdb_meta_round_trip(cache): assert await cache.get_tmdb_meta("12345", "movie") is None await cache.set_tmdb_meta("12345", "movie", {"title": "A Movie", "vote_average": 7.5}) meta = await cache.get_tmdb_meta("12345", "movie") assert meta == {"title": "A Movie", "vote_average": 7.5} @pytest.mark.asyncio async def test_tmdb_meta_expires_after_ttl(cache): await cache._db.execute( "INSERT INTO tmdb_meta (tmdb_id, media_type, json, fetched_at) VALUES (?, ?, ?, ?)", ("999", "movie", '{"title": "Old"}', time.time() - TMDB_META_TTL_SECS - 1), ) await cache._db.commit() assert await cache.get_tmdb_meta("999", "movie") is None @pytest.mark.asyncio async def test_thumb_round_trip(cache): assert await cache.get_thumb("thumbhash1") is None await cache.put_thumb("thumbhash1", "file1", b"\xff\xd8fakejpeg") assert await cache.get_thumb("thumbhash1") == b"\xff\xd8fakejpeg" @pytest.mark.asyncio async def test_prune_file_removes_thumb_and_mapping_but_not_shared_meta(cache): # Two episodes of the same show share one tmdb_meta row (§2's stated case). await cache.set_file_tmdb("ep1", "555", "tv") await cache.set_file_tmdb("ep2", "555", "tv") await cache.set_tmdb_meta("555", "tv", {"name": "A Show"}) await cache.put_thumb("thumb-ep1", "ep1", b"jpeg-bytes-1") await cache.prune_file("ep1") assert await cache.get_file_tmdb("ep1") is None assert await cache.get_thumb("thumb-ep1") is None # 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"} # ── auto-resolved matches vs manual "Fix match" corrections ───────────────── @pytest.mark.asyncio async def test_clear_tmdb_matches_drops_auto_matches_but_keeps_overrides(cache): await cache.set_file_tmdb("auto1", "10", "movie") await cache.set_file_tmdb("auto2", "20", "movie") await cache.set_file_tmdb("fixed", "30", "movie") await cache.mark_tmdb_override("fixed") removed = await cache.clear_tmdb_matches(["auto1", "auto2", "fixed", "never-seen"]) assert removed == 2 assert await cache.get_file_tmdb("auto1") is None assert await cache.get_file_tmdb("auto2") is None assert await cache.get_file_tmdb("fixed") == ("30", "movie"), ( "a manual Fix match correction must survive ops.rematch_video") @pytest.mark.asyncio async def test_clear_tmdb_matches_empty_list_is_a_noop(cache): assert await cache.clear_tmdb_matches([]) == 0 @pytest.mark.asyncio async def test_clear_file_tmdb_drops_one_auto_match_but_keeps_an_override(cache): await cache.set_file_tmdb("renamed", "10", "movie") await cache.clear_file_tmdb("renamed") assert await cache.get_file_tmdb("renamed") is None await cache.set_file_tmdb("renamed-fixed", "11", "movie") await cache.mark_tmdb_override("renamed-fixed") await cache.clear_file_tmdb("renamed-fixed") assert await cache.get_file_tmdb("renamed-fixed") == ("11", "movie") @pytest.mark.asyncio async def test_drop_tmdb_match_forgets_both_the_match_and_the_override(cache): await cache.set_file_tmdb("f", "10", "movie") await cache.mark_tmdb_override("f") await cache.drop_tmdb_match("f") assert await cache.get_file_tmdb("f") is None # override marker is gone: a fresh match is now treated as ordinary await cache.set_file_tmdb("f", "20", "movie") assert await cache.clear_tmdb_matches(["f"]) == 1 @pytest.mark.asyncio async def test_prune_file_also_clears_the_override_marker(cache): await cache.set_file_tmdb("gone", "10", "movie") await cache.mark_tmdb_override("gone") await cache.prune_file("gone") # the marker must not linger after the file leaves the index and then # shield a later match on the same id from ops.rematch_video assert await cache.get_file_tmdb("gone") is None await cache.set_file_tmdb("gone", "99", "movie") assert await cache.clear_tmdb_matches(["gone"]) == 1 # ── 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"}