Commit graph

573 commits

Author SHA1 Message Date
Antti Kettunen
6dca19ca1e Use metadata source priority in unknown artist fixer job
Unknown artist resolution now uses the shared metadata source priority and only filters to the sources that can actually participate in this job. Deezer and iTunes remain direct lookup sources, while Hydrabase can now join the title-search path when it is the configured priority source.
2026-04-15 21:28:10 +03:00
Broque Thomas
fe399636b2 Fix Spotify API calls leaking when Deezer/iTunes is primary source
Spotify was being called for album/artist data fetching across multiple
background workers and the Artists page search even when the user had
Deezer or iTunes set as their primary metadata source. Being authenticated
for playlist sync was treated as permission to use Spotify for everything.

- watchlist_scanner: add _spotify_is_primary_source() that checks both
  auth and primary source config; use it for all album/artist data fetching
  (discovery pool, recent album caching, playlist curation, similar artist
  ID matching, proactive ID backfill). _spotify_available_for_run() is kept
  for sync_spotify_library_cache which must run regardless of primary source
- repair_jobs/metadata_gap_filler: gate Spotify ISRC lookup on primary
  source being 'spotify'; MusicBrainz lookup unaffected
- repair_jobs/unknown_artist_fixer: replace hardcoded spotify_client with
  source-aware client selection — primary source ID tried first, each ID
  matched to its correct client (fixes latent bug passing Deezer IDs to
  Spotify)
- web_server.py /api/match/search: Artists page search was hardcoded to
  spotify_client.search_artists(); now uses _get_metadata_fallback_client()
  so results come from the configured primary source
2026-04-15 09:47:43 -07:00
BoulderBadgeDad
3618f3fa7f
Merge pull request #298 from kettui/fix/respect-metadata-provider-in-album-completeness-job
Honor primary metadata source in album_completeness job and associated repair flow
2026-04-15 07:15:15 -07:00
Broque Thomas
251c27e006 Add Last.fm Track Radio to Discover page
Adds a new Last.fm Radio section to the Discover page that lets users
search a track on Last.fm, generate a similar-tracks playlist, and run
it through the existing discovery/download/sync pipeline. Also generates
playlists automatically from top listening history during watchlist scans
(max once per week).

- core/lastfm_client.py: Add get_similar_tracks() using track.getsimilar
- core/listenbrainz_manager.py: Add save_lastfm_radio_playlist() with
  deterministic MBID (MD5 seed), cleanup limit of 5 for lastfm_radio type
- web_server.py: Add /api/lastfm/configured, /api/lastfm/search/tracks,
  /api/lastfm/radio/generate, /api/discover/listenbrainz/lastfm-radio;
  fix playlist['name'] KeyError in discovery worker that was resetting
  phase back to 'fresh' after completion
- core/watchlist_scanner.py: Add _generate_lastfm_radio_playlists() with
  weekly throttle, called at end of scan_all_watchlist_artists()
- webui/index.html: Add #lastfm-radio-section above ListenBrainz section,
  hidden unless Last.fm API key is configured
- webui/static/script.js: Search/generation/card-load functions; fix
  discovery modal labels (Last.fm Radio vs ListenBrainz), description
  update on completion, belt-and-suspenders completion handling inside
  updateYouTubeDiscoveryModal; fix album/duration display for tracks
  without metadata; music note SVG placeholder for missing art
- webui/static/style.css: Styles for search bar, dropdown, result rows
2026-04-14 23:41:36 -07:00
Antti Kettunen
106d202ccc Share metadata source priority
Centralize the ordered metadata source list and source-priority helper so album completeness and the repair worker follow the same Deezer/iTunes-first fallback order. This also removes the last duplicate priority logic from the touched repair paths.
2026-04-15 07:48:25 +03:00
Antti Kettunen
8a1ae00946 Utilize global Spotify client in repair jobs
Album completeness and any other repair job now uses the centralized source/client helpers instead of a worker-local Spotify client or override plumbing
  - This keeps source selection aligned with the configured primary provider and removes the last Spotify-only special case from the job path.

This change ultimately is a step towards further centralizing the Spotify client access and the associated `is_spotify_authenticated` check.
  - Currently these look-ups are done all over the place in different feature implementations directly, but moving forward, any feature that uses `get_primary_client` or `get_client_for_source` to access the Spotify client, won't have to duplicate any rate-limiting or auth checks as long as these getters are used
2026-04-15 07:18:34 +03:00
Broque Thomas
ce129010e1 Add ReplayGain analysis and tagging support
New core/replaygain.py module uses FFmpeg's ebur128 filter (already a
project dependency) to analyze integrated loudness and true peak, then
writes ReplayGain 2.0 tags (-18 LUFS reference) to MP3 (TXXX frames),
FLAC/OGG/Opus (Vorbis comments), and M4A/MP4 (freeform atoms).

Three analysis modes in the enhanced library view:
- Per-track RG button: synchronous single-track analysis (~1-3 s)
- Album "ReplayGain" button: background job writing both track gain
  and album gain (mean LUFS across all album tracks) to every file
- Bulk bar "ReplayGain" button: batch track-gain for selected tracks

read_file_tags() in tag_writer.py extended with four new optional keys
(replaygain_track_gain/_peak, replaygain_album_gain/_peak) so existing
RG values surface in the tag-preview diff view. Purely additive — no
existing endpoints or DB schema changed.
2026-04-14 19:09:25 -07:00
Broque Thomas
1071b2ebe5 Make OAuth callback ports configurable via environment variables
Hardcoded ports 8888/8889 conflict when SoulSync runs behind Gluetun or
other containers that claim those ports. Introduce SOULSYNC_SPOTIFY_CALLBACK_PORT
and SOULSYNC_TIDAL_CALLBACK_PORT env vars (defaulting to 8888/8889) so
users can remap without rebuilding the image.

docker-compose.yml exposes the vars with comments explaining how to keep
the port mappings in sync with the redirect URI in Settings → Connections.
2026-04-14 14:08:31 -07:00
Broque Thomas
6e405143a7 Improve Tidal download failure diagnostics and error messaging
Track per-quality-tier failure reasons across all failure paths (stream
error, empty manifest, download exception, stub file, MP4 extraction
failure) and include them in the exhausted-tiers log message so failures
are diagnosable from logs.

When HiRes is configured with no fallback and all tiers are exhausted,
log an actionable hint directing the user to enable Quality Fallback.

Surface Tidal-specific error messages in the UI task on retry
exhaustion: distinguishes HiRes-unavailable (with actionable guidance)
from general Tidal auth/quality failures, rather than showing the
generic Soulseek error string.
2026-04-14 14:07:50 -07:00
Broque Thomas
0edb8e93bb Fix MBID Mismatch Detector comparing MB title against DB title (filename) instead of embedded tag
The scan was reading `t.title` from the database for comparison, which is
populated from the filename rather than the file's embedded TITLE tag. Any
track whose filename differs from the clean MB title (e.g. includes artist,
album, bitrate) was incorrectly flagged as a mismatch.

Fix: extend `_read_mbid_from_file` → `_read_file_tags` to also read the
embedded TITLE tag (ID3 TIT2, Vorbis `title`, MP4 `©nam`). The comparison
now uses the embedded title, falling back to the DB title only when no TITLE
tag is present in the file.

Fixes #296
2026-04-14 12:19:41 -07:00
Antti Kettunen
1a459412a3 Honor primary metadata source in album_completeness job
Album completeness and downstream repair flow now follow the configured
primary provider first, with Discogs and Hydrabase support added alongside
existing Spotify, iTunes, and Deezer paths.

Keep spotify_track_id for compatibility while preserving source-aware track
IDs for provider-neutral handling.
2026-04-14 21:14:46 +03:00
Broque Thomas
3b8b369492 Add Your Albums — multi-source liked albums pool (Spotify, Tidal, Deezer)
Builds a new Your Albums section on the Discover page that aggregates
saved/liked albums from all connected services, mirroring the Your Artists
pattern. Deezer works via both OAuth and ARL.

- tidal_client: add get_favorite_albums() with V2/V1 API fallback
- deezer_client: add get_user_favorite_albums() via OAuth (user/me/albums)
- deezer_download_client: add get_user_favorite_albums() via ARL session
- music_database: add liked_albums_pool table (deduped by artist::album
  normalized key), upsert_liked_album, get_liked_albums,
  get_liked_albums_last_fetch, clear_liked_albums
- web_server: GET /api/discover/your-albums (ownership-checked, paginated),
  GET /api/discover/your-albums/sources, POST /api/discover/your-albums/refresh,
  _fetch_liked_albums background worker (Spotify + Tidal + Deezer OAuth/ARL)
- frontend: Your Albums section with source selector cog, album grid reusing
  spotify-library-card styles, search/filter/sort/pagination, download missing
  button, auto-refresh poll on first load

Also fix: Deezer greyed out in Your Artists sources when using ARL — connection
check now accepts ARL auth (deezer_dl.is_authenticated()) in addition to OAuth,
and _fetch_and_match_liked_artists falls back to ARL client for artist fetching.
2026-04-13 23:02:37 -07:00
Broque Thomas
453eb90f19 Add Deezer ARL favorite artists support
Add get_user_favorite_artists(limit=200) to DeezerDownloadClient to fetch a user's favorite artists via the public API using an ARL-authenticated session (paginated, error-handled, returns deezer_id, name, image_url).

Update web_server to treat Deezer as connected if either OAuth or ARL is authenticated, and to fetch favorite artists from OAuth client when available or from soulseek_client.deezer_dl (ARL) otherwise. Fetched artists are upserted into the database and appropriate log/console messages and counters are updated.
2026-04-13 19:46:39 -07:00
Broque Thomas
5bb36412be Fix Jellyfin playlist track fetch truncating at default API limit
get_playlist_tracks() had no Limit or StartIndex params, so Jellyfin
defaulted to ~100 items per response. This caused the Server Playlists
comparison to show most tracks as missing even though they were present.

Now paginates in batches of 1000 until a partial batch signals the last page.
2026-04-13 15:17:52 -07:00
Broque Thomas
20c8bff85c Upgrade artwork to highest available resolution for Spotify and iTunes
Spotify album art: replace the 4-char size segment after '0000ab67616d'
with '82c1' to request the original uploaded master (up to 2000px+).
Applied via _upgrade_spotify_image_url() in Track, Artist, and Album
dataclass constructors and as a catch-all in _download_cover_art.
Scoped to the ab67616d album art prefix only — artist images use a
different prefix (ab676161) where the trick does not apply.

iTunes/Apple Music: replace '100x100bb' with '3000x3000bb' in all
artworkUrl100 replacements across Track, Artist, Album, and the
get_album images arrays. Also applied as a catch-all in _download_cover_art.

Deezer already uses cover_xl at its maximum — no changes needed there.
2026-04-13 15:04:37 -07:00
Broque Thomas
352ad5ff68 Fix AcoustID test connection falsely failing for valid API keys
The fallback test (used when no audio files exist in the library) sends
a dummy fingerprint to the AcoustID API. The API correctly rejects the
dummy fingerprint but this is not error code 4 (invalid key), so the
test was returning False instead of True. Any non-code-4 error from the
fallback means the API key was accepted — only code 4 means a bad key.
2026-04-13 09:37:50 -07:00
Broque Thomas
0edd8f5c81 Raise artist discography limit from 50 to 200 with Deezer pagination
Deezer and iTunes defaulted to 50 albums max, silently truncating large
discographies. Deezer now paginates (100 per page) up to 200. iTunes
raised to 200 (single call). All callers in web_server.py updated to
use the new defaults instead of hardcoding limit=50.

Also adds diagnostic logging for allow_duplicates album comparison
to help debug inconsistent singles behavior.
2026-04-12 21:46:04 -07:00
Broque Thomas
3a7c25f20f Fix allow_duplicates not working for singles in watchlist scanner
Singles like "idol" weren't added when the same song existed on a
different album because check_track_exists used album-aware matching
that found the track via album name. Now skips the album hint when
allow_duplicates is on so matching is title+artist only, then compares
album names ourselves with a strict 0.85 threshold. Only affects users
with allow_duplicates enabled.
2026-04-12 17:41:55 -07:00
Broque Thomas
e65f73abe2 Fix allow_duplicates setting not working in watchlist scanner
The setting only affected wishlist dedup but the watchlist scanner's
library check still skipped tracks by title+artist regardless. Now
when allow_duplicates is enabled, the scanner compares album names
and only skips if the same album matches. Same song on a different
album is allowed through to the wishlist.
2026-04-12 15:41:55 -07:00
BoulderBadgeDad
4f25cf4661
Merge pull request #286 from kettui/fix/improve-graceful-shutdown
Improve shutdown procedures so that the application can close gracefully
2026-04-12 12:42:00 -07:00
Broque Thomas
445cb242f2 Use Deezer contributors field for multi-artist track tagging
Deezer's API returns a contributors array with all credited artists on
a track, but only the primary artist field was used. Now extracts all
contributor names into the artists array for feature/collab tracks.

Only affects the per-track ARTIST tag (TPE1) — album artist, folder
paths, and matching are unchanged. Falls back to single primary artist
when contributors field is absent (search results) or has only one
entry.
2026-04-12 11:52:06 -07:00
Broque Thomas
a7877e6e0b Skip albums with placeholder track names in watchlist scanner
Spotify lists unreleased albums with placeholder names like "Track 1",
"Track 2" before the real tracklist is revealed. The scanner was trying
to download these, searching Soulseek for "Track 1" by artist which
matches random files. Now skips any album where more than half the
tracks match the placeholder pattern. Covers both the watchlist scan
and discovery pool paths.
2026-04-12 10:55:06 -07:00
Broque Thomas
027549be59 Fix album matching using full similarity instead of word subset check
The old subset check treated "Paradise" as matching "Club Paradise"
because {'paradise'} is a subset of {'club', 'paradise'}. Both got
the same +0.10 bonus, so the wrong album could be selected.

Now uses SequenceMatcher for full-string similarity between the wanted
album name and each path segment. Exact matches (>= 0.85) get +0.10,
partial matches (>= 0.60) get +0.03, no match gets +0.00. No penalty
applied — purely adjusts bonus sizing so the correct album ranks higher.
2026-04-12 10:48:24 -07:00
Broque Thomas
1c1be6190a Fix Plex playlists crash on Tag objects and add Unknown Artist guard
Plex API can return Tag objects mixed with playlists — these lack the
playlistType attribute, causing AttributeError. Use getattr with safe
default instead of direct attribute access.

Add 3-tier Unknown Artist guard in post-processing: checks track_info
artists, original search result, then re-fetches from metadata API
before building folder paths or embedding tags. Prevents files from
landing in Unknown Artist folders when the download context has
incomplete artist data.
2026-04-12 10:43:51 -07:00
Broque Thomas
d77274c2ea Reject tracks with Unknown Artist from metadata cache
The junk entity filter checked track name but not artist_name, allowing
tracks like "Woman Like You by Unknown Artist" to be cached. Now rejects
any track or album where artist_name matches the junk names list
(unknown, unknown artist, empty, null, etc). Prevents stale incomplete
data from persisting across retries.
2026-04-12 09:27:17 -07:00
Antti Kettunen
29d964e8b0 Tighten up teardown logic
- Stop active DB update work before tearing down executor pools.
- Short-circuit scan completion callbacks during shutdown so in-flight timer ticks don’t queue follow-up work.
- Prevent the download monitor from draining deferred/completed tasks after shutdown starts.
- Make listening stats startup stop-aware so it exits cleanly if teardown begins during warmup.
- If a metadata update is already running, it can now observe should_stop and exit cleanly instead of continuing after SIGTERM.
2026-04-12 18:13:27 +03:00
Antti Kettunen
aec3047216 Improve graceful shutdown and rollback safety
- Add interruptible stop events to background workers so shutdown
  wakes out of long sleeps instead of waiting on fixed delays.
- Stop scan managers, repair worker, executors, and cleanup helpers
  deterministically so process exit does not leave background threads
  alive.
- Add startup warnings for stale SQLite WAL/SHM sidecars so unclean
  shutdowns are easier to spot before init/migration errors cascade.
- Prevent forced kills from leaving SQLite sidecars behind, which
  made rollbacks to older branches fail with malformed database
  errors.
2026-04-12 15:17:18 +03:00
Broque Thomas
08de91685d Add first-run setup wizard and fix download path reloading
7-step full-screen wizard: Welcome, Metadata Source, Download Source,
Paths & Media Server, Add Artists, First Download, Done. All settings
save to DB identically to the Settings page. Supports all 6 download
sources with inline config and test buttons. First download goes through
the full matched download pipeline with metadata context.

Fixes:
- Download clients (YouTube/HiFi/Tidal/Qobuz/Deezer) now reload
  download_path when settings change instead of caching from init
- watchlist_artists table migrations now include deezer_artist_id and
  discogs_artist_id in all 3 table rebuild locations (was being dropped)
- CREATE TABLE for watchlist_artists includes all provider ID columns
- Serverless download sources (YouTube/HiFi/Qobuz) show green status
  instead of red disconnected on sidebar and dashboard
- Suppress repeated slskd 401 errors — logs once then silences until
  connection recovers
2026-04-11 22:45:16 -07:00
Broque Thomas
71e4df65e3 Remove emojis from all Python log and print statements
Stripped 4,200+ emoji characters from print(), logger calls across
39 Python files. Logs are now clean text — easier to grep, more
professional, no encoding issues on terminals without Unicode support.

Seasonal config icons preserved for UI display.
2026-04-11 21:11:02 -07:00
Broque Thomas
a3ab5adcba Backfill MusicBrainz recording ID from Navidrome during database scan
Navidrome provides musicBrainzId on tracks — now captured during
database updates so the MusicBrainz enrichment worker can skip
tracks that already have an MBID.

Uses COALESCE on UPDATE to never overwrite existing enrichment data
with NULL (safe for Plex/Jellyfin which don't provide this field).

Inspired by PR #279 — fixed data loss bug in the original where
unconditional UPDATE would erase existing MBIDs.
2026-04-11 18:47:54 -07:00
Broque Thomas
7fb4cf6b0e Use per-track artist in tag writer for compilations and DJ mixes
Tag preview and writer now use track_artist (per-track artist) for the
Artist tag when available, falling back to artist_name (album artist)
when NULL. Album Artist tag always uses artist_name.

Fixes #277 — DJ mix albums no longer overwrite per-track artists
(Technique, Gouryella) with the album artist (Tiësto).
2026-04-11 18:31:11 -07:00
Broque Thomas
805f72c5fd Add acappella to live/commentary cleaner patterns 2026-04-11 13:56:56 -07:00
BoulderBadgeDad
8977120ba8
Merge pull request #274 from kettui/feat/metadata-client-caching
Add caching for metadata clients
2026-04-11 13:07:54 -07:00
BoulderBadgeDad
6d0ffae5fb
Merge pull request #275 from kettui/fix/spotify-ratelimited-search
Add missing rate-limit handling for Spotify search requests
2026-04-11 11:26:45 -07:00
BoulderBadgeDad
2ecc434c7a
Merge pull request #276 from kettui/feat/misc-cleanup
Reduce redundant logging during watchlist scan, initialize db once at startup
2026-04-11 11:10:38 -07:00
Broque Thomas
6b2352411b Add Lidarr to all remaining source lists in orchestrator and web_server 2026-04-11 08:05:54 -07:00
Broque Thomas
fc38ec4787 Add Lidarr as 7th download source and validate music video path
Lidarr integration:
- New core/lidarr_download_client.py with full interface parity
  (search, download, status, cancel — same as Qobuz/Tidal/HiFi)
- Registered in download orchestrator with source routing
- Settings: URL + API key on Downloads tab with connection test
- Available as standalone source or in Hybrid mode priority order
- API key encrypted at rest
- All streaming source checks updated to include 'lidarr'

Lidarr downloads full albums via Usenet/torrent — SoulSync imports
only the tracks it needs and discards the rest.

Music video path validation:
- Empty/unconfigured path returns clear error instead of silent failure
- Write permission test before starting download
- Default changed from './MusicVideos' to empty (must be configured)
2026-04-11 07:59:12 -07:00
Antti Kettunen
4946ff0d03 Remove redundant repetition of lookback period change during watchlist scan
Unnecessary noise on the logs
2026-04-11 14:01:52 +03:00
Antti Kettunen
fd6335a66e Add / improve metadata client caching
Clients are for the most part being initialized per-request, which leads to a lot of redundant client initialization, as well as noise on the logs, since each client initialization emits a row on the logs, eg. 'Deezer client initialized'
2026-04-11 13:55:10 +03:00
Antti Kettunen
1b979193eb Skip Spotify requests for the rest of the watchlist scan if rate-limited
State is stored per-scan
2026-04-11 13:41:05 +03:00
Antti Kettunen
36dbb3357e Add rate-limit handling for Spotify searches 2026-04-11 13:14:50 +03:00
Broque Thomas
54b7a0f0e8 Add music video download with progress and metadata matching
Click any video card in Music Videos tab to download. Flow:
1. Search primary metadata source for clean artist/title
2. Fall back to YouTube title parsing if no match
3. Download video via yt-dlp (best quality MP4)
4. Save to configured Music Videos folder as Artist/Title-video.mp4

UI shows circular progress ring on the thumbnail during download,
green checkmark on completion, red X on error (clickable to retry).
Cards are non-interactive while downloading.

Backend: /api/music-video/download and /api/music-video/status endpoints
YouTube client: download_music_video() method keeps video format
2026-04-10 23:14:20 -07:00
Broque Thomas
b44bb34b44 Add Music Videos search tab to enhanced and global search
New "Music Videos" pill tab alongside Spotify/Deezer/iTunes/Discogs
in both enhanced search and global search. Searches YouTube via yt-dlp
and displays results in a video card grid with 16:9 thumbnails, play
overlay, duration badge, channel name, and view count.

- Backend: /api/enhanced-search/source/youtube_videos endpoint with
  search_videos() method on YouTubeClient returning YouTubeSearchResult
- Frontend: Video grid layout with responsive cards, YouTube red tab
  color, proper section hiding when switching between metadata and
  video tabs
- Global search: Full parity with enhanced search video rendering
- No download functionality yet — display only
2026-04-10 23:07:14 -07:00
Broque Thomas
5be6a46fb0 Fix dismissed findings reappearing and reduce false orphan detections
The finding dedup check only looked for 'pending' and 'resolved' status,
missing 'dismissed'. Dismissed findings were recreated as new entries on
every scan. Now includes 'dismissed' in the dedup check.

Orphan file detector improvements:
- Increased path suffix matching depth from 3 to 4 segments (covers
  Genre/Artist/Album/track.flac paths)
- Added filename-based fallback when Mutagen can't read file tags —
  parses title from "NN - Title [Quality].ext" pattern and matches
  against parent/grandparent folder names as artist
2026-04-10 17:42:39 -07:00
Broque Thomas
603d66ba5d Add artist gate and fix substring matching in matching engine
Prevents downloading tracks from completely wrong artists by adding
minimum artist score gates:
- Soulseek: artist_score < 0.25 → reject (catches Belvedere vs Periphery)
- YouTube: artist_score < 0.15 → reject (catches lizzylou06 vs Muse)

Fixes artist substring matching to use word boundaries instead of plain
containment — "muse" no longer matches "museum", "art" no longer matches
"heart". This was causing false positives where wrong artists passed with
artist_score=1.0 due to accidental substring containment.

Improves similarity fallback by comparing against individual path segments
instead of the full filename, so misspelled artist names (Radiohedd vs
Radiohead) still match correctly.

Adjusts YouTube weights from Title 70%/Artist 10% to Title 60%/Artist 20%
to give artist more influence in YouTube matching.

Addresses user reports of unreleased albums being downloaded with garbage
content from wrong artists on Soulseek and YouTube.
2026-04-10 13:45:07 -07:00
Broque Thomas
a6117d5174 Replace all legacy metadata_service imports with canonical functions
All callers of _create_fallback_client() and _get_configured_fallback_source()
now use get_primary_client() and get_primary_source() directly. No more
legacy alias usage anywhere in the codebase.
2026-04-10 12:52:36 -07:00
Broque Thomas
10a2766557 Fix remaining Spotify-first source selection in seasonal discovery and search API
Seasonal discovery had 3 use_spotify checks using is_authenticated()
(always True) instead of deriving from the configured source. Search API
(tracks, albums, artists) also defaulted to Spotify when authenticated.

All now check configured primary source first via get_primary_source().
2026-04-10 12:47:16 -07:00
Broque Thomas
498c22e7c3 Centralize metadata source selection in core/metadata_service.py
All metadata source decisions now flow through get_primary_source() and
get_primary_client() in core/metadata_service.py. Previously 6 different
files reimplemented this logic with inconsistent defaults ('itunes' vs
'deezer') and auth checks, causing bugs when any one was missed.

Changes:
- metadata_service.py: Added canonical get_primary_source/get_primary_client
- web_server.py: _get_metadata_fallback_source() and _get_active_discovery_source()
  are now thin wrappers delegating to metadata_service
- seasonal_discovery.py: _get_source() delegates to metadata_service
- personalized_playlists.py: _get_active_source() delegates to metadata_service
- spotify_client.py: Fixed _fallback_source default from 'itunes' to 'deezer'
- watchlist_scanner.py: _get_fallback_metadata_client() delegates to metadata_service

Future changes to source selection only need to update one file.
2026-04-10 12:34:25 -07:00
Broque Thomas
52a5d93018 Add Fix Unknown Artists maintenance job
New repair job that scans the library for tracks filed under "Unknown
Artist" and corrects them. Resolves correct metadata by:
1. Reading embedded file tags (if file has correct artist)
2. Looking up by source track ID (Spotify/Deezer/iTunes)
3. Searching by title as last resort

Dry run mode (default) creates findings for review. Live mode re-tags
the audio file, moves it to the correct folder structure, and updates
the database. Includes fix handler for applying individual findings.
2026-04-10 12:17:11 -07:00
Broque Thomas
57fc18f994 Respect configured primary source in seasonal, playlists, and explorer
Seasonal discovery, personalized playlists, and playlist explorer all
defaulted to Spotify when authenticated, ignoring the user's configured
primary source. Now they read from config first.

Spotify's related_artists API (no Deezer/iTunes equivalent) is preserved
as a fallback for all users in personalized playlists. Artist discography
endpoint intentionally unchanged — ID-based lookups need the source that
owns the ID.
2026-04-10 11:43:06 -07:00