- 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.
346 lines
12 KiB
Python
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"
|