Commit graph

743 commits

Author SHA1 Message Date
Broque Thomas
119840fa5d Redesign toast notification system with bell button and history panel
Complete replacement of the old bottom-center stacking toast system:

Compact Toasts: Single toast at a time, bottom-right above buttons.
Pill shape with type-colored left border stripe, icon, message, and
optional "Learn more" link. Slides in, fades out after 3.5s. Click
to dismiss. New toasts replace the current one smoothly.

Notification Bell: 44px circle button next to the helper (?), with
red badge counter for unread notifications. Click opens panel.

Notification Panel: Glass popover above bell button showing history
of last 50 notifications. Each entry has type icon, message, relative
timestamp, and optional help link. Unread dot indicator. Clear All
button. Marks all as read when panel opens.

Same showToast(message, type, helpSection) signature — all 842
callers unchanged. Deduplication preserved. Updated version modal
and helper What's New.
2026-04-01 16:41:41 -07:00
Broque Thomas
ce9ba42a91 Fix hybrid download mode not trying fallback sources (#235)
The download orchestrator's hybrid search stops at the first source
that returns ANY results, even if all those results fail quality
filtering. This meant Soulseek returning 100 low-quality results
would prevent HiFi/Tidal/YouTube from ever being tried.

Added hybrid fallback in the download worker: when all queries exhaust
with no valid candidates and hybrid mode is active, remaining sources
in the hybrid order are searched directly, bypassing the orchestrator's
stop-at-first-hit logic. Each fallback source gets the first 2 queries
with full quality filtering and candidate validation.

Fixed guard condition from convoluted diagnostic string parsing to a
simple mode check. Uses getattr for safe attribute access.
2026-04-01 15:49:51 -07:00
Broque Thomas
8105f589ce Update version modal and helper What's New with all recent features
Added: Track Redownload with source info and provenance tracking,
Spotify API rate limit improvements (caching, auto-pause workers),
YouTube Topic fix, CAA art toggle, Genius search improvements,
Genius rate limit interval increase. Updated existing redownload
entry to reflect final implementation (removed old blacklist-from-delete
reference, added streaming source search and provenance tracking).
2026-04-01 15:39:23 -07:00
Broque Thomas
b8006d93c3 Refine track number filename extraction — separator required
_extract_track_number_from_filename now requires a separator after
digits (dash, dot, bracket) to prevent parsing artist names like
"50 Cent" as track number 50. Also handles disc-track format "1-03".

This is a last-resort fallback only — standard downloads get track
numbers from Spotify/iTunes/Deezer metadata. Only affects files with
no metadata where the filename starts with digits.
2026-04-01 15:24:03 -07:00
Broque Thomas
0493f566df Fix redownload pipeline — full parity, stuck batch, button timing
Pipeline parity: redownload/start now fetches full track details from
the selected metadata source (Spotify/iTunes/Deezer) for real
track_number, disc_number, and album context. Sets explicit album
context flags so post-processing uses the standard album download path.

Stuck batch fix: active_count was 0, decremented to -1 on completion,
so batch never detected as complete. Now initializes active_count=1
and queue_index=1 since we submit the worker directly.

Button timing: Download Selected handler wired up immediately before
streaming starts, reads from window._redownloadCandidates which
updates live as results arrive. No longer blocked by slow Soulseek.

Track number: _extract_track_number_from_filename requires separator
after digits so "50 Cent" is not parsed as track 50.

Progress: real download stats from /api/downloads/status. Handles
streaming sources showing "Processing..." when no transfer found.
2026-04-01 14:59:30 -07:00
Broque Thomas
af98cb54c4 Fix redownload pipeline — Track object, confidence, lock safety
Three bugs that would crash when clicking Download Selected:

1. Passed raw dict to _attempt_download_with_candidates which accesses
   track.artists/track.album as attributes — now constructs Track object
2. TrackResult missing confidence attr — sort would crash — now set
   from candidate data
3. Redownload hook (old file delete + DB update) was inside tasks_lock
   blocking all task state changes — moved outside lock with proper
   None initialization
2026-04-01 14:14:50 -07:00
Broque Thomas
0fd6be3239 Stream download source results progressively via NDJSON
Step 2 of the redownload modal now streams results as each download
source responds instead of waiting for all sources to finish. Tidal/
YouTube/Qobuz columns appear instantly while Soulseek searches.

Backend: search-sources endpoint uses ThreadPoolExecutor + NDJSON
streaming — one JSON line per source as it completes.

Frontend: reads the NDJSON stream, appends columns with fade-in
animation as each source responds. Download button enables as soon
as any results arrive.

Each source gets its own column with results grouped and sorted by
confidence. Visual confidence bars, format badges, and source-specific
metadata (Soulseek username/slots). Best overall match auto-selected.
2026-04-01 13:53:57 -07:00
Broque Thomas
40f3621c48 Redesign redownload modal — columns, glass blur, Deezer fix, cover art
Major redesign:
- All metadata sources shown as side-by-side columns (not tabs)
- Frosted glass modal background with blur(40px) saturate(1.4)
- Album cover art in header from DB thumb_url (resolved for Plex)
- 1100px width, all elements scaled up, white text on accent buttons

Bug fixes:
- Deezer: use global singleton client, title-only fallback search,
  strip version suffixes from query
- Track.__init__: added missing popularity=0 parameter
- Overlay: dedicated .redownload-overlay class avoids CSS conflicts
2026-04-01 12:33:14 -07:00
Broque Thomas
04f01d36e1 Add track download provenance tracking and source info UI (#234)
New track_downloads table records every download with full source data:
service type (soulseek/youtube/tidal/etc), username, remote filename,
file size, and audio quality. Recorded at all 3 post-processing
completion points.

Source Info button (ℹ) on each track in the enhanced library view shows
a popover with download provenance: service, username, original filename,
size, quality, download date. Includes "Blacklist This Source" button
that stores the real username+filename (not guessed local filenames).

Removed broken "Delete & Blacklist" option from Smart Delete since it
had no access to real source data. Blacklisting now done exclusively
from the Source Info popover where actual provenance data exists.

Added blacklist CRUD API endpoints (GET/POST/DELETE /api/library/blacklist).
2026-04-01 11:37:21 -07:00
Broque Thomas
f8f87e0e38 Update version modal and helper What's New with redownload feature 2026-04-01 10:40:07 -07:00
Broque Thomas
35dd0546d1 Add Track Redownload modal with manual source selection (#234)
Three-step redownload flow in the enhanced library view:
1. Metadata Source — searches Spotify/iTunes/Deezer simultaneously,
   shows results with match scores, flags current match
2. Download Source — searches all active download sources (Soulseek,
   YouTube, Tidal, etc.), shows candidates with format/bitrate/size/
   confidence, flags blacklisted sources
3. Download — starts download, polls for progress, deletes old file
   on success, updates DB path

Also integrates the download blacklist into the download pipeline —
_attempt_download_with_candidates now skips blacklisted sources
automatically during all downloads (wishlist, playlist sync, etc.).

New redownload button (↻) on each track row in enhanced library view.
Post-processing hook deletes old file and updates DB track path after
successful redownload.
2026-04-01 10:36:51 -07:00
Broque Thomas
8d6486bee3 Add Smart Delete with file removal and download blacklist (#234)
Track delete in the enhanced library now shows three options:
- Remove from Library: DB record only (existing behavior)
- Delete File Too: DB + os.remove() the file from disk
- Delete & Blacklist: DB + file removal + add source to blacklist

New download_blacklist table stores rejected sources (username + filename)
with CRUD methods. Blacklist will be checked by the download pipeline
and the upcoming track redownload modal.

Smart delete modal styled with the same glass/dark theme as other
SoulSync modals, with color-coded destructive options.
2026-04-01 10:29:07 -07:00
Broque Thomas
660221d86a Show 'Yielding for downloads' on auto-paused enrichment workers
Dashboard enrichment chips show 'Yielding' instead of 'Paused' when
workers are auto-paused during downloads. Tooltips show 'Yielding for
downloads' for full context. Distinguishes user-paused from auto-paused.

Also handles edge case where user manually resumes a worker during
downloads — adds to override set so the loop doesn't re-pause it.
Override resets when downloads finish so next download session re-pauses.
2026-04-01 09:09:33 -07:00
Broque Thomas
c73df05fd9 Auto-pause rate-limited enrichment workers during downloads
Spotify, Last.fm, and Genius enrichment workers are now automatically
paused while any download batch is active. This prevents enrichment
API calls from competing with post-processing metadata lookups for
rate limit headroom, especially during heavy download scenarios
(3 playlist workers + wishlist downloads simultaneously).

Workers resume automatically when all downloads finish. Only workers
that were auto-paused are resumed — manually paused workers stay paused.
Piggybacks on the existing 2-second enrichment status loop with no new
threads or timers.
2026-04-01 08:55:38 -07:00
Broque Thomas
778e68a844 Improve Genius artist search for manual matching (#233)
The manual match modal for Genius only returned 0 or 1 artist result
because search_artist() searched songs (per_page=5), extracted the
primary artist, and returned the first match or None.

Added search_artists() that returns multiple unique artists extracted
from song results with broader search (per_page=20). The manual match
endpoint now shows up to 8 artist candidates and multiple track results
instead of one-or-nothing. Also shows the Genius URL as extra info.
2026-04-01 08:31:17 -07:00
Broque Thomas
982ca77501 Make Cover Art Archive album art opt-in instead of default (#232)
CAA art can be higher resolution (1200x1200+) but quality is
inconsistent — some releases have cellophane-wrapped photos or
low-quality scans. Spotify/iTunes/Deezer art is lower res (640x640)
but consistently clean and official.

New toggle: Settings → Post-Processing → "Use MusicBrainz Cover Art
Archive for album art" (off by default). Applies to both embedded
art and cover.jpg downloads.
2026-04-01 07:59:40 -07:00
Broque Thomas
89ef5f931f Route all Spotify search calls through cached methods
Five places in web_server.py called spotify_client.sp.search() directly,
bypassing the cached search_tracks()/search_artists() methods. Each
discovery worker (Tidal, YouTube, ListenBrainz, Beatport) was also
doubling API calls — sp.search() for raw data then search_tracks() for
Track objects.

Now all use cached methods only. Raw track data for album art is
retrieved from the metadata cache by track ID after matching. Also fixed
a pre-existing bug where Tidal discovery could pair stale Spotify raw
data with a newer iTunes match.

Bumped is_spotify_authenticated() probe cache TTL from 5 to 15 minutes
to reduce /v1/me calls (~288/day → ~96/day). Manual disconnect still
takes effect immediately via _invalidate_auth_cache().
2026-03-31 22:18:02 -07:00
Broque Thomas
54f927320d Fix skip_cache crash when watchlist scan uses non-Spotify client
iTunes and Deezer clients don't accept skip_cache param — only pass
it when the active client is the Spotify client (detected via sp attr).
2026-03-31 22:01:20 -07:00
Broque Thomas
a7ebde8c01 Add skip_cache param to get_artist_albums for watchlist scans
The watchlist auto-scan needs fresh data from Spotify to detect new
releases, so it bypasses the cache added in the previous commit.
All other callers (UI browsing, completion badges, discography views)
continue to benefit from cached results.
2026-03-31 21:59:46 -07:00
Broque Thomas
87f17a1318 Fix cover.jpg not using Cover Art Archive high-res source
The MusicBrainz release ID found by _embed_source_ids was stored in the
metadata dict but never propagated to album_info. The old code tried to
write album_info['musicbrainz_release_id'] inside _embed_source_ids, but
album_info wasn't in that function's scope — causing a silent NameError.

Fix: copy the MBID from metadata to album_info in _enhance_file_metadata
(where both are in scope) right after _embed_source_ids returns. This
makes _download_cover_art see the MBID and use Cover Art Archive for
cover.jpg instead of falling back to the smaller Spotify/iTunes thumbnail.
2026-03-31 21:40:19 -07:00
Broque Thomas
f275a9831e Strip '- Topic' suffix from YouTube auto-generated channel names (#231)
YouTube's auto-generated artist channels use the format "Artist - Topic"
as the channel name. This suffix was not being stripped during playlist
parsing, causing metadata discovery to fail (e.g., searching for
"Koven - Topic" instead of "Koven" on iTunes/Deezer).

Fixed in all three places where YouTube artist names are cleaned:
- web_server.py clean_youtube_artist() — playlist parsing
- ui/pages/sync.py clean_youtube_artist() — UI-side parsing
- core/youtube_client.py — search result fallback artist extraction
2026-03-31 21:06:05 -07:00
Broque Thomas
34c8b1bb50 Add Server Playlist Manager with dual-column compare editor
New "Server Playlists" tab (default on Sync page) lets users compare
mirrored playlists against their media server and fix match issues.

- Dual-column comparison: source tracks (left) vs server tracks (right)
- Smart matching: exact title first, then fuzzy artist+title (≥75%)
- Find & Add: search library to fill missing slots at correct position
- Swap: replace matched tracks with different versions
- Remove: delete tracks from server playlists with confirmation
- Title similarity percentage badge on each match
- Disambiguation modal when multiple mirrored playlists share a name
- Album art on source tracks, server tracks, and search results
- Cross-column click-to-scroll highlighting
- Filter buttons (All/Matched/Missing/Extra) with live counts
- Escape key and backdrop click to close modals
- Mobile responsive (stacked columns under 768px)
- Works with Plex, Jellyfin, and Navidrome
2026-03-31 20:58:27 -07:00
Broque Thomas
de3fba3f37 Add Sync History dashboard with per-track match caching and detail modal
New dashboard section shows recent syncs as scrolling cards with
playlist art, source badge, match percentage bar, and health color.
Click any card to open a detail modal showing every track's match
status, confidence score, album art, and download/wishlist status.

Per-track data is now cached in sync_history.track_results for all
sync paths: server-sync (playlist→media server), download missing
tracks, and wishlist processing. SyncResult carries match_details
from the sync service. Both image URLs and matched track info are
preserved for review.

Features:
- Staggered card entrance animation, delete button on hover
- Filter bar: All/Matched/Unmatched/Downloaded
- Color-coded confidence badges (green/amber/red)
- Unmatched tracks show "→ Wishlist" status
- 32px album art thumbnails per track row
- Auto-refreshes every 30 seconds on dashboard
- Falls back gracefully for old syncs without track_results
2026-03-31 15:26:46 -07:00
Broque Thomas
ad262822a4 Cache per-track results in sync history + fix config_manager import
Sync operations now store per-track data (name, artist, match status,
confidence, download status) in a new track_results column on
sync_history. Also fixed missing config_manager import in
add_to_wishlist that crashed the duplicate tracks toggle.
2026-03-31 14:21:37 -07:00
Broque Thomas
cfe2ab7dec Add toggle to disable auto-clearing slskd search history
Users who keep manual searches in slskd as reminders were losing
them when SoulSync auto-cleaned at 200+ entries. New toggle in
Settings → Downloads → Soulseek: "Auto-clear slskd search history"
(on by default, preserving current behavior). When disabled, both
the hourly cleanup automation and the full cleanup step skip the
search history maintenance.
2026-03-31 13:03:37 -07:00
Broque Thomas
f4407490c1 Skip slskd connection check when Soulseek is not an active source
The dashboard status poll and hybrid connection check were pinging
slskd every 2 minutes regardless of download source, flooding logs
with connection errors when slskd wasn't running. Now only checks
slskd when the download mode is 'soulseek' or when 'soulseek' is
in the hybrid order. Hybrid mode also only checks sources in the
configured priority order instead of all six.
2026-03-31 12:37:14 -07:00
Broque Thomas
2f4ff8213f Fix artist sync endpoint returning 404 for non-integer IDs (#230)
Route used <int:artist_id> which rejected Spotify/iTunes/Deezer
artist IDs. Changed to accept any string — tries as DB integer ID
first (verified against DB), then falls back to checking
spotify_artist_id, itunes_artist_id, deezer_artist_id columns.
Handles numeric iTunes/Deezer IDs correctly by verifying the DB
row exists before assuming it's a DB ID.
2026-03-31 11:33:46 -07:00
Broque Thomas
c50eee1563 Fix discovery sync running outside Flask context + pass profile ID
_sync_discovery_results_to_mirrored was calling get_current_profile_id()
which requires Flask's g context, but runs in a background thread.
Now captures the profile ID in the Flask endpoint (where context exists)
and passes it through the discovery state to the background worker.
Also added debug logging to trace mirrored playlist matching.
2026-03-31 10:54:11 -07:00
Broque Thomas
06f0e53a6f Sync discovery results back to mirrored playlist extra_data
Tidal, Deezer, and Beatport discovery workers found metadata matches
but only stored them in memory and the discovery cache — never wrote
them back to the mirrored playlist tracks. This meant playlists had
to be re-discovered every time. Now writes {discovered, provider,
confidence, matched_data} to each mirrored track's extra_data after
discovery completes, matching the pattern YouTube and automation
pipeline discovery already used. Matches tracks by source_track_id
first, falls back to position index. Purely additive — discovery
logic is untouched.
2026-03-31 10:27:44 -07:00
Broque Thomas
3154d16cf3 Fix slow Tidal playlist loading — metadata only, no track fetching
get_user_playlists_metadata_only() was fetching full track lists for
every playlist sequentially (1+ API calls per playlist with 1s sleep
between pagination pages). For 20+ playlists this took 30-60 seconds.

Now returns only metadata (name, ID, track count, image) from a
single V2 API call. Track count comes from the numberOfTracks
attribute. Tracks are fetched on-demand when the user selects a
specific playlist to sync/mirror via the existing get_playlist()
endpoint.
2026-03-31 09:27:15 -07:00
Broque Thomas
03298afac1 Change fresh install defaults: hybrid download + Deezer metadata
Fresh installs now default to hybrid download mode (HiFi → YouTube →
Soulseek) instead of Soulseek-only, and Deezer as the metadata
fallback source instead of iTunes. Existing users with saved settings
are unaffected — defaults only apply when config keys don't exist.
2026-03-31 09:04:35 -07:00
Broque Thomas
1fb9b45d59 Fix wishlist download status tooltip showing wrong track name (#227)
The tooltip on failed/not_found tracks was offset by 3-6 items because
the backend cleanup step removed owned tracks from the wishlist between
when the frontend rendered the table and when the backend assigned
track indices. Surviving tracks got new enumeration indices (0,1,2...)
that didn't match their original table row positions (0,1,3,4...).

Fix: stamp each track with its position in the frontend's track_ids
array as _original_index, so the track_index always matches the modal
table row regardless of how many tracks were cleaned during processing.
2026-03-31 08:41:01 -07:00
Broque Thomas
6c4d8d9348 Fix race condition: paused enrichment workers making API calls on startup
Worker threads were started before paused flag was set, allowing one
loop iteration (and API call) before pause took effect. Now sets
paused=True BEFORE start() so the thread sees it immediately. Fixes
the Spotify rate limit re-trigger on every container restart for users
with paused enrichment. Also increased max-retries rate limit ban
from 1 hour to 4 hours to prevent endless retry cycles.
2026-03-31 08:14:25 -07:00
Broque Thomas
a52edde733 Fix discovery toast always showing iTunes instead of active source
YouTube and ListenBrainz discovery toasts had source_label hardcoded
to 'iTunes' when not using Spotify. Now uses discovery_source.upper()
like the other discovery functions, so it correctly shows DEEZER when
Deezer is the active metadata source.
2026-03-30 22:20:44 -07:00
Broque Thomas
13ea2fc1fe Guard album_info None check in cover art MBID storage
album_info can be None for single track downloads — the dict
assignment would crash with TypeError. Added None check.
2026-03-30 22:02:45 -07:00
Broque Thomas
f3047c46cf Use Cover Art Archive for cover.jpg downloads (high-res)
cover.jpg was always using the Spotify/iTunes thumbnail (640x640).
Now tries Cover Art Archive first (1200x1200+) when MusicBrainz
release ID is available. One line stores the MBID on album_info
during _enhance_file_metadata so _download_cover_art can use it.
Falls back to the source thumbnail if CAA has no art. Works for
all metadata sources since MusicBrainz enrichment is source-agnostic.
2026-03-30 19:49:17 -07:00
Broque Thomas
d944d4a7d2 Fix Japanese/CJK text mangled in Soulseek search queries
normalize_string() was running unidecode on all text, converting
Japanese kanji to Chinese pinyin gibberish (命の灯火 → "tvanimedei").
Now detects CJK characters (kanji, hiragana, katakana, hangul,
fullwidth forms) and skips unidecode for text containing them —
just lowercases instead. Non-CJK text (Latin accents, Cyrillic)
still goes through unidecode normally.
2026-03-30 18:28:25 -07:00
Broque Thomas
1646c3d9e1 Fix partial name matching false positives (#225)
"Believe" was falsely matching "Believe In Me" because SequenceMatcher
gives high scores when the search string is fully contained in the
match. Added a length ratio penalty: when cleaned titles differ in
length by more than 30%, the similarity score is multiplied by the
ratio (min/max length). This crushes prefix/suffix false positives
while leaving exact matches and cleaned variants (remastered, deluxe)
unaffected.
2026-03-30 17:58:59 -07:00
Broque Thomas
f788361b08 Fix pipeline stopping when metadata discovery fails (#224)
Playlist auto-sync was dropping tracks that failed iTunes/Apple Music
discovery — they never reached the wishlist or download pipeline. Now
undiscovered tracks continue through using available metadata: first
from the spotify_hint (embed scraper data with real Spotify track ID,
name, artists), then from raw playlist fields if a source track ID
exists. Album cover art from the mirrored playlist is included. Only
tracks with no usable ID or name are skipped.
2026-03-30 17:50:30 -07:00
Broque Thomas
69346ec313 Add Playlist Explorer — visual discovery tree for expanding playlists
New dedicated Explorer page with interactive node graph visualization.
Users select a mirrored playlist, choose Albums or Discographies mode,
and the app builds a branching tree: playlist root → artist nodes →
album nodes → track nodes. Supports all metadata sources (Spotify,
iTunes, Deezer) with source-aware discovery cache integration.

Features:
- Streaming NDJSON builds tree progressively as artist data arrives
- Circular artist nodes with photos, rounded album nodes with art
- SVG bezier connections that draw in on completion, fade on hover
- Click artist to expand albums, double-click album for track listing
- Single-click albums to select, Select All/Deselect for bulk ops
- Wishlist confirmation modal with per-album progress (NDJSON streaming)
- Artist nodes glow when any of their albums are selected
- Playlist picker with source tabs, discovery % gate (50% minimum)
- Zoom (scroll/pinch/buttons), pan (right/middle-drag), fit-to-view
- Metadata cache for discographies and album track listings
- Owned album detection from library database
- Fallback track-name matching when album names are missing
2026-03-30 17:13:30 -07:00
Broque Thomas
fde1a7d77e Fix .lrc files written without timestamps for plain lyrics
Plain (unsynced) lyrics were being saved with .lrc extension despite
having no timestamps, making them invalid for Plex and other players
that expect LRC format. Synced lyrics now write as .lrc, plain lyrics
write as .txt. Both types still get embedded in audio file tags.
Updated all file move/rename operations to handle .txt sidecars
alongside .lrc.
2026-03-30 13:28:41 -07:00
Broque Thomas
c5652b0d4b Add API call counts and Spotify budget to dashboard service chips
Enrichment chips now show live activity: 24h call count for all
services and daily budget usage (used/3,000) with gradient progress
bar for Spotify. Tracking is centralized in _get_enrichment_status
using cumulative stat diffs over a rolling deque — no worker files
modified. Added section header, "Configure →" label for unconfigured
services, and full 1h/24h breakdown in tooltips.
2026-03-30 13:20:22 -07:00
Broque Thomas
525a09c840 Fix collaborative album artist not applied to single downloads (#215)
Single path template was missing _artists_list and _itunes_artist_id
context keys, so the collab mode first-artist extraction in
_apply_path_template had nothing to work with — $albumartist resolved
to the full multi-artist string. Added both keys matching the exact
pattern used by album and playlist modes, including the iTunes
spotify_album.external_urls fallback. Updated settings UI hints to
show $albumartist as available for single and playlist templates.
2026-03-30 11:37:05 -07:00
Broque Thomas
f2e24a36df Fix enrichment overwriting manual match status (#221)
When a user manually matched an artist to a service ID then triggered
enrichment, the worker re-searched by name, failed to find a match,
and overwrote the status back to not_found — despite the ID being
valid. Now both Genius and AudioDB workers check for existing service
IDs before searching by name. If an ID exists (from manual match),
the worker uses it for a direct API lookup to enrich metadata while
preserving the matched status. Added AudioDB lookup-by-ID client
methods for artist, album, and track.
2026-03-30 11:07:48 -07:00
Broque Thomas
edaa55ae82 Harden Spotify OAuth callback for Docker/SSH tunnel setups (#220)
Top-level try/except in do_GET ensures an HTTP response is always sent
— previously, unhandled exceptions caused BaseHTTPRequestHandler to
silently close the connection (ERR_EMPTY_RESPONSE). All callback
logging now uses the app logger instead of print() so output appears
in app.log rather than only Docker stdout. Added health check at / to
verify the callback server is running, and startup now logs the actual
bind address to help diagnose port conflicts.
2026-03-30 10:19:32 -07:00
Broque Thomas
32adc66fe3 Show all services on dashboard with click-to-configure (#219)
Dashboard now displays all enrichment services as live-status chips
below the core service cards. Each chip shows Running, Idle, Paused,
Stopped, or Not Configured state with color-coded left border accents.
Unconfigured services appear dimmed with dashed borders — clicking any
configurable chip navigates to Settings → Connections and scrolls to
the relevant service section.

Also fixes the Spotify card always being labeled "Apple Music" when
using iTunes fallback — card now always says "Spotify" with an amber
"Using iTunes/Deezer" indicator when fallback is active.
2026-03-30 10:04:13 -07:00
Broque Thomas
ab8e44dafd Add Qobuz credentials to Settings Connections tab (#218)
Qobuz login was only available on the Downloads tab when Qobuz was
selected as download source. But Qobuz credentials are also needed
for the enrichment worker which runs independently. Users saw
"Connect Qobuz in Settings" on the dashboard but couldn't find it.

Adds a Qobuz section to Settings → Connections (same pattern as
Tidal's existing Connections section). checkQobuzAuthStatus() now
syncs both the Connections and Downloads tab sections. Login from
either tab updates both. No backend changes — same API endpoints.
2026-03-30 08:45:30 -07:00
Broque Thomas
7133595e0d Fix enrichment widget showing Running when rate limited (#217)
The tooltip only checked paused/authenticated/idle/running states.
When Spotify was rate limited or daily budget exhausted, the worker
thread was still alive (sleeping in guards) so it showed "Running"
with no current item and stale 0% progress.

Now checks rate_limited and daily_budget.exhausted before running:
- Rate limited: "Rate Limited — Waiting Xm for rate limit to clear"
- Budget exhausted: "Daily Limit Reached — Resets in Xh Xm"
- No current item: "Waiting for next item..." instead of blank

Also adds rate_limit info object to get_stats() response for the
countdown display.
2026-03-30 08:12:56 -07:00
Broque Thomas
59587162cd Add metadata cache maintenance and health monitoring
Cache maintenance:
- Input validation rejects junk entities (Unknown Artist, empty names)
  from being cached, with exemptions for synthetic entries (_features,
  _tracks suffixes)
- CacheEvictorJob expanded to 4 phases: TTL eviction, junk cleanup,
  orphaned search cleanup, MusicBrainz failed lookup cleanup
- MusicBrainz null results now expire after 30 days (was 90) so failed
  lookups get retried sooner

Cache health UI:
- Polished modal accessible from Dashboard "Cache Health" button and
  repair dashboard health bar
- Shows health status banner (healthy/fair/poor), stat cards, source
  breakdown with colored progress bars, type pills, and metrics table
- Repair dashboard shows compact bar with health dot indicator
2026-03-30 07:40:20 -07:00
Broque Thomas
4e6b424bc7 Fix wishlist Download Selection ignoring checkbox selections
downloadSelectedCategory() was passing only the category name to the
download function, which fetched ALL tracks in that category. Now
collects checked track IDs from checkboxes BEFORE closing the modal
(DOM is destroyed on close), then filters the fetched tracks to only
the selected ones.

If nothing is checked, downloads the full category (same as before).
Other callers of openDownloadMissingWishlistModal are unaffected —
the new selectedTrackIds parameter defaults to null.
2026-03-29 21:43:48 -07:00