Commit graph

1702 commits

Author SHA1 Message Date
Broque Thomas
08f708eb71 Add logging to path mismatch fix handler for diagnosis
The fix was failing silently — returning error results without
writing to any log. Added info/warning logs at each failure point
(missing paths, source not found, path escapes transfer, destination
conflict) so Docker path resolution issues can be diagnosed.
2026-03-28 09:26:19 -07:00
Broque Thomas
b5b03a2b86 Add Download Discography feature on artist detail page
New "Download Discography" button in artist hero section opens a modal
showing the full catalog — albums, EPs, and singles — with filter
toggles, select/deselect all, and per-album owned/missing indicators.

Modal features:
- Glassmorphic design with artist image blurred background header
- Filter pills for Albums/EPs/Singles with instant grid filtering
- Album cards with cover art, year, track count, and checkbox
- Owned albums dimmed and unchecked by default, missing pre-selected
- Live NDJSON streaming: each album updates in real-time as processed
- "Process Wishlist Now" button after completion
- Albums sorted by track count (Deluxe first) to prevent duplicate
  folder contexts from standard/deluxe edition ordering

Backend: NDJSON streaming endpoint POST /api/artist/<id>/download-discography
- Fetches tracks per album via active metadata client
- Adds to wishlist with dedup (no slow fuzzy matching)
- Streams one JSON line per album as it completes
- Works on both Artists search page and Library artist detail page
2026-03-27 22:15:05 -07:00
Broque Thomas
2909d29614 Update version modal and helper What's New with all recent features
Added 7 new sections to version modal: Stream Source, YouTube Fix,
Completion Badges, Collab Album Handling, Per-Artist Sync, Stability
fixes. Updated helper What's New with 5 new entries.
2026-03-27 15:46:25 -07:00
Broque Thomas
326bb548ce Add per-artist Sync button on enhanced library view
New "Sync" button in the enhanced view header validates an artist's
library entries against files on disk. Removes stale tracks (missing
files), cleans empty albums, and updates track counts.

- POST /api/library/artist/<id>/sync endpoint
- Checks each track's file_path via _resolve_library_file_path
- Empty album cleanup checks ALL tracks (not just this artist's)
  to avoid deleting albums shared with other artists
- Toast shows results: stale removed, albums cleaned, or "All files
  verified" if everything checks out
- Auto-refreshes enhanced view when changes are made
2026-03-27 15:36:08 -07:00
Broque Thomas
f94f043dc6 Fix album delete returning HTML instead of JSON for non-integer IDs
The DELETE /api/library/album/<id> route used <int:album_id> which
rejected non-integer IDs (e.g., Navidrome string IDs). Flask returned
its default 404 HTML page, causing "unexpected token <" JSON parse
error on the frontend. Changed to <album_id> to match all other
album routes which already accept any ID type.
2026-03-27 15:00:41 -07:00
Broque Thomas
a928522b45 Handle censored track titles from Apple Music in library matching
Apple Music returns censored titles like "B*****t Faucet" for
"Bullshit Faucet". The string similarity function now detects
asterisk patterns and matches by comparing non-censored words
exactly and censored words by prefix/suffix characters.

Only fires when * is present in one string — zero impact on
normal comparisons. Prevents daily re-downloads of explicit
tracks that exist in the library under their uncensored names.
2026-03-27 14:05:18 -07:00
Broque Thomas
b49806a83a Add collaborative album artist handling with per-source resolution
New setting in Settings → Library → File Organization: "Collaborative
Album Artist" — choose between first listed artist (default) or all
artists combined for $albumartist in folder paths and album_artist tag.

Per-source resolution:
- Spotify: artists array has separate objects — picks first directly
- Deezer: API already returns first artist only — no change needed
- iTunes: combined string ("Larry June, Curren$y & The Alchemist") —
  resolves via artistId API lookup to get primary name ("Larry June").
  Safe for "Tyler, the Creator" and "Simon & Garfunkel" because their
  IDs resolve to the same combined name (no change).

Applied to both folder path ($albumartist template) and album_artist
metadata tag for consistency. Track artist tag always keeps all artists.
iTunes lookup only fires when source is iTunes (numeric ID + not Deezer).
2026-03-27 13:39:27 -07:00
Broque Thomas
72fab16bad Add POST /api/wishlist/process endpoint for external API access
Allows external apps to trigger wishlist processing without needing
to look up automation IDs. Returns 409 if already running. Uses the
same _process_wishlist_automatically function as the automation engine.
2026-03-27 11:58:22 -07:00
Broque Thomas
7f9755a26e Add album-aware track matching for multi-artist albums
When artist-specific track search fails, falls back to album-aware
matching: finds the album by title (any artist), then checks if the
track exists on it. Fixes daily re-downloads of collaborative albums
filed under a different artist (e.g., "Spiral Staircases" tagged
under "The Alchemist" but scanned from "Larry June's" watchlist).

- check_track_exists: new album parameter, album-aware fallback with
  0.8 album title threshold + 0.7 track title threshold
- Watchlist scanner: passes album_data.get('name') to track checks
- Download modal: passes batch_album_context to fallback track search
- Wishlist callers (4 spots): extract and pass track album name
- Backwards compatible: album=None default, no change for callers
  without album context (singles, playlists)
2026-03-27 11:44:01 -07:00
Broque Thomas
4baf5e53d4 Fix track numbering and disc assignment for non-Spotify metadata sources
The download context builder always called spotify_client.get_track_details()
first for track_number/disc_number, which fails or returns wrong data when
using Deezer/iTunes as metadata source. All tracks got track_number: 0,
defaulting to 01, and disc_number: 1.

Fix: check track_info (from frontend album data) first, then the track
object, then API call as last resort. The album tracks response already
has correct track_position and disk_number from Deezer — no API call needed.
2026-03-27 11:33:51 -07:00
Broque Thomas
cab12b61a6 Fix missing year and track numbers when Deezer metadata cache is stale
Root cause: search_albums cached album data without release_date or
track_position. get_album_metadata accepted this incomplete cache hit
(only checked for title), never called the full API. Result: year
empty, all tracks numbered 01.

Fix: get_album_metadata now requires release_date in cache to use it.
Search results without release_date trigger a fresh /album/{id} API
call which returns complete data. Also improved track_number fallback
in download context builder when Spotify isn't configured.
2026-03-27 11:24:36 -07:00
Broque Thomas
a0b2fa9441 Fix false album completion badges, add multi-artist album matching
Completion accuracy:
- Exact match only: "Complete" requires owned_tracks >= expected_tracks,
  no more 90% rounding that hid missing tracks
- Deduplicate track counting: DISTINCT (title, track_number) prevents
  duplicate album entries from inflating owned count (e.g., 3 "GNX"
  entries with 12+1+2 rows counted as 12 unique tracks, not 15)
- MAX(track_count) instead of SUM for stored count — uses largest
  album entry rather than summing duplicates
- file_path IS NOT NULL filter ensures only real files are counted
- Frontend uses real numbers instead of overriding missing=0 when
  backend says "completed"

Multi-artist albums:
- Title-only fallback search when artist-specific search fails
- Finds "Anger Management" filed under "The Alchemist" when checking
  from Rico Nasty's page
- Same confidence scoring prevents false matches
2026-03-27 09:25:08 -07:00
Broque Thomas
a33f891fa6 Add per-artist watchlist lookback period override
New "Scan Lookback" dropdown in the watchlist artist config modal.
Each artist can override the global lookback period (7d to entire
discography). Default is "Use Global Setting" (NULL in DB).

- Database: lookback_days INTEGER DEFAULT NULL on watchlist_artists,
  auto-migrated on startup
- Scanner: checks per-artist lookback_days first, falls back to
  global discovery_lookback_period if NULL
- Backend: GET/POST /api/watchlist/artist/<id>/config includes
  lookback_days. Changing lookback clears last_scan_timestamp to
  force a rescan with the new window
- Frontend: dropdown with 8 options in artist config modal
- Fully backwards compatible — existing artists unchanged
2026-03-27 08:17:02 -07:00
Broque Thomas
db104ec155 Auto-reconnect Hydrabase WebSocket when server restarts
Background monitor checks connection every 30s. If auto_connect is
enabled and socket is dead, reconnects using saved credentials.
Exponential backoff (30s→60s→120s→max 300s) on repeated failures.
Log suppression after 3 consecutive failures to prevent spam.

Fixes: Hydrabase server restart required manual reconnect from UI.
Now recovers automatically within 30s of server becoming available.
2026-03-26 20:21:14 -07:00
Broque Thomas
835ddcdbd5 Fall back to stream source when library file not found on disk
When clicking play on an "In Library" track, if the file can't be
resolved on disk (e.g., media server path not accessible from
SoulSync), silently falls back to streaming via the configured
stream source instead of showing an error.
2026-03-26 19:48:03 -07:00
Broque Thomas
9fcbd323a5 Add stream source setting, auto-update yt-dlp on container start
Stream source:
- New setting in Settings → Downloads: "Stream / Preview Source"
- Options: YouTube (instant, default) or Active Download Source
- YouTube streams require no auth and are instant
- If active source is Soulseek, automatically falls back to YouTube
- Uses direct client search (bypasses orchestrator's download mode)
- Config key: download_source.stream_source

Docker:
- entrypoint.sh now runs pip install -U yt-dlp on every container
  start, so Docker users always have the latest yt-dlp without
  rebuilding the image
2026-03-26 19:27:35 -07:00
Broque Thomas
6a41f5c0d7 Fix YouTube downloads failing with "Requested format not available"
Root cause: two issues compounding.

1. extractor_args with player_client: ['android', 'web'] + skip: ['hls', 'dash']
   stripped all real audio formats. Android client returns HLS/DASH streams,
   skip removes them, leaving only storyboard thumbnails. bestaudio then fails
   because there's nothing valid to select.

2. Browser cookies (cookiesfrombrowser) cause authenticated YouTube sessions
   to return restricted format data for some videos. Same video works fine
   without cookies.

Fix:
- Removed all hardcoded player_client and skip overrides from 4 locations
  (download opts, connection check, search, retry). Let yt-dlp use its own
  defaults which are updated with each release.
- Retry strategy: attempt 1 uses cookies (respects user setting), attempt 2
  drops cookies (fixes auth-restricted formats), attempt 3 uses format 'best'
  as last resort.
- Updated user_agent to Chrome 131.
2026-03-26 19:07:50 -07:00
Broque Thomas
de5e3f6b3f Skip slskd API calls when known disconnected to prevent timeout spam
When slskd is unreachable, the download status emitter was making two
blocking API calls every 2 seconds (0.75s cache TTL), each timing out
after 5-10s. With 9 enrichment workers + WebSocket emitters competing
for threads, this created enough thread pressure to trigger Werkzeug's
"write() before start_response" race condition and crash the process.

Fix: check _status_cache (refreshed every 2 min by /status endpoint)
before calling slskd. If soulseek is known disconnected, skip both
transfers/downloads calls and return empty data. Normal behavior
resumes within 2 minutes of slskd recovering.
2026-03-26 14:49:25 -07:00
Broque Thomas
8e41feaade Fix Navidrome playlist sync truncation, clarify discovery label
Navidrome fix:
- createPlaylist and other write operations now use POST instead of
  GET. Large playlists (161+ tracks) exceeded URL length limits when
  all songId params were in the query string, causing silent truncation
  (e.g., only 6 of 161 tracks added). POST sends params as form body
  with no size limit.
- Write operation timeout bumped to 30s (was 10s)
- _WRITE_ENDPOINTS set defines which Subsonic endpoints use POST

UX fix:
- Mirrored playlist cards now show "161/161 discovered on Spotify"
  instead of just "161/161 discovered" — clarifies that discovery
  means metadata matching, not library ownership
2026-03-26 14:15:20 -07:00
Broque Thomas
a98108b3ed Persist enrichment worker pause state across restarts
Bug: pausing workers via UI only called .pause() in memory — never
saved to config. On process restart (crash, OOM, Docker healthcheck),
all workers came back active, silently discarding user's changes.

Fix:
- All 18 pause/resume endpoints (9 workers × 2) now write to
  config_manager: e.g. config_manager.set('spotify_enrichment_paused', True)
- All 9 worker startup blocks check config and immediately pause
  if the saved state says paused
- Status endpoint already reads the same config keys (line 4217),
  so dashboard display is consistent

Workers: MusicBrainz, AudioDB, Deezer, Spotify, iTunes, Last.fm,
Genius, Tidal, Qobuz
2026-03-26 13:41:51 -07:00
Broque Thomas
f19db4ecce Add launch PIN lock screen with credential-based recovery
Security:
- Toggle in Settings → Advanced: "Require PIN to access SoulSync"
- Full-screen lock overlay on every page load when enabled
- PIN validated server-side against admin profile (bcrypt hash)
- Inline PIN creation if admin has no PIN set, change PIN button if set
- One-time session flag: verify-launch-pin sets it, /profiles/current
  consumes it — every page load re-requires PIN

Recovery:
- "Forgot PIN?" on lock screen switches to credential verification
- User pastes any configured API key/token/secret (Spotify, Tidal,
  Plex, Jellyfin, Navidrome, ListenBrainz, AcoustID, Last.fm, Genius)
- Server checks against all 9 stored values — any match clears PIN
  and disables lock, with toast guiding to Settings to set a new one

Profile switch integration:
- Entering PIN during profile switch also sets launch_pin_verified
  flag, preventing double-PIN prompt on the subsequent page reload

Updated version modal and helper What's New with this feature.
2026-03-26 13:15:36 -07:00
Broque Thomas
59e258a922 Add idle glow pulse to help button for new users
Subtle accent border glow breathes every 3s on the ? button until
the user opens the menu for the first time, then stops permanently
via localStorage. Helps new users notice the help system exists.
2026-03-26 12:32:03 -07:00
Broque Thomas
5305481187 Update version modal and helper What's New with latest features
- Added 6 new sections to /api/version-info: Interactive Help System,
  Rich Artist Profiles, Enhanced Library Manager, In Library Badges,
  FLAC Bit Depth, Enrichment Worker Improvements
- Consolidated helper WHATS_NEW under v2.1 (no version bump) with
  12 entries covering all recent features with Show me navigation
- Removed stale v2.2 key from WHATS_NEW that referenced unbumped version
2026-03-26 12:25:53 -07:00
Broque Thomas
c1287f0ec0 Helper V2 complete + enrichment worker fixes
Helper system phases 2-7:
- Setup Progress: onboarding checklist with progress ring, auto-detection
  via /status, /api/settings, /api/library, /api/watchlist, /api/automations
- Quick Actions: accent pill buttons in popovers (service cards get
  "Open Settings" and "View Docs" actions)
- Keyboard Shortcuts: full-screen overlay with key cap styling, grouped
  by scope (Global, Player, Helper, Forms)
- Search: fuzzy search across 200+ help entries, 11 tours, and shortcuts
  with cross-page navigation via _guessPageFromSelector()
- What's New: version-tagged highlights with "Show me" navigation,
  red badge on ? button for unseen versions, older version cycling
- Troubleshoot: scans dashboard service cards for disconnected/error
  states, shows fix steps with action buttons, "All Clear" when healthy
- Contextual menu: page-aware tour suggestion at top of menu
- Ctrl+K / Cmd+K opens helper search globally
- First-launch welcome tooltip with pulsing ? button
- Redesigned floating button (48px, accent gradient, glass effect)
- Redesigned menu (unified card panel, accent left-stripe on contextual)

Enrichment worker fixes:
- AcoustID: individual recording matches downgraded INFO→DEBUG to reduce
  log noise (14 lines for one track → 1 summary line)
- Name normalization: strip " - Suffix" dash format (Spotify) same as
  "(Suffix)" parens format across all 8 workers. Fixes false mismatch
  on tracks like "Electric Eyes (Studio Brussels Remix)" vs
  "Electric Eyes - Studio Brussels Remix" (was 0.54, now matches)
2026-03-26 12:20:58 -07:00
Broque Thomas
019d485e2a Guided tours V2: 11 tours with 97 steps covering every page
- Rewrote all 6 existing tours to match dashboard quality (was 31 steps total)
- Added 5 new tours: Discover, Stats, Import, Settings, Issues
- Fixed broken selectors in first-download and artists-browse tours
  (referenced elements that only exist after user interaction)
- All tours now work on fresh page load — describe post-interaction UI
  instead of pointing at empty containers
- Sync tour expanded from 5→11 steps covering all 8 source tabs
- Library tour expanded from 2→7 steps with filters, pagination, detail view
- Automations tour expanded from 3→6 steps with builder detail and signals
2026-03-26 10:47:48 -07:00
Broque Thomas
38ee6b6957 Complete helper system: all pages covered (200+ entries)
Helper content for every major page and interactive element:
- Dashboard: service cards, stats, 9 tools, watchlist/wishlist modals
- Sync: 8 source tabs, discovery modals, URL inputs, sidebar
- Search: enhanced/basic modes, filters, download manager, library badges
- Discover: hero, 14 playlists, cache sections, tabs, build tool
- Artists: search, hero profile, discography, similar artists
- Automations: list, cards, hub tabs, builder slots, history
- Library: grid, detail hero, filters, enhanced view, tag modals
- Stats: time range, charts, rankings, library health, storage
- Import: staging, album/singles tabs, matching, processing
- Settings: 5 tabs, all source configs, quality profile, file org,
  post-processing, appearance, advanced, API keys

Docs updated with 4 new sync subsections (Spotify Public, Deezer,
Import from File, Sync History).
Total: 200+ contextual help entries with documentation links.
2026-03-26 09:50:40 -07:00
Broque Thomas
1e9abb588c Add clickable artist name link in download modal hero subtitle
The artist name in album download modals is now a clickable link
that navigates to the Artists page with that artist's discography.
Uses the correct source-specific artist ID from the album data.
Works on enhanced search, artists page, and discover page modals.
Excluded from playlist, wishlist, and default contexts where the
subtitle isn't an artist name.
2026-03-26 08:22:52 -07:00
Broque Thomas
77b6d4927c Add View Discography button to watchlist artist detail overlay
Clicking the button closes the watchlist modal and navigates to the
Artists page with the artist's discography loaded. Uses the correct
source ID based on the active metadata source (Spotify/Deezer/iTunes).
2026-03-26 08:01:31 -07:00
Broque Thomas
f6709c7cc3 Add library ownership badges to enhanced search results
Search results now show "In Library" badges on albums and tracks
that already exist in the user's library. Badges appear with a
staggered fade-in animation after results render (non-blocking).

- Backend: /api/enhanced-search/library-check endpoint builds
  owned album/track sets in 2 queries, O(1) lookups per result
- Frontend: async call after render, 30ms stagger per badge
- Tracks in library get play button rewired for direct playback
  from media server instead of searching download sources
- Fixed enhanced search album card text not visible (info div
  now absolute-positioned with gradient overlay)
- Download manager panel hidden by default for more search space
2026-03-25 19:39:19 -07:00
Broque Thomas
6c3b9ddfc2 Optimize watchlist card CSS, backfill missing album covers
Watchlist cards: removed spring-bounce transitions, staggered
animations, and multi-layer hover shadows. Added CSS containment
and will-change for smoother scrolling.

Recent releases: backfill missing album cover art on page load
via metadata source lookup. Persists found covers to database.
2026-03-25 18:36:45 -07:00
Broque Thomas
f725b66afe Extend helper system: wishlist tracks, download modal, track rows
Adds contextual help coverage for:
- Wishlist track list view (album cards, individual tracks, batch bar)
- Download modal (hero stats, track analysis table, all action buttons)
- Track rows (checkbox, library match, download status, actions)
- Force Download and Playlist Folder toggles
- Add to Wishlist button
- Dynamic ID selectors for playlist-scoped elements
2026-03-25 18:19:52 -07:00
Broque Thomas
46308d8d31 Fix watchlist artist image not saving for Deezer source
Two bugs preventing Deezer artist images in the watchlist:

1. web_server.py: The image fetch called get_artist() which doesn't
   exist on DeezerClient. Replaced with direct Deezer API call for
   picture_xl — simple and reliable.

2. music_database.py: update_watchlist_artist_image() only matched
   spotify_artist_id and itunes_artist_id in the WHERE clause.
   Deezer artists use deezer_artist_id, so the UPDATE matched zero
   rows and the image was never saved. Added deezer_artist_id to
   the WHERE clause.
2026-03-25 17:49:33 -07:00
Broque Thomas
de8f758596 Fix watchlist artist image not loading for Deezer source
The watchlist add code called fallback.get_artist() which doesn't
exist on the Deezer client (only get_artist_info). The AttributeError
was silently caught, leaving image_url as None. Now uses
get_artist_info() and falls back to a direct Deezer API call for
picture_xl when the Spotify-compatible format doesn't have images.
2026-03-25 17:37:59 -07:00
Broque Thomas
38ba6ddbc1 Add interactive contextual help system with floating helper button
New feature: click the floating ? button (bottom-right corner) to
enter help mode. Click any UI element to see a popover explaining
what it is and how to use it. Covers Dashboard + Sidebar + Watchlist.

- Floating button always visible above modals (z-index 999999)
- Click interception via capture phase prevents accidental actions
- Popover with smart positioning (right/left/below fallback)
- Arrow pointing to target element with accent highlight pulse
- "View full documentation" links navigate to the correct docs section
- Escape key dismisses popover or exits help mode
- Works inside modals (watchlist, artist config, global settings)
- 45+ contextual help entries covering sidebar nav, service cards,
  stat cards, all 9 tool cards, watchlist modal buttons, artist
  config options, content filters, and activity feed
- Separate helper.js file for maintainability
2026-03-25 17:34:43 -07:00
Broque Thomas
6820e2d4e3 Enforce FLAC bit depth preference and prioritize audio quality in sorting
Two changes to the Soulseek quality filter:

1. Sort candidates by effective bitrate first, peer quality second.
Previously a 16-bit FLAC from a fast peer could beat a 24-bit FLAC
from a slower peer. Now highest audio quality always wins within the
same format tier, with peer speed as tiebreaker.

2. Enforce the FLAC bit depth UI preference (Any/16-bit/24-bit) that
was previously cosmetic-only. Uses effective bitrate threshold of
1450 kbps to distinguish 16-bit (<1411 kbps theoretical) from 24-bit
(>2116 kbps theoretical). Respects the bit_depth_fallback toggle —
when enabled, gracefully accepts any FLAC if preferred depth is
unavailable. When disabled, strictly rejects non-matching depth.

Default bit_depth="any" — zero behavior change for existing users.
2026-03-25 15:59:22 -07:00
Broque Thomas
cb008a2e61 Cache Deezer enrichment worker API calls in metadata cache
The Deezer enrichment worker's 5 raw API methods (search_artist,
search_album, search_track, get_album_raw, get_track_raw) were
bypassing the metadata cache — every enrichment cycle hit the
Deezer API fresh for every item. Spotify and iTunes workers properly
cached all results.

Now all 5 methods store results via store_entity(). get_album_raw
and get_track_raw also check cache first (verified by presence of
full-detail fields like 'label' and 'bpm' to distinguish from
search stubs). Cache failures are silently ignored to never block
enrichment. This eliminates redundant API calls and automatically
populates genre data for the genre explorer.
2026-03-25 15:13:55 -07:00
Broque Thomas
d248c36da1 Redesign Artists page: rich hero section, full-bleed cards, multi-source genres
Artists page hero section:
- Large portrait artist photo (400x480px, rounded rectangle)
- Blurred saturated background from artist image
- 2.6em bold name with text shadow
- Real service logo badges (Spotify, MusicBrainz, Deezer, iTunes,
  Last.fm, Genius, Tidal, Qobuz) — matching library page
- Genre pills merged from metadata cache + Last.fm tags
- Last.fm bio with read more/show less toggle
- Last.fm listener count + playcount stats (large bold numbers)
- Backend enriches discography response with artist_info from
  metadata cache + library (all service IDs, Last.fm data, genres)

Album/Single/EP cards:
- Full-bleed cover art filling entire card with gradient overlay
- Album name + year overlaid at bottom over dark gradient
- Image zoom on hover, accent glow for dynamic-glow cards
- Responsive grid (220px desktop, 170px tablet, 140px mobile)

Similar artist cards:
- Full-bleed image cards matching library artist card style
- Gradient overlay with name at bottom, aspect-ratio 0.8
- Grid-controlled sizing via existing responsive breakpoints

Genre explorer (multi-source):
- Queries all allowed sources (iTunes+Deezer always, Spotify when
  authed) via _get_genre_allowed_sources() helper
- Deezer genre support: genre_id mapping from search results,
  one-time backfill from stored raw_json, album-to-artist propagation
- Genre deep dive deduplicates artists across sources
- Source dots on artists/tracks in deep dive modal
- Artist clicks route through source-specific client
- Album endpoint falls back across sources when IDs don't match
- Genre explorer cached 24hr in-memory, positioned at top of
  Discover page below hero slider

All changes mobile responsive with proper breakpoints.
2026-03-25 11:49:51 -07:00
Broque Thomas
e08462a002 Multi-source genre explorer with Deezer genre support and cross-source routing
Genre explorer and deep dive modal now combine data from all available
metadata sources (iTunes + Deezer always, Spotify when authenticated).
Artists are deduplicated by name across sources, preferring entries
with images. Source dots (green/red/purple) indicate data origin.

Deezer genre support:
- Extract genre_id from Deezer album search responses via ID-to-name
  mapping table (26 Deezer genre categories)
- Extract full genre names from Deezer get_album responses
- One-time backfill updates existing cached albums from stored raw_json
- Propagate album genres to Deezer artist entities

Cross-source album routing:
- /api/discover/album endpoint uses source-specific client (iTunes or
  Deezer) based on the item's source, not just the active fallback
- Spotify path falls back to active fallback when album not found
- Track clicks use album_id directly instead of name-based resolution
- resolve-cache-album adds partial match and live search fallback

Other fixes:
- Genre explorer positioned at top of Discover page (below hero)
- Genre explorer results cached 24hr in-memory for fast reload
- Related genres computed from all albums by matched artists
- Artist clicks open Artists page with discography (not library detail)
- Discovery pool genre queries restored to source-filtered (Browse by
  Genre tabs stay source-isolated as designed)
2026-03-24 19:36:11 -07:00
Broque Thomas
a3309d28a6 Update pages.gif 2026-03-24 15:06:01 -07:00
Broque Thomas
f3bb8d2f0f Fix genre browser returning empty when metadata source changes
The genre query filtered by active source (spotify/deezer/itunes)
but discovery pool entries keep their original source. Switching
metadata sources caused all genres to disappear. Removed the source
filter since artist genres are source-agnostic metadata.
2026-03-24 14:50:37 -07:00
Broque Thomas
bb9564ee88 Downgrade 'Soulseek client not configured' from ERROR to DEBUG (#201)
Users who intentionally don't use Soulseek were getting spammed with
ERROR-level logs every second. These are expected when Soulseek isn't
configured as a source and don't need user attention.
2026-03-24 14:03:06 -07:00
Broque Thomas
616b377225 Fix Deezer download crash: config_manager not in scope
Deezer client imports config_manager locally in __init__ and stores
it as self._config, unlike the other clients which import at module
level. The allow_fallback line was referencing the wrong name.
2026-03-24 13:12:23 -07:00
Broque Thomas
6101832ee1 Add search filter and rematch to discovery pool modal
Discovery pool lists (matched and failed) now have a search input
that filters tracks client-side by name, artist, or playlist.

Matched tracks get a "Rematch" button that opens the fix modal in
cache-only mode — deletes the old cache entry and saves the new
match directly to the discovery cache via /api/discovery-pool/rematch.
This works regardless of whether a mirrored playlist track exists.

Failed tracks retain the existing "Fix Match" flow unchanged.
2026-03-24 13:07:04 -07:00
Broque Thomas
54b02b86c1 Update help docs: add HiFi/Deezer sources, quality fallback, template vars
- Overview and setup sections now list all 6 download sources
- Services table adds HiFi (no auth) and Deezer (ARL token)
- Enhanced Search documents multi-source tabs (Spotify/iTunes/Deezer)
- Download Sources table adds HiFi and Deezer rows
- Post-processing explains AcoustID skip for streaming sources and
  the new artist/title verification for streaming candidates
- File Organization documents $albumtype, $disc, and all template vars
- Quality Profiles adds callout for per-source fallback toggle
- Settings credentials list adds HiFi and Deezer entries
2026-03-24 12:30:26 -07:00
Broque Thomas
ee9f3b7386 Fix $albumtype template not working for singles in album context search
The album-aware search (_detect_album_info_album_context) was forcing
is_album=True for every track found in any release, including singles.
This routed singles through the album_path template instead of
single_path, ignoring the user's $albumtype folder structure.

Now applies the same classification logic as _detect_album_info_web:
checks album_type, total_tracks, and name comparisons to correctly
distinguish albums from singles/EPs. Works for all metadata sources
(Spotify, iTunes, Deezer) since all album objects have album_type.
2026-03-24 12:02:48 -07:00
Broque Thomas
89cfea0fe7 Add per-source quality fallback toggle for streaming downloads (#187)
Each streaming source (Tidal, Qobuz, HiFi, Deezer) now has an "Allow
quality fallback" checkbox in Settings. When disabled, the source only
tries the exact quality selected — if unavailable, it skips and lets
the orchestrator try the next source. Default is ON (current behavior).
2026-03-24 11:42:47 -07:00
Broque Thomas
e28aeccfd1 Redesign pool fix match modal: fixed height, no layout shift (#186)
Modal now uses a fixed 600px height from open — results scroll within
a dedicated area instead of growing the modal and pushing inputs up.
This eliminates the layout shift that caused accidental result clicks.

Other fixes:
- Input fields now have labels (Track, Artist)
- Overlay dismiss uses mousedown with stopPropagation to prevent
  accidental close when clicking near inputs
- Reduced results from 50 to 20 for faster response
- Clean minimal design matching app style
- Mobile: full-screen modal, stacked inputs with 44px touch targets
2026-03-24 11:16:37 -07:00
Broque Thomas
434ac417fd Add artist/title verification for streaming source download candidates
Streaming sources (Tidal, Qobuz, Deezer, HiFi, YouTube) were blindly
trusted with confidence 1.0 — all search results passed through without
any matching. When searching common track titles like "Die for You",
the most popular version (The Weeknd) could be downloaded instead of
the intended artist (Grabbitz/Valorant).

Now verifies each streaming result's artist and title against the
expected track before accepting it. Artist must substring-match or
have >=0.6 similarity; title needs >=0.5 similarity. Results sorted
by title confidence. If nothing passes, falls through to the standard
matching engine instead of silently downloading the wrong track.
2026-03-24 11:03:43 -07:00
Broque Thomas
7b615a9534 Redesign enhanced search results: modern flat layout with responsive cards
- Search bar: stripped heavy purple chrome, minimal dark input style
- Dropdown: inline flow instead of overlay, hides page header when active
- Section labels: flat uppercase text, no bordered glass boxes
- Artist cards: full-bleed photo with gradient overlay and name at bottom
  (matches library page style), flexbox wrap layout with fixed dimensions
- Album cards: discover-style dark cards in horizontal scroll on desktop,
  wrap to 2-per-row on mobile
- Track rows: clean flat list, subtle hover, smaller cover art
- Source tabs: compact pills with per-source accent colors
- Renamed grid classes (enh-artists-grid, enh-albums-grid, enh-tracks-list)
  to avoid collision with generic .artists-grid rule
- Mobile: downloads-main-panel min-width:0 fix for 1190px overflow,
  cards use calc(50% - 8px) for 2-per-row fill, touch-friendly targets
2026-03-24 10:45:14 -07:00
Broque Thomas
16be814a67 Fix album completeness fix returning 400 for stale findings
When missing_tracks was empty (API unavailable at scan time) or the
album was completed between scan and fix, the handler returned a 400
error. Now it re-fetches missing tracks from Spotify/iTunes/Deezer at
fix time and auto-resolves findings where the album is already complete.
2026-03-24 09:32:17 -07:00