diff --git a/core/artists/quality.py b/core/artists/quality.py index a8a56eeb..737d9339 100644 --- a/core/artists/quality.py +++ b/core/artists/quality.py @@ -56,12 +56,13 @@ Returns `(payload_dict, http_status_code)` so the route wrapper can from __future__ import annotations -import logging import os from dataclasses import dataclass from typing import Any, Callable, Optional -logger = logging.getLogger(__name__) +from utils.logging_config import get_logger + +logger = get_logger('artists.quality') @dataclass diff --git a/core/metadata/multi_source_search.py b/core/metadata/multi_source_search.py index 19b78c77..b7df0705 100644 --- a/core/metadata/multi_source_search.py +++ b/core/metadata/multi_source_search.py @@ -25,13 +25,14 @@ contract is shared. from __future__ import annotations -import logging from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field from difflib import SequenceMatcher from typing import Any, Dict, List, Optional, Tuple -logger = logging.getLogger(__name__) +from utils.logging_config import get_logger + +logger = get_logger('metadata.multi_source_search') @dataclass diff --git a/web_server.py b/web_server.py index 72150ada..9a07cc5b 100644 --- a/web_server.py +++ b/web_server.py @@ -8627,6 +8627,58 @@ def get_artist_discography(artist_id): from core.metadata.lookup import MetadataLookupOptions from core.metadata_service import get_artist_discography as _get_artist_discography + # Server-side per-source ID resolution. Look up the library row + # by ANY of the IDs the frontend might send: library DB id, + # spotify_artist_id, itunes_artist_id, deezer_id, or + # musicbrainz_id. Once matched, pull every stored provider ID + # and dispatch the right ID to each source via + # ``artist_source_ids``. Mirrors what the watchlist scanner + # already does. + # + # Without this, the frontend's ID choice fully decides which + # source can answer correctly: + # - sends DB id 194687 → Deezer accepts (wrong: it's a real + # Deezer ID for a different artist) + # - sends Spotify ID `1bDWGdIC...` → Deezer rejects → falls + # back to fuzzy name search → may pick wrong artist + # With server-side resolution, every source gets its OWN stored + # ID regardless of which one the URL carries. + artist_source_ids = {} + try: + _db = get_database() + _conn = _db._get_connection() + try: + _cur = _conn.cursor() + _cur.execute(""" + SELECT spotify_artist_id, itunes_artist_id, + deezer_id, musicbrainz_id + FROM artists + WHERE id = ? + OR spotify_artist_id = ? + OR itunes_artist_id = ? + OR deezer_id = ? + OR musicbrainz_id = ? + LIMIT 1 + """, (artist_id, artist_id, artist_id, artist_id, artist_id)) + _row = _cur.fetchone() + if _row: + if _row['spotify_artist_id']: + artist_source_ids['spotify'] = str(_row['spotify_artist_id']) + if _row['itunes_artist_id']: + artist_source_ids['itunes'] = str(_row['itunes_artist_id']) + if _row['deezer_id']: + artist_source_ids['deezer'] = str(_row['deezer_id']) + if _row['musicbrainz_id']: + artist_source_ids['musicbrainz'] = str(_row['musicbrainz_id']) + logger.info( + f"Discography: resolved per-source IDs for artist_id={artist_id} → " + f"{artist_source_ids}" + ) + finally: + _conn.close() + except Exception as _id_exc: + logger.debug(f"Could not resolve per-source artist IDs for {artist_id}: {_id_exc}") + discography = _get_artist_discography( artist_id, artist_name=artist_name, @@ -8636,6 +8688,7 @@ def get_artist_discography(artist_id): skip_cache=False, max_pages=0, limit=50, + artist_source_ids=artist_source_ids or None, ), ) diff --git a/webui/static/helper.js b/webui/static/helper.js index 404a3692..9d2669f0 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3432,6 +3432,7 @@ const WHATS_NEW = { '2.4.2': [ // --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps --- { date: 'Unreleased — 2.4.2 dev cycle' }, + { title: 'Fix: Download Discography Showed Wrong Artist\'s Albums', desc: 'clicked download discography on 50 cent → modal showed young hot rod\'s albums. clicked weird al → modal showed the beatles. cause: the endpoint received whichever single artist id the frontend happened to pick (spotify or itunes or deezer or library db id) and dispatched it as-is to whichever source it queried. when the picked id didn\'t match the queried source\'s id format, lookup either returned wrong-artist results (numeric collisions — db id 194687 was a real deezer artist for someone else) or fell back to a fuzzy name search that picked a wrong artist. the per-source id dispatch mechanism (`MetadataLookupOptions.artist_source_ids`) already existed and the watchlist scanner already used it; the on-demand discography endpoint just wasn\'t wired to it. fixed: when the url artist_id matches a library row by ANY stored id (db id, spotify_artist_id, itunes_artist_id, deezer_id, musicbrainz_id), backend pulls every stored provider id and dispatches the right id to each source. each source gets its OWN stored id regardless of what the url carries. when the url id is a non-library source-native id and the row lookup misses entirely, behavior is identical to before (single-id fallback). also fixed two log-namespace bugs: enhance quality and multi-source search were writing through `getLogger(__name__)` which resolves to `core.artists.quality` / `core.metadata.multi_source_search` — neither under the soulsync handler — so every diagnostic line was silently dropped. switched both to `get_logger()` from utils.logging_config so they actually land in app.log.', page: 'library' }, { title: 'Enhance Quality: Direct ID Lookup Like Download Discography', desc: 'two related fixes. (1) discord report: enhance quality on an artist with neither spotify nor deezer connected added tracks as "unknown artist - unknown album - unknown track". enhance was running a single-source itunes fallback chain that returned junk matches with empty fields, while track redownload had been doing parallel multi-source search the whole time. extracted that search into `core/metadata/multi_source_search.py` and pointed both flows at it. (2) followup: enhance was still using fuzzy text search even when the library track had a stored source ID (spotify_track_id / deezer_id / itunes_track_id / soul_id) on the row, which meant tracks with messy tags ("Title (Live)", featured artists in the artist field, etc.) failed to match even though a perfect ID was sitting right there. download discography never had this problem because it resolves albums by stable ID, not by name. enhance now does the same: for every source you have configured, if the track has a stored ID for that source, it calls `get_track_details(id)` directly — no fuzzy matching. preferred source (your configured primary) is tried first so a deezer-primary user gets deezer payloads on the wishlist entry. text search is only the fallback now (kicks in for tracks with no stored IDs). also fixed the modal toast that lied "matching tracks to spotify..." regardless of which sources were actually being queried.', page: 'library' }, { title: 'Internal: Media Server Engine Cin/JohnBaumb Pass', desc: 'internal — applied the same architectural cleanups the download engine PR went through to the media server engine PR before review. (1) every server client (Plex / Jellyfin / Navidrome / SoulSync) now explicitly inherits `MediaServerClient` instead of relying on structural typing — drift in any class fails at the conformance test boundary. (2) generic accessors on the engine: `configured_clients()` (replaces per-server `if X and X.is_connected(): clients[name] = X` chains in web_server.py) and `reload_config(name=None)` (generic dispatch instead of per-client reload calls). (3) singleton factory: `get_media_server_engine()` / `set_media_server_engine()` matching the metadata + download engine shape. web_server.py boots via `set_media_server_engine(...)` so factory + global handle share state. (4) ~70 direct `plex_client.X` / `jellyfin_client.X` / `navidrome_client.X` / `soulsync_library_client.X` attribute reaches in web_server.py migrated to `media_server_engine.client(\'\').X`. ~60 standalone refs (truthy checks, media_client assignments, source-name tuples) also routed through the engine. (5) the per-server `plex_client` / `jellyfin_client` / `navidrome_client` / `soulsync_library_client` globals in web_server.py are gone entirely — engine owns the client instances now, every caller reaches via `media_server_engine.client(\'\')`. four multi-client consumers (`PlaylistSyncService`, `ListeningStatsWorker`, `WebScanManager`, discovery `SyncDeps`) refactored to take the engine instead of separate per-server kwargs. (6) `TrackInfo` and `PlaylistInfo` lifted out of `core/plex_client.py` / `jellyfin_client.py` / `navidrome_client.py` (each was defining a near-identical copy) into the neutral `core/media_server/types.py` module — same lift Cin caught on the download `TrackResult`/`AlbumResult`/`DownloadStatus` situation. consumers (matching engine, sync service) get one import. zero behavior change.' }, { title: 'Internal: Media Server Engine Foundation', desc: 'internal — companion to the download engine refactor. introduces a media-server engine + plugin contract on top of the four server clients (plex / jellyfin / navidrome / soulsync standalone). pre-refactor web_server.py held four separate per-server client globals that every dispatch site reached individually. new `core/media_server/` package provides `MediaServerEngine` that owns the per-server clients + a small set of generic accessors (`client(name)`, `active_client()`, `configured_clients()`, `reload_config(name)`) so call sites use one canonical lookup pattern. plugin contract requires the four methods every server actually implements (is_connected, ensure_connection, get_all_artists, get_all_album_ids) — methods that exist on most-but-not-all servers (search_tracks, trigger_library_scan, get_library_stats, etc.) are listed in `KNOWN_PER_SERVER_METHODS` for discoverability and reached directly via `engine.client(name).` since there\'s no uniform safe-default that fits every method. honest scope: the four uniform-shape `is_connected` dispatches were lifted into `engine.is_connected()` (the one cross-server wrapper kept on the engine); the ~18 server-specific chains that do genuinely different per-server work (playlist track replace, metadata sync, scan strategies) stay explicit at the call site per the "lift what\'s truly shared" standard, but reach the per-server client through the engine. 42 tests pin: per-server observable behavior (4 server pinning files, 20 tests), engine surface + accessor contracts (15 tests), structural conformance + explicit-inheritance (9 tests). zero behavior change for users.' }, @@ -3775,6 +3776,17 @@ const WHATS_NEW = { // Section shape: { title, description, features: [bullet strings], // usage_note?: 'optional hint shown at the bottom' } const VERSION_MODAL_SECTIONS = [ + { + title: "Download Discography No Longer Shows Wrong Artist", + description: "clicked download discog on 50 cent → modal showed young hot rod\'s albums. clicked weird al → modal showed beatles albums. real bug, not just data weirdness.", + features: [ + "• endpoint received whichever single artist id the frontend happened to pick and dispatched it as-is to whichever source it queried — when the picked id didn\'t match the queried source\'s id format, lookup either returned wrong-artist results (numeric id collisions) or fell back to a fuzzy name search that picked a different artist", + "• fixed: backend now looks up the library row by ANY stored id (db id, spotify, itunes, deezer, musicbrainz) and dispatches the correct stored id to each source — every source gets its OWN id regardless of what the frontend chose to send", + "• mechanism already existed (`MetadataLookupOptions.artist_source_ids`) and the watchlist scanner already used it — discog endpoint just wasn\'t wired to it", + "• also fixed two log-namespace bugs: enhance quality + multi-source search were writing to a logger with no handlers, so every diagnostic line was silently dropped — now lands in app.log where you can actually see them", + ], + usage_note: "no settings to change — applies on next click of download discography", + }, { title: "Enhance Quality Now Behaves Like Download Discography", description: "discord report — clicking enhance quality on an artist with no spotify/deezer was adding tracks as \"unknown artist - unknown album - unknown track\". root issue ran deeper than that single edge case.",