Commit graph

728 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
01b0b70515 Fix enhanced view reorganize not moving sidecars, add template debug logging
Enhanced album reorganize now moves LRC, cover.jpg, folder.jpg and other
sidecar files alongside audio files. Added debug log to library reorganize
repair job showing which template is loaded from config vs default.
2026-03-24 07:12:35 -07:00
Broque Thomas
21837be84e Fix 403 on followed Spotify playlists: fallback to public embed scraper
When fetching tracks from a followed playlist returns 403 (owner made it
private or API scope insufficient), fall back to the public embed scraper
which doesn't need API auth. Tracks still load with basic metadata.
2026-03-23 19:57:55 -07:00
Broque Thomas
571ba6b532 Version 2.1: Deezer downloads, cache discovery, stats, glass UI, album consistency
Major version bump with 40+ commits of features and fixes:
- Deezer as 6th download source (ARL auth, FLAC/MP3, Blowfish decrypt)
- Cache-powered discovery: 5 sections + Genre Deep Dive modal
- Listening Stats page with charts and play buttons
- Picard-style MB release preflight for consistent album tagging
- Album Tag Consistency repair job
- Unified glass UI across dashboard, sync, and modals
- Mobile responsive overhaul for all pages
- Enrichment fixes: retry loops, rate limits, worker pause during scans
- AcoustID skip for trusted API sources
2026-03-23 16:45:23 -07:00
Broque Thomas
deadfa1e82 Skip AcoustID verification for trusted API download sources
Tidal, Qobuz, Deezer, and HiFi download by exact track ID from official
APIs — files are guaranteed correct. AcoustID fingerprinting only runs
for Soulseek (P2P) and YouTube (extracted audio) where mislabeling is
possible. Users with AcoustID disabled see no change.
2026-03-23 16:08:27 -07:00
Broque Thomas
7eed88c72c Fix Deezer download integration: add deezer_dl to all streaming source checks
- Added deezer_dl to 7 hardcoded streaming source username tuples in web_server
- Streaming sources now bypass Soulseek filename-matching engine in get_valid_candidates
  (API search results are already properly matched, no need for P2P filename scoring)
- Fixes download progress tracking, completion detection, and candidate filtering for Deezer
2026-03-23 15:57:53 -07:00
Broque Thomas
2ae5050ef1 Add Deezer download source: client, settings UI, ARL authentication
- New core/deezer_download_client.py: full download client with ARL auth,
  Blowfish decryption, quality fallback (FLAC/MP3 320/MP3 128), search,
  thread-safe download tracking. Not yet integrated into orchestrator.
- Settings UI: Deezer download quality selector, ARL token input, test
  connection button. Appears in download source dropdown and hybrid list.
- Test endpoint: /api/deezer-download/test verifies ARL and returns tier.
- Added deezer_download to settings save whitelist and sensitive paths.
- Fixed Spotify enrichment worker default to unpaused (like other workers).
2026-03-23 15:06:40 -07:00
Broque Thomas
5d6ccee066 Add Picard-style MB release preflight for album downloads
Before album tracks start post-processing, pre-populate the MusicBrainz
release cache with ONE verified release. All tracks then hit the cache
during per-track processing and get the same release MBID — no second pass,
no file lock contention, tags correct on first write.

Handles edition name mismatches (Spotify says 'Super Deluxe', MB says
just the base name) by stripping qualifiers and searching again.
Validates track count to pick the right edition. Three download paths
covered: download missing modal, enhanced album, and matched album.
2026-03-23 12:42:29 -07:00
Broque Thomas
68f06d663b Pause enrichment workers during database scans to reduce lock contention
All 11 background workers (9 enrichment + repair + SoulID) pause when a DB
scan starts and resume when it finishes. Tracks which workers were running
so manually-paused workers stay paused after scan. Covers incremental,
full refresh, deep scan, automation-triggered, and startup scans.
2026-03-23 10:33:10 -07:00
Broque Thomas
efe8164501 Fix missing album art on wishlist items from mirrored playlists
Embed scraper was pre-marking spotify_public tracks as discovered with
playlist-level images instead of per-track album art. Discover step then
skipped them entirely. Now stores spotify_hint (track ID) without marking
discovered, so Discover runs proper API lookups for real album art.
2026-03-23 09:22:49 -07:00
Broque Thomas
df390feece Fix automation signal chain: forward event data (playlist_id) to action handlers
Event-triggered automations now receive playlist_id from the triggering event
when the action config doesn't have one set. Fixes silent 'No playlist specified'
failures in Discover/Sync chains. Added debug logging to trace event matching.
2026-03-23 08:51:52 -07:00
Broque Thomas
17e06aaae5 Update What's New modal with cache discovery, genre deep dive, DB storage, performance, mobile, and album split fix 2026-03-22 21:38:24 -07:00
Broque Thomas
e3d70da55a Add DB storage visualization + cache-powered discovery sections + Genre Deep Dive
- Stats page: database storage donut chart with per-table breakdown and total size
- Discover page: 5 new sections mined from metadata cache (zero API calls):
  Undiscovered Albums, New In Your Genres, From Your Labels, Deep Cuts, Genre Explorer
- Genre Deep Dive modal: artists (clickable → artist page), popular tracks,
  albums with download flow, related genre pills, in-library badges
- All cache queries filtered by active metadata source (Spotify/iTunes/Deezer)
- Stale cache entries (404) gracefully fall back to name+artist resolution
- Album cards show "In Library" badge, artist avatars scaled by prominence
2026-03-22 21:35:18 -07:00
Broque Thomas
40a9858f28 Fix album split in Navidrome: normalize MusicBrainz release cache keys across edition variants 2026-03-22 16:51:03 -07:00
Broque Thomas
93005298ee Cap Opus bitrate at 256kbps, fix lossy copy flavor text, redesign artist action buttons 2026-03-22 16:34:36 -07:00
Broque Thomas
8c84189121 Add per-artist enrichment coverage rings to artist hero section 2026-03-22 15:56:12 -07:00
Broque Thomas
b59a0eaf95 Add play buttons to stats page with cover art support 2026-03-22 15:24:38 -07:00
Broque Thomas
9e75731f6c Add scrobbling to Last.fm/ListenBrainz + update What's New
Scrobbling:
- Last.fm: API signing, web auth flow, batch scrobble (50/request)
- ListenBrainz: submit_listens (batch 1000, listen_type=import)
- Worker hook: scrobbles unscrobbled events after each poll
- DB: scrobbled_lastfm/scrobbled_listenbrainz tracking columns
- Settings: API secret input, authorize button, scrobble toggles
- Config: lastfm.api_secret, lastfm.session_key, *.scrobble_enabled

What's New: added all features from this session (scrobbling,
personalized discovery, stats page, SoulID, lossy codecs, import,
hero redesign, Hydrabase, orphan fixes, year collection).
2026-03-22 14:38:53 -07:00
Broque Thomas
232481fd13 Personalize discovery playlists using listening stats
Integrates play history data into the discovery algorithm:

- Listening profile: _get_listening_profile() builds user's top artists,
  genres, play counts, and listening velocity from the last 30 days
- Artist genre cache: pre-built from local DB for O(1) genre lookups
- Release Radar: +10 genre affinity, +15 artist familiarity, -10 overplay
  penalty. Weights rebalanced to 45% recency + 25% popularity + bonuses
- Discovery Weekly: serendipity scoring within tiers — boosts unheard
  artists in preferred genres, penalizes overplayed artists
- Recent Albums: adaptive time window (21-60 days) based on listening
  velocity — heavy listeners get fresher content, casual listeners more
- New "Because You Listen To" sections: personalized carousels based on
  user's top 3 played artists via similar artists + genre fallback
- New endpoint: /api/discover/because-you-listen-to with artist images
- Frontend: BYLT sections with artist photo headers on discover page
- All changes gracefully fall back when no listening data exists
2026-03-22 13:54:37 -07:00
Broque Thomas
cfb0e85564 Add Listening Stats page with media server play data integration
Full stats dashboard that polls Plex/Jellyfin/Navidrome for play
history and presents it with Chart.js visualizations:

Backend:
- ListeningStatsWorker polls active server every 30 min
- listening_history DB table with dedup, play_count/last_played on tracks
- get_play_history() and get_track_play_counts() for all 3 servers
- Pre-computed cache for all time ranges (7d/30d/12m/all) rebuilt each sync
- Single cached endpoint serves all stats data instantly
- Stats query methods: top artists/albums/tracks, timeline, genres, health

Frontend:
- New Stats nav page with glassmorphic container matching dashboard style
- Overview cards (plays, time, artists, albums, tracks) with accent hover
- Listening timeline bar chart (Chart.js)
- Genre breakdown doughnut chart with legend
- Top artists visual bubbles with profile pictures + ranked list
- Top albums and tracks ranked lists with album art
- Library health: format breakdown bar, unplayed count, enrichment coverage
- Recently played timeline with relative timestamps
- Time range pills with instant switching via cache
- Sync Now button with spinner, last synced timestamp
- Clickable artist names navigate to library artist detail
- Last.fm global listeners shown alongside personal play counts
- SoulID badges on matched artists
- Empty state when no data synced yet
- Mobile responsive layout

DB migrations: listening_history table, play_count/last_played columns,
all with idempotent CREATE IF NOT EXISTS / PRAGMA checks.
2026-03-22 13:18:14 -07:00
Broque Thomas
aca8a8e996 Fix Opus cover art and quality: map audio only, embed art via Mutagen
Opus ffmpeg command now uses -map 0:a to extract only the audio stream,
matching the user's working command and preventing picture stream
interference with the encoder. After conversion, cover art is embedded
from the source FLAC using Mutagen: Opus gets METADATA_BLOCK_PICTURE
(base64-encoded per OGG spec), AAC gets MP4Cover. Applied to both
the post-download lossy copy and the repair job fix handler.
2026-03-22 08:32:04 -07:00
Broque Thomas
491b89a1d2 Redesign library artist hero with Last.fm integration
- Add get_artist_top_tracks to Last.fm client (up to 100 tracks)
- Include lastfm_listeners, lastfm_playcount, lastfm_tags, lastfm_bio,
  and soul_id in artist detail API response
- New endpoint: /api/artist/<id>/lastfm-top-tracks for lazy loading
- Hero layout: image (160px) | center (name, badges, genres, bio,
  listener/play stats, progress bars) | right card (scrollable top
  100 tracks from Last.fm)
- Badges 36px with hover lift, bio in subtle card with Read More
  toggle, Last.fm tags merged with existing genres
- Numbers formatted: 1234567 → 1.2M
- Graceful degradation: sections hidden when Last.fm data unavailable
2026-03-21 23:04:34 -07:00
Broque Thomas
f9fc95c9f5 Add Opus and AAC codec options to lossy copy (Blasphemy Mode)
Lossy copy now supports MP3, Opus, and AAC (M4A) codecs with a
configurable dropdown in settings. Each codec uses the appropriate
ffmpeg encoder (libmp3lame/libopus/aac) and Mutagen tag writer
(ID3/Vorbis/MP4). Quality tag, filename substitution, and Blasphemy
Mode file cleanup all work per-codec. Backward compatible — existing
configs default to MP3.
2026-03-21 20:19:36 -07:00
Broque Thomas
a3592d2a14 Fix playlist sync crash: 'Working outside of application context'
get_current_profile_id() uses Flask's g object which doesn't exist
in background threads. Now profile_id is captured at request time
and passed as a parameter to _run_sync_task. All 8 call sites
updated. Automation path defaults to profile_id=1.
2026-03-21 16:09:24 -07:00
Broque Thomas
4dba3757be Fix orphan detector false positives and add staging/delete choice
Orphan detector: add normalized tag matching that strips parentheticals
and brackets (feat. X, [FLAC 16bit], etc.) and tries first-artist-only
for comma-separated artists. Prevents false orphan flags for tracks
like "The Mountain (feat. Dennis Hopper...)" that exist in DB as
"The Mountain". All lookups remain O(1) set operations.

Orphan fix: replace auto-delete with user choice prompt. Single Fix
and Fix All both show modal asking "Move to Staging" or "Delete".
Move to Staging relocates file to import staging folder for proper
re-import with metadata matching. Fix action flows through API
endpoint → repair_worker.fix_finding → _fix_orphan_file handler.
Staging path uses docker_resolve_path for container compatibility.
2026-03-21 11:27:51 -07:00
Broque Thomas
2d511d0a16 Add SoulID worker with API-based debut year disambiguation
SoulID worker generates deterministic soul IDs for all library entities:
  - Artists: hash(name + debut_year) — searches iTunes + Deezer APIs,
    verifies correct artist by matching discography against local DB
    albums via MusicMatchingEngine, pools years from both sources and
    picks the earliest. Falls back to hash(name) if no match found.
  - Albums: hash(artist + album)
  - Tracks: song ID hash(artist + track) + album ID hash(artist + album + track)

Dashboard button with trans2.png logo, rainbow spinner, hover tooltip.
Worker orb with rainbow effect. SoulSync badge on library artist cards.
DB migration adds soul_id columns with indexes to artists/albums/tracks.
Migration version flag auto-resets artist soul IDs when algorithm changes.
2026-03-21 10:21:06 -07:00
Broque Thomas
a7bef972e0 Smarter staging import: tag-first matching and auto-grouping
1. Fix filename parser pattern order — "01 - Title" now matched
   before "Artist - Title", preventing track numbers being treated
   as artist names (e.g., "08" no longer becomes the artist)

2. Tag priority over filename parsing — shared _read_staging_file_metadata()
   helper reads title, artist, albumartist, album, track_number, disc_number
   from Mutagen tags. Only falls back to filename parsing when BOTH title
   AND artist tags are empty. Applied to all 3 staging scan sites.

3. Improved match scoring — rebalanced from title(0.5)+tracknum(0.5) to
   title(0.45)+artist(0.15)+tracknum(0.30)+album_bonus(0.10). Files
   whose album tag matches the selected album get boosted.

4. Auto-group detection — new /api/import/staging/groups endpoint groups
   staging files by album+artist tags. Frontend shows "Auto-Detected
   Albums" section with one-click search. Match endpoint accepts
   optional file_paths filter to scope matching to a specific group.
2026-03-20 22:34:20 -07:00
Broque Thomas
ee3500242e Fix Hydrabase search types, ID routing, and plugin passthrough
- Use correct server request types: 'tracks', 'albums', 'artists',
  'artist.albums', 'album.tracks' (were singular, caused timeouts)
- Normalize artists to strings (server may send dicts)
- Use native plugin IDs (iTunes/Spotify) instead of soul_id for
  album/artist/track IDs so downstream endpoints can resolve them
- Carry soul_id and plugin_id in external_urls for routing
- Pass plugin param from frontend to server for correct client routing
  (iTunes vs Deezer vs Spotify) with isdigit() fallback
- Route source=hydrabase to iTunes client for artist images
- Include external_urls in enhanced search API response
- Reduce WebSocket timeout from 15s to 8s
2026-03-20 21:04:56 -07:00
Broque Thomas
a4f0745547 Fix Hydrabase not appearing as enhanced search source tab
- Remove stale hydrabase.enabled check, use is_connected() directly
- Add hydrabase to frontend alternate source fetch list
- Normalize Hydrabase artists to strings (server may send dicts),
  fixing silent crashes that prevented albums/tracks from appearing
2026-03-20 18:24:20 -07:00
Broque Thomas
c69040886e Collect release year from all metadata sources during post-processing
Post-processing now extracts release year from MusicBrainz, Deezer,
Tidal, Qobuz, and Spotify context (first source wins). Writes
ORIGINALDATE and DATE tags to file, and backfills the album year
in the DB if currently missing. Fixes Library Reorganize showing
blank years for Tidal-only downloads.

Also raises Library Reorganize API year lookup cap from 50 to 200.
2026-03-20 17:31:00 -07:00
Broque Thomas
ce89154952 Fix hybrid source toggle/reorder not saving and skip unconfigured sources
- Add debouncedAutoSaveSettings() to moveHybridSource and toggleHybridSource
- Skip unconfigured sources at search time with is_configured() check
- Add get_source_status() to orchestrator, include in settings API response
- Auto-disable unconfigured sources in UI on settings load
2026-03-20 16:22:48 -07:00
Broque Thomas
485a2d2792 Add fallback queries to album pre-flight Soulseek search
Album pre-flight now tries 3 query variations instead of one:
full artist+album, cleaned artist+album (no feat./parentheticals),
and album-only. Stops at first success. Fixes cases where a banned
keyword in the artist name caused zero results, falling back to
slower track-by-track search unnecessarily.
2026-03-20 16:01:50 -07:00
Broque Thomas
d7b9b3ba26 Refactor post-processing metadata lookups + fix Hydrabase as fallback source
Post-processing: Extract 7 inline source lookup blocks into standalone
_pp_lookup_* functions called in configurable order via
metadata_enhancement.post_process_order config. Default order matches
original hardcoded sequence — zero behavioral change.

Hydrabase: Fix connected Hydrabase incorrectly becoming primary source.
_is_hydrabase_active() now only returns True in dev_mode (legacy).
Auto-connect no longer forces dev_mode. Hydrabase as fallback works
through normal _get_metadata_fallback_client path like iTunes/Deezer.
Settings button min-width fix.
2026-03-20 15:48:48 -07:00
Broque Thomas
10361bb837 Complete Hydrabase as selectable fallback metadata source
- Remove redundant enable checkbox — fallback dropdown is the enable
- Hydrabase option only appears in dropdown when connected
- Connect/disconnect dynamically adds/removes dropdown option
- _is_hydrabase_active checks fallback_source == hydrabase (not config toggle)
- Fallback client returns hydrabase_client when selected, iTunes if disconnected
- Auto-reconnect respects fallback selection for dev_mode handling
- hydrabase added to settings save service list for persistence
- Status shows green Connected on page load when auto-connected
2026-03-20 14:18:10 -07:00
Broque Thomas
214985482d Add Hydrabase as a searchable tab in multi-source enhanced search
Hydrabase appears as a switchable source tab alongside Spotify/iTunes/
Deezer when connected and enabled. Added to alternate_sources list and
enhanced-search/source endpoint. Tab only shows when Hydrabase is
available — zero change when disconnected.
2026-03-20 13:51:21 -07:00
Broque Thomas
2f9491c71b Expose Hydrabase as a configurable metadata source (no dev mode needed)
Add Hydrabase section to Settings → Connections with enable toggle,
WebSocket URL, API key, auto-connect, and connect/disconnect button.
_is_hydrabase_active() now checks hydrabase.enabled config in addition
to dev_mode — either path activates it. Default disabled, zero change
for existing users. Dev admin page stays behind dev mode password.
2026-03-20 13:41:02 -07:00
Broque Thomas
0edf6197ec Update What's New modal with latest features and fixes
Add 7 new sections: Multi-Source Search Tabs, Per-Profile Service
Credentials, Modern Settings Redesign, Hybrid N-Source Priority,
Automation Hub Pipelines, Staging Folder Check, Library Safety Fixes.
2026-03-20 12:33:33 -07:00
Broque Thomas
53477768cb Complete per-profile service credentials feature
Adds Tidal per-profile OAuth with token storage on profile row.
Auth initiation stores profile_id in PKCE state, callback detects
it and stores encrypted tokens per-profile instead of globally.

Personal settings modal now shows Spotify, Tidal, Server Library,
and ListenBrainz sections for non-admin profiles. Admin sees only
ListenBrainz (unchanged). Server library selection wired into
playlist sync via _apply_profile_library.

Full per-profile support: Spotify (credentials + OAuth + playlists),
Tidal (OAuth + token storage), server library (Plex/Jellyfin/Navidrome),
ListenBrainz (existing). All backwards compatible — upgrading users
see zero change.
2026-03-20 12:08:20 -07:00
Broque Thomas
be27c36da2 Wire per-profile server library into playlist sync
PlaylistSyncService now accepts profile_id and applies per-profile
library selection before syncing. _apply_profile_library sets Plex
library name or Jellyfin user/library on the client based on the
profile's saved preferences. All existing _get_active_media_client
calls automatically pick up the active profile via instance state.

Admin and profiles without custom library settings: zero change.
2026-03-20 12:00:11 -07:00
Broque Thomas
e7fe083099 Add per-profile Spotify credentials and server library selection
Complete end-to-end per-profile Spotify support:
- DB migration: 11 columns on profiles table (Spotify creds, Tidal
  tokens, server library IDs) with encryption
- API endpoints: save/load/delete Spotify creds + server library
- Per-profile Spotify client cache with separate OAuth token storage
- Profile-aware OAuth flow (auth initiation + callback via state param)
- Playlist listing endpoint uses per-profile client
- Frontend: Spotify + Server Library + ListenBrainz sections in
  personal settings modal (non-admin only)
- Admin users see zero change — fully backwards compatible
- Tidal per-profile deferred (needs device auth flow)
2026-03-20 11:49:30 -07:00
Broque Thomas
06caf43f2f Add per-profile Spotify client with cache and playlist endpoint wiring
get_spotify_client_for_profile() returns the right SpotifyClient for
the active profile. Admin and profiles without custom creds get the
global client (zero behavioral change). Profiles with custom creds
get a dedicated cached instance with separate OAuth token storage.

Playlist listing endpoint now uses per-profile client so non-admin
users with their own Spotify credentials see their own playlists.
2026-03-20 10:47:58 -07:00
Broque Thomas
efe4561bcb Add per-profile service credentials foundation (DB + API)
DB migration adds 11 columns to profiles table for per-profile Spotify
credentials, Tidal tokens, and media server library selection. All
encrypted, all default NULL (fall back to global config).

API endpoints follow existing ListenBrainz per-profile pattern:
- GET/POST/DELETE /api/profiles/me/spotify
- GET/POST /api/profiles/me/server-library

Foundation only — no frontend UI or client initialization changes yet.
2026-03-20 10:30:56 -07:00
Broque Thomas
e0533215da Fix enrichment workers looping on tracks with NULL IDs
Workers would endlessly match the same track because UPDATE WHERE id =
NULL matches 0 rows in SQL. Added AND id IS NOT NULL to all enrichment
queries (individual, batch EXISTS, and batch fetch) across all 9
workers. Also added process-level guard for belt-and-suspenders safety.

Fix Deezer get_track → get_track_details method name mismatch.
2026-03-20 09:36:25 -07:00
Broque Thomas
81852caa59 Fix wishlist download UI showing status on wrong track rows
Stamp _original_index on tracks before submitting to download processor
so task indices match frontend modal row order. Sanitization and
filtering could reorder tracks, causing the backend to assign indices
that didn't match the displayed rows. Visual-only fix — actual
downloads were always correct.
2026-03-20 08:42:37 -07:00
Broque Thomas
46f82027cb Allow re-sync from download_complete state and add Rediscover button
All 6 playlist sync endpoints now accept download_complete phase so
users can re-sync after downloading. Added Rediscover button to
discovered and download_complete states (YouTube + Beatport). Added
full button set (Sync, Download Missing, Rediscover) for
download_complete which previously showed no buttons at all.
2026-03-19 22:17:59 -07:00
Broque Thomas
c53583716a Fix batch_complete firing with 0 downloads and album ID int cast error
- Only emit batch_complete automation event when successful_downloads > 0
- Remove int() cast on album_id in _fix_incomplete_album — Plex/Jellyfin
  use hash/text IDs that SQLite stores as-is in INTEGER columns
- Use string comparison for album_id equality check
2026-03-19 19:48:06 -07:00
Broque Thomas
fc4e16337a Redesign hybrid mode with N-source priority ordering
Replace fixed primary/secondary hybrid dropdowns with an ordered list
of all 5 download sources. Users enable/disable each source and reorder
with up/down arrows to set download priority. Sources are tried in
order until one returns results.

- New hybrid_order config field (backward compat with legacy primary/secondary)
- Download orchestrator loops ordered list with per-source error handling
- Sortable source list UI with icons, toggle switches, priority numbers
- Source-specific settings shown for all enabled hybrid sources
- Seamless migration from legacy 2-source to N-source format
2026-03-19 18:51:52 -07:00
Broque Thomas
3293a476fc Check staging folder for existing files before downloading
Before searching Soulseek/YouTube, checks the staging folder for a
matching file using tag-based or filename-based fuzzy matching. On match,
copies the file to the transfer folder and runs normal post-processing
(tagging, AcoustID, path organization). Staging scan is cached per batch
to avoid re-walking for every track.
2026-03-19 16:51:10 -07:00
Broque Thomas
e715ceef3e Add multi-source search tabs for enhanced search (Spotify/iTunes/Deezer)
Search results now show switchable tabs for alternate metadata sources.
Primary source renders immediately, alternate sources load in parallel
and tabs appear progressively as each completes.

- New /api/enhanced-search/source/<name> endpoint for per-source queries
- Source-aware routing via ?source= param on discography, album tracks,
  album detail, and artist image endpoints (prevents numeric ID
  misrouting between iTunes and Deezer)
- Source override stored on artistsPageState for consistent navigation
- Tabs styled with source brand colors, show result counts
- All additive — users who ignore tabs see zero behavioral change
2026-03-19 13:23:43 -07:00
Broque Thomas
b9c83a50fa Add Soulseek peer queue filtering and configurable download timeout
- Add max_peer_queue setting to skip peers with long queues (soft filter
  with fallback to unfiltered if all results removed)
- Add download_timeout setting replacing hardcoded 10-minute limit
- Include quality_score (peer health: upload speed, free slots, queue
  length) in result ranking — was calculated but never used in sort key
- New UI controls in Soulseek settings section
2026-03-19 11:47:37 -07:00
Broque Thomas
11b1222c90 Fix YouTube playlist parsing capped at ~100 tracks
Force full playlist resolution with lazy_playlist=False and materialize
entries list to prevent yt-dlp from stopping at the first page of results.
2026-03-19 09:47:12 -07:00
Broque Thomas
99481a0232 Fix Track Match search ignoring Track/Artist fields and low result limit
- Frontend was concatenating Track and Artist inputs into a single
  query string, causing Spotify to return mixed results matching
  either word in any field. Now sends track and artist as separate
  params; backend builds field-filtered query (track:X artist:Y).
- Result limit was silently capped at 10 in spotify_client.search_tracks
  via min(limit, 10). Raised to respect requested limit up to
  Spotify's API max of 50.
- iTunes fallback endpoint updated with same field-specific params.
- Legacy ?query= param still supported for backward compatibility.

Fixes #194
2026-03-19 08:23:19 -07:00
Broque Thomas
0a3cca4f1f Fix Spotify invalid_client and Tidal redirect URI mismatch on auth
- Duplicate `spotify` key in saveSettings() object literal caused
  second definition (embed_tags/tags) to silently overwrite the first
  (client_id/client_secret/redirect_uri), destroying credentials on
  every save. Merged into single key.
- authenticateSpotify() and authenticateTidal() now await saveSettings()
  before opening auth window, ensuring credentials are persisted.
- Tidal auth now dynamically sets redirect_uri from request host for
  LAN/Docker users and stores it in tidal_oauth_state so the callback
  token exchange uses the same URI.

Fixes #191
2026-03-19 08:02:12 -07:00
Broque Thomas
6b5e37aded Add preflight track-hash comparison and fix duplicate sync history entries
Preflight: hash track IDs before syncing and compare against last sync.
Skip only if exact same tracks were already synced and all matched.
Replaces the old count-based smart-skip which could miss track swaps.

Sync history: update existing entry for same playlist_id instead of
creating duplicates. Re-syncing the same playlist now refreshes the
existing row with new timestamps and stats.
2026-03-18 18:17:50 -07:00
Broque Thomas
0428a4565d Deduplicate variant album releases in artist discography view
Metadata sources (Deezer/iTunes/Spotify) return separate entries for
Explicit/Clean/Remastered variants of the same album. Normalize titles
by stripping variant suffixes and group by name+year, keeping the
version with the most tracks or preferring explicit.
2026-03-18 17:22:39 -07:00
Broque Thomas
e4579b4f2a Fix wishlist items from playlist sync missing album name and cover art
Playlist refresh was storing matched_data.album as a bare string with
no images dict. When unmatched tracks were added to the wishlist, they
showed as 'Unknown Album' with no cover art. Now all three playlist
import paths (authenticated API, public scraper, spotify_public) store
album as a proper dict with name and images array, matching the format
used by the discovery worker.
2026-03-18 17:11:26 -07:00
Broque Thomas
7b3bfd8db4 Fix artist discography miscategorizing albums as singles/EPs
Track count heuristics were overriding the album_type field from
Spotify/iTunes/Deezer, forcing any release with <=3 tracks into
singles and 4-6 tracks into EPs regardless of actual album type.
Now trusts the source's album_type directly.
2026-03-18 16:52:27 -07:00
Broque Thomas
b9732156cd Add per-tag granular toggle settings for all 47 embedded tags
Replace category-based tag settings (10 toggles) with per-tag controls
grouped by source service in an accordion UI. Each of the 11 service
groups (Spotify, iTunes, MusicBrainz, Deezer, AudioDB, Tidal, Qobuz,
Last.fm, Genius, General) has a master toggle that disables all child
tags, with individual toggles for fine-grained control. ISRC and
copyright fallback chains are now per-source toggleable. Genre merge
contributions from each source are independently controllable. All
tags default to enabled for backward compatibility.
2026-03-18 16:17:20 -07:00
Broque Thomas
d3bff90fd6 Add Picard-compatible MusicBrainz tags and per-category tag settings
Post-processing now writes all 18 MusicBrainz tags that Picard writes:
Release Group ID, Album Artist ID, Release Track ID, Release Type,
Status, Country, Original Date, Media, Barcode, Catalog #, ASIN,
Script, Total Discs (plus the 5 already supported). One cached API
call per album via get_release with recordings include.

New "Tags to Embed" settings section with 10 category toggles (all
enabled by default): MusicBrainz IDs, Release Info, Source IDs, ISRC,
BPM, Mood & Style, Copyright & Label, Genre Merging, URLs, Quality.
Each shows inline description of what it includes.
2026-03-18 15:49:36 -07:00
Broque Thomas
5adfdad9a3 Speed up dashboard polling intervals for more responsive UI
Service status 10s→5s, activity feed 5s→2s, watchlist/wishlist/db stats 30s→10s.
2026-03-18 15:02:00 -07:00
Broque Thomas
2c2c006f83 Fix wishlisted tracks showing 'Unknown Album' with no cover art
Multiple code paths built wishlist album data as a bare string instead
of a dict with images/album_type/total_tracks, causing wishlist entries
to lose album name and artwork. Fixed in: Beatport fallback conversion,
sync normalization, _ensure_spotify_track_format, and wishlist service
track reconstruction. Now uses track name as album fallback and always
includes required album dict keys.
2026-03-18 14:23:53 -07:00
Broque Thomas
333b8577ca Fix NoneType crash on Soulseek test when download orchestrator fails to init 2026-03-18 08:56:36 -07:00
Broque Thomas
2656927f79 Beatport enrichment progress, metadata cache fixes, per-source cache clearing
- Fix enrichment progress never updating: remove `continue` that skipped
  progress_callback for successful tracks in enrich_chart_tracks
- Split chart/extract into two-step flow: extract raw tracks, then enrich
  via polling endpoint with live progress overlay updates
- Move Beatport enrichment cache to persistent metadata cache system
- Fix metadata cache detail modal for Beatport (URL entity_ids with slashes)
- Add per-source Clear dropdown (Spotify/iTunes/Deezer/Beatport) to cache browser
- Remove debug logging from enrichment progress tracking
2026-03-18 08:39:02 -07:00
Broque Thomas
cde5754f0f Add Auto-Fill fix handler for incomplete album findings
When the maintenance worker flags an incomplete album, users can now
click "Auto-Fill" to automatically locate missing tracks in the library,
move/copy them into the album folder, and apply full metadata enhancement
(MusicBrainz, Deezer, cover art, etc.). Singles are moved; tracks from
multi-track albums are copied. Quality gate prevents filling FLAC albums
with lossy files. Tracks not found in library are added to wishlist with
album context for auto-download.
2026-03-17 22:56:24 -07:00
Broque Thomas
fbf44123ec Add Beatport download bubbles, enrichment cache, and batch enrichment
- Persistent download bubble cards on Beatport page and dashboard,
  matching the existing artist/search bubble UX with click-to-reopen,
  green checkmark on completion, and snapshot persistence
- In-memory enrichment cache (2h TTL, thread-safe) skips re-scraping
  when the same chart is clicked twice
- Batch enrichment replaces one-by-one HTTP requests with a single
  POST, using WebSocket progress events for overlay updates
- Fix _beatportModalOpening guard blocking modal open after fast
  cached enrichment by resetting the flag in openBeatportChartAsDownloadModal
- Hide Browse/My Playlists tabs — Browse is now the only Beatport view
2026-03-17 21:41:04 -07:00
Broque Thomas
27a0cf8b81 Add Sync History feature with live re-sync progress and source detection
- New sync_history DB table tracks last 100 syncs with full cached context
- Records history for all sync types: Spotify, Tidal, Deezer, YouTube,
  Beatport, ListenBrainz, Mirrored playlists, and Download Missing flows
- Sync History button on sync page with modal showing entries, source
  filter tabs, stats badges, and pagination
- Re-sync button: server syncs expand card inline with live progress bar,
  matched/failed counts, and cancel button; download syncs open download modal
- Re-syncs update the original entry (moves to top) instead of creating duplicates
- Delete button (x) on each entry with smooth remove animation
- Fix mirrored playlist source detection (youtube_mirrored_ matched youtube_)
- Fix broken server import thumbnails with URL validation
2026-03-17 20:39:41 -07:00
Broque Thomas
f8f113d0e7 Beatport direct download — skip discovery, open download modal directly
Clicking any Beatport item now opens the download modal directly instead of
going through the discovery flow (scrape → chart card → discovery modal →
Spotify/iTunes matching → download).

Releases (albums/EPs) open as album downloads with full context.
Charts/playlists (Top 100, Featured Charts, DJ Charts, Top 10) open as
playlist downloads with per-track enrichment — each track's individual
Beatport page is visited to get release name, duration, artwork, BPM, key,
genre, and label.

Key changes:
- Add get_release_metadata() and enrich_chart_tracks() to scraper
- Add /api/beatport/release-metadata and /api/beatport/enrich-tracks endpoints
- Rewrite all Beatport click handlers to open download modal directly
- Per-track enrichment with live progress overlay (one-by-one fetching)
- Split combined artist strings so folder paths use primary artist only
- Prevent Beatport IDs from being written to Spotify tag fields
- Add beatport_release_ prefix detection for album download mode
- Support enrich=false query param for frontend-driven enrichment
2026-03-17 19:28:11 -07:00
Broque Thomas
b186fed05b Fix Beatport new releases scraper broken by hashed CSS class changes
Replace all hardcoded styled-components class selectors (with build hashes
that break on every Beatport deploy) with partial class name matches using
[class*="prefix"] pattern. Applies to new releases endpoint and hero
release scraper.
2026-03-17 15:56:00 -07:00
Broque Thomas
6bf337423d Prefer album versions over singles when matching tracks to metadata sources
Add album_type and total_tracks fields to Track dataclass, populate from
Spotify/iTunes/Deezer API responses, and apply a small tiebreaker bonus
(+0.02 for albums, +0.01 for EPs) in all matching loops so album versions
win when confidence scores are otherwise equal.
2026-03-17 15:18:28 -07:00
Broque Thomas
6759f68b2b Fix $year template variable empty in discovery worker downloads
All discovery workers (playlist, Tidal, Deezer, Spotify Public, YouTube,
ListenBrainz, Beatport) built album_data dicts without release_date when
using fallback metadata sources. The Track dataclass had release_date
available but it was never extracted into album_data, causing $year to
resolve as empty in file path templates.
2026-03-17 12:46:39 -07:00
Broque Thomas
6bccbd9cf2 Fix Hydrabase background comparison to use configured metadata source instead of hardcoded iTunes 2026-03-17 12:09:05 -07:00
Broque Thomas
503e45fd9c Update What's New modal with recent fixes and downsample feature 2026-03-17 11:58:21 -07:00
Broque Thomas
a9d0607d25 Add hi-res FLAC to CD quality downsampling in post-processing
New toggle under Post-Download Conversion: automatically converts 24-bit
or high sample rate FLAC files to 16-bit/44.1kHz after download, replacing
the original. Uses ffmpeg with temp file + verify + atomic swap for safety.
Runs before lossy copy so MP3s are made from the downsampled version.
Also prevents bit depth strict mode from rejecting files that will be
downsampled anyway.
2026-03-17 11:53:56 -07:00
Broque Thomas
a62cc821ee Fix $year template variable empty in playlist/sync downloads
The fallback download path (playlists, discovery, sync) built spotify_album_context
from track_info.album which can be a plain string (no release_date) for discovery
playlists or missing metadata. Now backfills release_date, album_type, total_tracks,
and album name from the existing get_track_details API call — zero extra API calls.
Also adds total_discs passthrough for fallback path parity.
2026-03-17 11:35:19 -07:00
Broque Thomas
cc62541a65 Add Select All, Fix Selected & Fix All to Library Maintenance findings 2026-03-17 09:25:21 -07:00
Broque Thomas
162542a0cf Fix wishlist Unknown Album caused by stale cross-provider discovery data
Mirrored playlist discovery results are cached per-track in extra_data with
the provider that discovered them. When the user switches metadata source
(e.g. Spotify → Deezer), prepare-discovery was serving stale data from the
old provider — wrong IDs, empty albums, no cover art. Now it compares the
cached provider against the current active source and invalidates mismatched
entries, forcing a clean re-discovery with the correct provider.
2026-03-17 09:09:07 -07:00
Broque Thomas
b9d3c4051e Fix Unknown Album in wishlist: album name lost when raw Spotify data missing
Root cause: discovery searches return (Track, raw_data, confidence) but
raw_data can be None (Strategy 4 extended search) or have mismatched index.
When raw_data is None, album_obj becomes {} — an empty dict that passes
normalization unchanged, so album name is never populated.

Fix: after extracting album_obj from raw_data, fall back to track_obj.album
(always populated from the SpotifyTrack dataclass) when album_obj has no
name. Applied to all 3 discovery paths (Deezer, Tidal, Spotify Public).

Also handle both string and dict album formats in wishlist UI rendering.
2026-03-17 08:12:34 -07:00
Broque Thomas
8b403e3947 Fix wishlist normalization mutating tracks_json in-place
The normalization loop was modifying dict objects shared with the
SpotifyTrack construction loop. SpotifyTrack.artists expects List[str]
but normalization converted to [{'name': ...}] dicts. Use dict(t) copy
so original tracks_json is untouched.
2026-03-17 07:57:02 -07:00
Broque Thomas
7fd29c4f77 Fix confirm modals hidden behind other modals and wishlist showing Unknown Album
- Bump confirm dialog z-index to 200000 (both #confirm-modal-overlay and
  .confirmation-modal-overlay) so they always appear above all other modals
- Normalize track data in _run_sync_task before storing in original_tracks_map:
  album as dict {'name': ...} and artists as [{'name': ...}] — fixes all
  source converters (Deezer, YouTube, Tidal, Spotify Public, ListenBrainz,
  Beatport) whose fallback paths stored plain strings, causing the wishlist
  UI to show "Unknown Album" for every synced track
2026-03-17 07:53:59 -07:00