soulsync/core/metadata
Broque Thomas d5de724f9b Multi-artist Deezer upgrade + double-append guard hardening
Two follow-ups to the multi-artist tag settings PR:

1. Deezer contributors upgrade — closes the "known limitation"
   flagged in the prior commit. Deezer's `/search` endpoint only
   returns the primary artist for each track; the full contributors
   array (feat., remix collaborators, producers credited as artists)
   lives on `/track/<id>` and gets parsed by `_build_enhanced_track`.
   Without the upgrade Deezer-sourced tracks never got multi-artist
   tags even with the right settings on.

   Fix in `core/metadata/source.py`: when source==deezer AND the
   search response had a single artist AND a track_id is available,
   fetch full track details via `get_deezer_client().get_track_details`
   and replace `all_artists` with the upgraded list.

   - One extra API call per affected Deezer track
   - Skipped when search already returned multiple (no-op fast path)
   - Skipped for non-Deezer sources (Spotify/Tidal/iTunes search
     responses already include all artists)
   - Skipped when no track_id is available
   - Defensive try/except: on /track/<id> failure (network error,
     deezer client unavailable), fall through to the search-result
     list — never lose the data we already had

2. Double-append guard hardened with a word-boundary regex.
   Prior commit checked for `"feat." not in title.lower() and "(ft."
   not in title.lower()` — too narrow. Source platforms produce
   wildly different feat-marker conventions: "(feat. X)", "(Feat X)",
   "(FEAT X)", "(Featuring X)", "[feat. X]", "ft. X" (no parens),
   "FT. X", etc. Any of these as the SOURCE title would cause a
   double-append: `"Track (Feat X) (feat. Y)"`.

   Replaced with `re.search(r'\b(?:feat|feat\.|featuring|ft|ft\.)\b',
   title, IGNORECASE)`. Word-boundary regex catches every common
   variant. Substring matches like "Aftermath" containing `ft`
   correctly fall through to the append path (pinned by a regression
   test).

16 new tests (29 total in the file):
- 9 parametrized variants of the double-append guard
- 1 substring guard ("Aftermath")
- 6 Deezer upgrade scenarios (fires when expected, doesn't fire
  for non-Deezer / multi-artist search / no track_id, defensive
  fall-through on failure, no false-positive when /track/<id>
  confirms single artist)

Full pytest 2727 passed.
2026-05-11 15:30:23 -07:00
..
__init__.py Move image URL normalization into metadata helpers 2026-05-02 22:02:36 +03:00
album_mbid_cache.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
album_tracks.py Auto-import: MBID/ISRC fast paths + duration sanity gate 2026-05-09 09:57:33 -07:00
artist_image.py Move metadata helpers into package modules 2026-04-29 11:28:42 +03:00
artwork.py Deezer cover-art download: fallback to original URL on CDN refusal 2026-05-10 18:29:36 -07:00
cache.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
common.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
completion.py Move metadata helpers into package modules 2026-04-29 11:28:42 +03:00
discography.py Migrate discography + quality scanner to typed Album path 2026-05-04 08:12:40 -07:00
enrichment.py Multi-artist tag settings: implement artist_separator + feat_in_title + populate _artists_list 2026-05-11 15:16:42 -07:00
lookup.py Move metadata API into package 2026-04-29 08:10:18 +03:00
lyrics.py Move shared metadata helpers into package 2026-04-27 19:54:44 +03:00
multi_source_search.py Fix Download Discography pulling wrong artist + log routing 2026-05-06 13:03:43 -07:00
registry.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
relevance.py Deezer search: drop advanced-syntax at endpoint, free-text + rerank wins 2026-05-10 09:36:48 -07:00
service.py Move metadata API into package 2026-04-29 08:10:18 +03:00
similar_artists.py Move metadata helpers into package modules 2026-04-29 11:28:42 +03:00
source.py Multi-artist Deezer upgrade + double-append guard hardening 2026-05-11 15:30:23 -07:00
status.py Make Spotify status updates event-driven 2026-05-02 22:02:01 +03:00
types.py Add Qobuz + Tidal album converters 2026-05-03 22:30:19 -07:00