soulsync/tests/test_enrichment_art_preservation.py
BoulderBadgeDad 3dfec8a157 Fix #764: import no longer destroys embedded cover art
enhance_file_metadata rebuilds tags from scratch: for FLAC it calls
clear_pictures(), for MP3/MP4 it clears the whole tag block — and it does
this UP FRONT, then saves the file, long before it tries to fetch and embed
the replacement art. So every way the re-embed could come up empty left the
file saved with the original art destroyed and nothing put back:

  - extract_source_metadata returns nothing  -> early save, no embed
  - no album-art URL / art download fails / rejected by the min-size guard
    -> embed_album_art_metadata returns early without adding a picture
  - art embedding disabled in config         -> embed skipped entirely
  - embed raises mid-enrichment               -> file left cleared on disk

This is the "cover art gets corrupted/destroyed during import" half of #764
(continuation of #755); distinct from #750's truncated-cache DISPLAY bug.

Fix: new core/metadata/art_preservation.py snapshots the existing art
(the live Picture / APIC / MP4Cover objects, so they re-apply verbatim)
BEFORE the clear, and restores it before each save IFF the file currently
has none. Wired into all three exit paths in enhance_file_metadata
(no-metadata early return, the final save, and the except handler). The
restore is a strict no-op when art is already present, so the happy path —
new art embedded — is byte-for-byte unchanged: it never clobbers or
duplicates a freshly-embedded cover. embed_album_art_metadata now returns a
bool so the intent (embedded / didn't) is explicit.

Tests:
- tests/test_art_preservation.py (5) — snapshot/restore round-trips through
  real mutagen FLAC + ID3 objects; restore no-ops when new art is present.
- tests/test_enrichment_art_preservation.py (4) — runs the REAL
  enhance_file_metadata over a real FLAC with embedded art and asserts the
  art survives on disk for missing-metadata / failed-embed / embed-raises,
  and is correctly REPLACED (exactly one picture, new bytes) on success.
1019 tests pass across the metadata/enrichment/imports/acoustid suites.
2026-06-02 08:40:05 -07:00

153 lines
5.3 KiB
Python

"""End-to-end proof that enhance_file_metadata never destroys embedded art.
Bug #764: the enrichment rewrite clears cover art up front and saves the file
regardless of whether new art gets re-embedded. These tests run the REAL
``enhance_file_metadata`` against a REAL FLAC that has embedded art, and assert
the art is still on disk after every failure path — and that the happy path
(new art embedded) correctly REPLACES it. This exercises the actual wiring
(snapshot -> clear -> rewrite -> restore/save), not just the helper functions.
Only the external collaborators are stubbed (config, source-metadata extraction,
the art fetch, source-id embed, verification) — the clear/snapshot/restore/save
sequence under test runs for real through mutagen.
"""
from __future__ import annotations
import os
import tempfile
from unittest.mock import patch
import pytest
pytest.importorskip("mutagen")
from mutagen.flac import FLAC, Picture # noqa: E402
import core.metadata.enrichment as enrichment # noqa: E402
_PNG = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00"
b"\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
)
class _Cfg:
"""Config stub: returns the caller's default for every key, so
metadata_enhancement.enabled / embed_album_art resolve True."""
def get(self, key, default=None):
return default
def _make_flac_with_art(path):
minimal = (
b"fLaC"
+ b"\x80\x00\x00\x22"
+ b"\x00\x10\x00\x10"
+ b"\x00\x00\x00\x00\x00\x00"
+ b"\x0a\xc4\x42\xf0\x00\x00\x00\x00"
+ b"\x00" * 16
)
with open(path, "wb") as f:
f.write(minimal)
audio = FLAC(path)
pic = Picture()
pic.data = _PNG
pic.type = 3
pic.mime = "image/png"
pic.width = 1
pic.height = 1
pic.depth = 24
audio.add_picture(pic)
audio.save()
@pytest.fixture
def flac_path():
fd, path = tempfile.mkstemp(suffix=".flac")
os.close(fd)
_make_flac_with_art(path)
yield path
try:
os.remove(path)
except OSError:
pass
def _disk_art(path):
"""Return the embedded picture bytes on disk, or None."""
pics = FLAC(path).pictures
return pics[0].data if pics else None
def _run(flac_path, *, metadata, embed_side_effect):
"""Drive enhance_file_metadata with stubbed collaborators."""
with patch.object(enrichment, "get_config_manager", return_value=_Cfg()), \
patch.object(enrichment, "strip_all_non_audio_tags"), \
patch.object(enrichment, "extract_source_metadata", return_value=metadata), \
patch.object(enrichment, "embed_source_ids"), \
patch.object(enrichment, "verify_metadata_written", return_value=True), \
patch.object(enrichment, "embed_album_art_metadata", side_effect=embed_side_effect):
return enrichment.enhance_file_metadata(
flac_path, context={}, artist={"name": "Coldplay"}, album_info={},
)
# ── failure paths: art MUST survive ──────────────────────────────────────
def test_art_survives_when_source_metadata_missing(flac_path):
# extract_source_metadata returns None -> early return path.
assert _disk_art(flac_path) == _PNG
result = _run(flac_path, metadata=None, embed_side_effect=lambda *a, **k: False)
assert result is True
assert _disk_art(flac_path) == _PNG # art preserved on disk
def test_art_survives_when_embed_produces_nothing(flac_path):
# Metadata is fine, but the art fetch fails -> embed is a no-op (returns
# False, adds no picture). The original art must remain.
def embed_noop(audio_file, metadata):
return False # mirrors "no art URL" / "download failed"
result = _run(flac_path, metadata={"title": "Yellow", "artist": "Coldplay"},
embed_side_effect=embed_noop)
assert result is True
assert _disk_art(flac_path) == _PNG
def test_art_survives_when_embed_raises(flac_path):
# A hard crash mid-enrichment must not leave the file art-less.
def embed_boom(audio_file, metadata):
raise RuntimeError("art backend exploded")
result = _run(flac_path, metadata={"title": "Yellow", "artist": "Coldplay"},
embed_side_effect=embed_boom)
assert result is False # enrichment reported failure
assert _disk_art(flac_path) == _PNG # ...but art was restored on disk
# ── happy path: new art REPLACES old, no duplication ──────────────────────
def test_new_art_replaces_old_when_embed_succeeds(flac_path):
new_bytes = _PNG + b"BRANDNEW"
def embed_real(audio_file, metadata):
# Simulate a successful fetch+embed: add the new picture in-place.
pic = Picture()
pic.data = new_bytes
pic.type = 3
pic.mime = "image/png"
audio_file.add_picture(pic)
return True
result = _run(flac_path, metadata={"title": "Yellow", "artist": "Coldplay"},
embed_side_effect=embed_real)
assert result is True
on_disk = FLAC(flac_path).pictures
# Exactly one picture, and it's the NEW art — restore must not have
# re-added the old one on top.
assert len(on_disk) == 1
assert on_disk[0].data == new_bytes