Commit graph

7 commits

Author SHA1 Message Date
BoulderBadgeDad
d2d71d8f05 Fix artist-detail showing wrong artist when a source id is duplicated
A pasted Deezer artist link (or any Deezer-source artist click) opened the
wrong artist's header: deezer_id 525046 is stamped on 4 library rows (Kendrick
+ 3 others — an enrichment-corruption bug), and the library-upgrade lookup did
WHERE deezer_id=? LIMIT 1, grabbing an arbitrary row (Jorja Smith) while the
discography loaded fresh from Deezer (Kendrick) — a Frankenstein page.

find_library_artist_for_source now detects when a source id maps to >1 library
artist and refuses to guess: it skips the id-based upgrade (still allowing the
name fallback), so the caller renders the source artist directly — landing on
the correct artist. Unique ids are unaffected (no regression).

The underlying enrichment bug that writes one source id onto multiple artists
is separate and still worth a follow-up.
2026-06-05 07:24:51 -07:00
BoulderBadgeDad
4fcc461616 Source IDs: add canonical registry, adopt at the highest-value sites
The same provider ID is stored under inconsistent column names across tables
(deezer_id vs deezer_artist_id vs album_deezer_id vs similar_artist_deezer_id;
spotify/itunes keep an entity qualifier, others don't; musicbrainz uses three
nouns), so code checks 2-5 name variants everywhere.

Add core/source_ids.py as the single source of truth for (provider, entity) ->
column, with accessors that read an ID from a dict/sqlite3.Row robustly
(canonical column first, then known aliases). NO database columns are renamed —
these are the real names today; the registry just centralizes the knowledge.

Targeted adoption (behavior-identical, verified):
- core/artist_source_lookup.SOURCE_ID_FIELD now derives from the registry
  instead of duplicating the mapping (values unchanged).
- web_server.py artist-detail builds artist_source_ids via source_id_map(...)
  instead of a hand-rolled per-source .get() dict.

Broader call-site adoption deferred as clearly-scoped follow-up.

Tests: tests/test_source_ids_registry.py (canonical columns, alias fallback,
canonical-preferred, sqlite3.Row, source_id_map, SOURCE_ID_FIELD unchanged).
Existing artist_source_lookup + artist_full_detail suites still green.
2026-05-29 12:19:59 -07:00
Broque Thomas
121651da2c Add amazon_id column to artists table for full source parity
Schema: ALTER TABLE artists ADD COLUMN amazon_id TEXT with index, added via
_add_amazon_columns migration called after Discogs in _run_migrations.

SOURCE_ID_FIELD: add "amazon" -> "amazon_id" entry. find_library_artist_for_
source now looks up Amazon artists by slug before falling back to name match,
same as every other source. artist_source_detail already stamps artist_info
[source_id_field] = artist_id so the amazon_id is set on source-only payloads.

Tests: add "amazon": "amazon_id" to EXPECTED_SOURCE_ID_FIELD; revert test
assertion back to strict equality (SOURCE_ONLY_ARTIST_SOURCES == SOURCE_ID_
FIELD.keys() holds again now that amazon has a column).
2026-05-16 17:06:54 -07:00
Broque Thomas
265fe5233e Fix Amazon artist detail: library upgrade lookup and artist images
Library upgrade: find_library_artist_for_source returned None immediately for
Amazon because SOURCE_ID_FIELD has no 'amazon' entry (no DB column for Amazon
artist IDs). The name-based fallback was unreachable. Fix: only skip the column
query when column is None, not the whole function — name lookup now runs for
any source when artist_name + active_server are provided.

Artist images: add AmazonClient._get_artist_image_from_albums so the standard
_get_artist_image_from_source path in metadata/artist_image.py can call it as
a fallback (same hook iTunes/Deezer/Discogs expose). Searches by unslugified
artist name, matches primary artist, fetches album cover from album_metadata.

Test: updated test_source_only_set_matches_mapping_keys → _contains_all_mapped_
sources to assert subset (not equality) — SOURCE_ONLY_ARTIST_SOURCES intentionally
includes sources without a DB column that rely on name-only lookup.
2026-05-16 17:01:37 -07:00
Broque Thomas
51e00d4ebf Fix Amazon Music search quality: images, dedup, explicit stripping, album/artist clicks
- All search_raw calls switched from single-type to types="track,album" — T2Tunes only
  returns results when both types are requested together
- _fetch_album_metas: parallel fetch (up to 5 workers) of album cover art via
  album_metadata(asin) — T2Tunes search results carry no image URLs
- search_tracks: populates image_url, release_date, total_tracks from album meta
- search_artists: strips feat. credits via _primary_artist() so "Artist feat. X" and
  "Artist ft. Y" collapse to one "Artist" entry; uses album cover as artist image
  stand-in (same approach as iTunes — T2Tunes has no artist images)
- search_albums: name-based dedup (display_name + artist key) instead of ASIN-based;
  populates image_url, release_date, total_tracks from album meta (cap 10 ASIN fetches)
- _strip_edition(): strips [Explicit]/(Explicit) from track/album names — explicit is
  the default version; Clean/Edited/Censored labels kept as-is so they stay distinct
- get_album(): applies _strip_edition to name and _primary_artist to artist so
  MusicBrainz preflight matching doesn't fail on "[Explicit]" album names
- get_album_tracks(): populates track_number and disc_number from T2TunesStreamInfo
  instead of hardcoding None — fixes track ordering in multi-track album downloads
- get_artist() / get_artist_albums(): _unslugify() converts slug artist IDs back to
  search names; _primary_artist() in comparison handles feat-annotated results
- SOURCE_ONLY_ARTIST_SOURCES: added "amazon" so artist detail page doesn't 404
- build_source_only_artist_detail: added amazon_client param + dispatch branch
- web_server.py: resolve amazon_client in _build_source_only_artist_detail wrapper;
  add source_override=="amazon" branch in get_spotify_album_tracks endpoint
- 77 tests covering all above paths; all pass
2026-05-16 15:55:15 -07:00
Broque Thomas
e66af77ff6 Make artist_name Optional in find_library_artist_for_source
Cin's review note: typing artist_name as plain `str` forced callers
that didn't have a name to pass `""` as a placeholder, which leaks the
parameter's emptiness contract into every call site and reads badly in
tests. Switching to `Optional[str] = None` lets callers omit it.

The function body's `if artist_name and active_server:` check already
handles None and "" identically, so no body changes were needed. Tests
that previously passed `artist_name=""` drop the argument; one new test
covers the omitted-arg path explicitly.

The web_server.py wrapper takes the same default for symmetry.
2026-04-22 22:15:03 -07:00
Broque Thomas
a097cf3d5a Extract source-artist lookup helpers from web_server.py to core module
Cin pointed out that the prior version of test_artist_source_lookup.py
AST-parsed web_server.py to verify a constant and to string-match a
function's response keys. That was a workaround for the fact that
web_server.py can't be imported at test time (it boots Spotify,
Soulseek, Plex, etc.) — the right answer is to move the logic into a
side-effect-free module so it can be imported and tested directly.

This commit:
  - adds core/artist_source_lookup.py containing the SOURCE_ID_FIELD
    map, the SOURCE_ONLY_ARTIST_SOURCES set, and find_library_artist_for_source
  - replaces the inline definitions in web_server.py with imports +
    a thin wrapper that injects the active media server
  - rewrites the tests to import from the core module directly:
      * mapping correctness is now a plain equality assertion
      * lookup behaviour is exercised against a real MusicDatabase
      * the AST parse and the string-matching contract test class are
        gone
  - drops the _build_source_only_artist_detail contract test entirely
    (the weakest of the four — it was just string-matching the function
    body); when that function moves to core/ it can get a real
    behavioural test alongside.

Test runtime drops from ~161s to ~5.8s. All 18 tests pass.
2026-04-22 22:07:23 -07:00