soulsync/tests/test_metadata_enrichment.py
Antti Kettunen 9656dbd46a
Thread runtime through metadata enrichment
- Pass the live runtime bundle into the shared metadata facade so worker-backed source enrichment can actually run.
- Forward runtime from the import pipeline and web-server wrapper into embed_source_ids.
- Add a regression test that verifies the runtime object reaches the source-ID embedding path.
2026-04-27 19:54:44 +03:00

346 lines
12 KiB
Python

import types
from core.metadata import enrichment as me
from core.metadata import artwork as ma
from core.metadata import source as ms
class _Config:
def __init__(self, values=None):
self.values = values or {}
def get(self, key, default=None):
return self.values.get(key, default)
class _FakeTag:
def __init__(self, kind, **kwargs):
self.kind = kind
self.kwargs = kwargs
class _FakeID3Tags:
def __init__(self):
self.added = []
def add(self, frame):
self.added.append(frame)
def clear(self):
self.added.clear()
def getall(self, _key):
return []
def keys(self):
return [frame.kind for frame in self.added]
def __len__(self):
return len(self.added)
class _FakeAudio:
def __init__(self):
self.tags = _FakeID3Tags()
self.save_calls = []
self.clear_pictures_calls = 0
def clear_pictures(self):
self.clear_pictures_calls += 1
def add_tags(self):
self.tags = _FakeID3Tags()
def save(self, **kwargs):
self.save_calls.append(kwargs)
class _FakeResponse:
def __init__(self, payload, content_type="image/jpeg"):
self._payload = payload
self._content_type = content_type
def read(self):
return self._payload
def info(self):
return types.SimpleNamespace(get_content_type=lambda: self._content_type)
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def _fake_symbols(audio):
def _tag_factory(kind):
return lambda **kwargs: _FakeTag(kind, **kwargs)
return types.SimpleNamespace(
File=lambda _path: audio,
APEv2=type("FakeAPEv2", (), {}),
APENoHeaderError=Exception,
FLAC=type("FakeFLAC", (), {}),
Picture=type("FakePicture", (), {"__init__": lambda self: None}),
ID3=_FakeID3Tags,
APIC=_tag_factory("APIC"),
TBPM=_tag_factory("TBPM"),
TCOP=_tag_factory("TCOP"),
TDOR=_tag_factory("TDOR"),
TDRC=_tag_factory("TDRC"),
TCON=_tag_factory("TCON"),
TIT2=_tag_factory("TIT2"),
TALB=_tag_factory("TALB"),
TPE1=_tag_factory("TPE1"),
TPE2=_tag_factory("TPE2"),
TPOS=_tag_factory("TPOS"),
TPUB=_tag_factory("TPUB"),
TRCK=_tag_factory("TRCK"),
TSRC=_tag_factory("TSRC"),
TXXX=_tag_factory("TXXX"),
UFID=_tag_factory("UFID"),
TMED=_tag_factory("TMED"),
MP4=type("FakeMP4", (), {}),
MP4Cover=types.SimpleNamespace(FORMAT_JPEG=1, FORMAT_PNG=2, __call__=lambda *args, **kwargs: ("cover", args, kwargs)),
MP4FreeForm=lambda data: ("freeform", data),
OggVorbis=type("FakeOggVorbis", (), {}),
OggOpus=None,
)
def test_extract_source_metadata_keeps_neutral_fields_and_skips_itunes_fallback_for_non_itunes_sources(monkeypatch):
monkeypatch.setattr(ms, "get_config_manager", lambda: _Config({"file_organization.collab_artist_mode": "first"}))
monkeypatch.setattr(ms, "get_itunes_client", lambda: (_ for _ in ()).throw(AssertionError("itunes fallback should not run for non-itunes sources")))
context = {
"source": "spotify",
"artist": {"name": "Artist One & Artist Two", "id": "123", "genres": ["rock", "indie"]},
"album": {
"name": "Album One",
"total_tracks": 12,
"release_date": "2024-01-02",
"images": [{"url": "https://img.example/album.jpg"}],
},
"track_info": {
"artists": [{"name": "Artist One"}],
"_source": "spotify",
"track_number": 3,
"disc_number": 2,
"total_tracks": 12,
},
"original_search_result": {
"title": "Song One",
"artists": [{"name": "Artist One"}],
"clean_title": "Song One",
"clean_album": "Album One",
"clean_artist": "Artist One",
"disc_number": 2,
"duration_ms": 180000,
},
}
metadata = me.extract_source_metadata(
context,
context["artist"],
{"is_album": True, "album_name": "Album One", "track_number": 3, "disc_number": 2, "album_image_url": "https://img.example/album.jpg"},
)
assert metadata["source"] == "spotify"
assert metadata["title"] == "Song One"
assert metadata["artist"] == "Artist One"
assert metadata["album_artist"] == "Artist One & Artist Two"
assert metadata["album"] == "Album One"
assert metadata["track_number"] == 3
assert metadata["total_tracks"] == 12
assert metadata["disc_number"] == 2
assert metadata["album_art_url"] == "https://img.example/album.jpg"
def test_embed_source_ids_uses_current_source_ids_and_legacy_fallback(monkeypatch):
audio = _FakeAudio()
symbols = _fake_symbols(audio)
monkeypatch.setattr(ms, "get_config_manager", lambda: _Config())
monkeypatch.setattr(ms, "get_mutagen_symbols", lambda: symbols)
monkeypatch.setattr(ms, "get_database", lambda: None)
current_metadata = {
"source": "deezer",
"source_track_id": "dz-track",
"source_artist_id": "dz-artist",
"source_album_id": "dz-album",
"title": "Song One",
"artist": "Artist One",
"album_artist": "Artist One",
"album": "Album One",
}
me.embed_source_ids(audio, current_metadata, context={"track_info": {}, "original_search_result": {}})
current_descs = [frame.kwargs.get("desc") for frame in audio.tags.added if frame.kind == "TXXX"]
assert "DEEZER_TRACK_ID" in current_descs
assert "DEEZER_ARTIST_ID" in current_descs
audio = _FakeAudio()
symbols = _fake_symbols(audio)
monkeypatch.setattr(ms, "get_mutagen_symbols", lambda: symbols)
legacy_metadata = {
"source": "",
"spotify_track_id": "sp-track",
"spotify_artist_id": "sp-artist",
"spotify_album_id": "sp-album",
"itunes_track_id": "it-track",
"itunes_artist_id": "it-artist",
"itunes_album_id": "it-album",
"title": "Song One",
"artist": "Artist One",
"album_artist": "Artist One",
"album": "Album One",
}
me.embed_source_ids(audio, legacy_metadata, context={"track_info": {}, "original_search_result": {}})
legacy_descs = [frame.kwargs.get("desc") for frame in audio.tags.added if frame.kind == "TXXX"]
assert "SPOTIFY_TRACK_ID" in legacy_descs
assert "SPOTIFY_ARTIST_ID" in legacy_descs
assert "SPOTIFY_ALBUM_ID" in legacy_descs
assert "ITUNES_TRACK_ID" in legacy_descs
assert "ITUNES_ARTIST_ID" in legacy_descs
assert "ITUNES_ALBUM_ID" in legacy_descs
def test_enhance_file_metadata_forwards_runtime_to_source_embedding(monkeypatch):
audio = _FakeAudio()
symbols = _fake_symbols(audio)
seen = {}
runtime = types.SimpleNamespace(marker="runtime")
monkeypatch.setattr(me, "get_config_manager", lambda: _Config(
{
"metadata_enhancement.enabled": True,
"metadata_enhancement.embed_album_art": False,
"metadata_enhancement.tags.write_multi_artist": False,
}
))
monkeypatch.setattr(me, "get_mutagen_symbols", lambda: symbols)
monkeypatch.setattr(me, "strip_all_non_audio_tags", lambda file_path: {"apev2_stripped": False, "apev2_tag_count": 0})
monkeypatch.setattr(
me,
"extract_source_metadata",
lambda context, artist, album_info: {
"source": "deezer",
"source_track_id": "dz-track",
"source_artist_id": "dz-artist",
"source_album_id": "dz-album",
"title": "Song One",
"artist": "Artist One",
"album_artist": "Artist One",
"album": "Album One",
"track_number": 3,
"total_tracks": 12,
"disc_number": 2,
"date": "2024",
"genre": "Rock",
},
)
monkeypatch.setattr(
me,
"embed_source_ids",
lambda audio_file, metadata, context, runtime=None: seen.setdefault("runtime", runtime),
)
monkeypatch.setattr(me, "embed_album_art_metadata", lambda *args, **kwargs: None)
monkeypatch.setattr(me, "verify_metadata_written", lambda file_path: True)
result = me.enhance_file_metadata(
"song.flac",
{"_audio_quality": ""},
{"name": "Artist One"},
{},
runtime=runtime,
)
assert result is True
assert seen["runtime"] is runtime
def test_enhance_file_metadata_writes_tags_and_propagates_release_id(monkeypatch):
audio = _FakeAudio()
symbols = _fake_symbols(audio)
strip_calls = []
verify_calls = []
monkeypatch.setattr(me, "get_config_manager", lambda: _Config(
{
"metadata_enhancement.enabled": True,
"metadata_enhancement.embed_album_art": False,
"metadata_enhancement.tags.write_multi_artist": False,
}
))
monkeypatch.setattr(ms, "get_config_manager", lambda: _Config())
monkeypatch.setattr(me, "get_mutagen_symbols", lambda: symbols)
monkeypatch.setattr(ms, "get_mutagen_symbols", lambda: symbols)
monkeypatch.setattr(ms, "get_database", lambda: None)
monkeypatch.setattr(me, "strip_all_non_audio_tags", lambda file_path: strip_calls.append(file_path) or {"apev2_stripped": False, "apev2_tag_count": 0})
monkeypatch.setattr(
me,
"extract_source_metadata",
lambda context, artist, album_info: {
"source": "deezer",
"source_track_id": "dz-track",
"source_artist_id": "dz-artist",
"source_album_id": "dz-album",
"title": "Song One",
"artist": "Artist One",
"album_artist": "Artist One",
"album": "Album One",
"track_number": 3,
"total_tracks": 12,
"disc_number": 2,
"date": "2024",
"genre": "Rock",
"musicbrainz_release_id": "mb-release-1",
},
)
monkeypatch.setattr(me, "embed_album_art_metadata", lambda *args, **kwargs: None)
monkeypatch.setattr(me, "verify_metadata_written", lambda file_path: verify_calls.append(file_path) or True)
album_info = {}
result = me.enhance_file_metadata(
"song.flac",
{"_audio_quality": ""},
{"name": "Artist One"},
album_info,
)
assert result is True
assert strip_calls == ["song.flac"]
assert verify_calls == ["song.flac"]
assert audio.clear_pictures_calls == 1
assert len(audio.save_calls) == 2
assert album_info["musicbrainz_release_id"] == "mb-release-1"
assert any(frame.kind == "TIT2" for frame in audio.tags.added)
assert any(frame.kind == "TPE1" for frame in audio.tags.added)
assert any(frame.kind == "TXXX" and frame.kwargs.get("desc") == "DEEZER_TRACK_ID" for frame in audio.tags.added)
def test_download_cover_art_uses_album_context_image_url(tmp_path, monkeypatch):
monkeypatch.setattr(ma, "get_config_manager", lambda: _Config(
{
"metadata_enhancement.cover_art_download": True,
"metadata_enhancement.prefer_caa_art": False,
}
))
monkeypatch.setattr(ma.urllib.request, "urlopen", lambda *args, **kwargs: _FakeResponse(b"cover-bytes"))
target_dir = tmp_path / "Album One"
target_dir.mkdir()
ma.download_cover_art(
{},
str(target_dir),
{"album": {"image_url": "https://img.example/album.jpg"}},
)
cover_path = target_dir / "cover.jpg"
assert cover_path.exists()
assert cover_path.read_bytes() == b"cover-bytes"