From 93d2399cf83e952c5d7620671a5f2119a5358239 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Thu, 4 Jun 2026 14:46:09 -0700 Subject: [PATCH] Library Re-tag: pull cover art from the configured cover-art sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback (Sokhi): after changing cover-art sources, re-tag should re-download fresh covers from THEM. The job took cover_url only from the matched metadata source's album image, ignoring the user's configured cover-art order. Now prefer select_preferred_art_url (the same metadata_enhancement.album_art_order the post-process embed honors), falling back to the source image when no order is configured (non-breaking). 'replace' cover mode already force-refreshes art on every matched album, and the embed replaces existing art (no duplicate pictures) — so 'replace' + a configured art order = fresh covers from those sources. Help text updated. Tests: prefers configured source URL / falls back to source image when unset. --- core/repair_jobs/library_retag.py | 23 +++++++++++++++++- tests/test_library_retag_job.py | 40 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/core/repair_jobs/library_retag.py b/core/repair_jobs/library_retag.py index 9bd0b463..ad1325e4 100644 --- a/core/repair_jobs/library_retag.py +++ b/core/repair_jobs/library_retag.py @@ -208,7 +208,11 @@ class LibraryRetagJob(RepairJob): 'Turn it off to auto-apply on scan.\n' '- Mode: "overwrite" rewrites every field the source provides; "fill_missing" ' 'only fills blank tags (keeps your existing values).\n' - '- Cover art: replace / fill-missing / skip.\n' + '- Cover art: replace / fill-missing / skip. "replace" force-refreshes ' + 'art on every matched album (use this after changing your cover-art ' + 'sources to re-pull fresh covers). When you have configured cover-art ' + 'sources (Settings > metadata enhancement art order), the art is pulled ' + 'from those; otherwise it falls back to the matched source\'s album image.\n' '- Source: which matched source to pull from (default: your source priority).' ) icon = 'repair-icon-retag' @@ -339,6 +343,23 @@ class LibraryRetagJob(RepairJob): cover_url = v break + # Honor the user's configured cover-art sources (the same + # `metadata_enhancement.album_art_order` the post-process embed uses), so + # changing those sources and re-tagging pulls fresh art FROM them rather + # than always using the matched metadata source's album image. Non- + # breaking: select_preferred_art_url returns None when no order is + # configured, so we keep the source image. Skipped when not embedding art. + if cover_mode != 'skip': + try: + from core.metadata.art_lookup import select_preferred_art_url + order = (context.config_manager.get('metadata_enhancement.album_art_order') + if context.config_manager else None) + preferred = select_preferred_art_url(artist_name, album_title, album_meta, order) + if preferred: + cover_url = preferred + except Exception as e: + logger.debug("preferred cover-art lookup failed for album %s: %s", album_id, e) + # Cover action (album-level), independent of tag changes. Decided first # so cover-only albums (tags fine, art missing) still include their # tracks for the apply to embed art into. diff --git a/tests/test_library_retag_job.py b/tests/test_library_retag_job.py index e843ec98..d0405827 100644 --- a/tests/test_library_retag_job.py +++ b/tests/test_library_retag_job.py @@ -111,6 +111,46 @@ def test_scan_dry_run_off_auto_applies_no_finding(tmp_path, monkeypatch): assert writes and writes[0]['title'] == 'Real Title' # actually wrote +def test_scan_prefers_configured_cover_art_source(tmp_path, monkeypatch): + """Sokhi's request: when cover-art sources are configured, the re-tag pulls + art from them (select_preferred_art_url) instead of the matched source's + album image — so changing sources + 'replace' re-downloads fresh covers.""" + track = tmp_path / 'track.flac'; track.write_bytes(b'') + conn = _db_with_album(str(tmp_path / 'm.db'), str(track), current_title='Old Title') + ctx = _context(conn, {'mode': 'overwrite', 'cover_art': 'replace', 'source': 'spotify'}) + _patch_source(monkeypatch, { + 'title': 'Old Title', 'album_artist': 'Real Artist', 'album': 'Real Album', + 'year': '2021', 'genre': 'Rock', 'track_number': 1, 'disc_number': 1, + }) + monkeypatch.setattr('core.metadata.art_lookup.select_preferred_art_url', + lambda artist, album, meta, order, **k: 'http://itunes/big-cover.jpg') + + result = lr.LibraryRetagJob().scan(ctx) + + assert result.findings_created == 1 + d = ctx.findings[0]['details'] + assert d['cover_url'] == 'http://itunes/big-cover.jpg' # configured source won + assert d['cover_action'] == 'replace' + + +def test_scan_falls_back_to_source_image_when_no_configured_art(tmp_path, monkeypatch): + """Non-breaking: with no configured cover-art order, keep using the matched + source's album image (select_preferred_art_url returns None).""" + track = tmp_path / 'track.flac'; track.write_bytes(b'') + conn = _db_with_album(str(tmp_path / 'm.db'), str(track), current_title='Old Title') + ctx = _context(conn, {'mode': 'overwrite', 'cover_art': 'replace', 'source': 'spotify'}) + _patch_source(monkeypatch, { + 'title': 'Old Title', 'album_artist': 'Real Artist', 'album': 'Real Album', + 'year': '2021', 'genre': 'Rock', 'track_number': 1, 'disc_number': 1, + }) + monkeypatch.setattr('core.metadata.art_lookup.select_preferred_art_url', + lambda *a, **k: None) + + result = lr.LibraryRetagJob().scan(ctx) + d = ctx.findings[0]['details'] + assert d['cover_url'] == 'http://art/cover.jpg' # _ALBUM_META['image_url'] + + def test_scan_full_depth_attaches_full_meta_to_finding(tmp_path, monkeypatch): """depth=full: each track plan carries a full_meta dict (title/album/artist + source ids) for the enrichment cascade, and details record the depth."""