Commit graph

886 commits

Author SHA1 Message Date
Broque Thomas
71e4df65e3 Remove emojis from all Python log and print statements
Stripped 4,200+ emoji characters from print(), logger calls across
39 Python files. Logs are now clean text — easier to grep, more
professional, no encoding issues on terminals without Unicode support.

Seasonal config icons preserved for UI display.
2026-04-11 21:11:02 -07:00
Broque Thomas
7fb4cf6b0e Use per-track artist in tag writer for compilations and DJ mixes
Tag preview and writer now use track_artist (per-track artist) for the
Artist tag when available, falling back to artist_name (album artist)
when NULL. Album Artist tag always uses artist_name.

Fixes #277 — DJ mix albums no longer overwrite per-track artists
(Technique, Gouryella) with the album artist (Tiësto).
2026-04-11 18:31:11 -07:00
BoulderBadgeDad
8977120ba8
Merge pull request #274 from kettui/feat/metadata-client-caching
Add caching for metadata clients
2026-04-11 13:07:54 -07:00
Broque Thomas
0d55356eaf Update What's New with music videos, Lidarr source, and bug fixes
Added sections for Music Videos search/download, Lidarr download source
(development), and recent bug fixes (dismissed findings, orphan detector,
duplicate audio streams, logs directory, stale discovery re-processing).
2026-04-11 09:50:33 -07:00
Broque Thomas
6fb32228e0 Create logs directory before source_reuse.log handler to prevent startup crash 2026-04-11 08:39:20 -07:00
Broque Thomas
6b2352411b Add Lidarr to all remaining source lists in orchestrator and web_server 2026-04-11 08:05:54 -07:00
Broque Thomas
fc38ec4787 Add Lidarr as 7th download source and validate music video path
Lidarr integration:
- New core/lidarr_download_client.py with full interface parity
  (search, download, status, cancel — same as Qobuz/Tidal/HiFi)
- Registered in download orchestrator with source routing
- Settings: URL + API key on Downloads tab with connection test
- Available as standalone source or in Hybrid mode priority order
- API key encrypted at rest
- All streaming source checks updated to include 'lidarr'

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

Music video path validation:
- Empty/unconfigured path returns clear error instead of silent failure
- Write permission test before starting download
- Default changed from './MusicVideos' to empty (must be configured)
2026-04-11 07:59:12 -07:00
Antti Kettunen
45f608bf12 Utilize cached spotify client in get_spotify_artist_discography 2026-04-11 13:55:14 +03:00
Antti Kettunen
fd6335a66e Add / improve metadata client caching
Clients are for the most part being initialized per-request, which leads to a lot of redundant client initialization, as well as noise on the logs, since each client initialization emits a row on the logs, eg. 'Deezer client initialized'
2026-04-11 13:55:10 +03:00
Broque Thomas
54b7a0f0e8 Add music video download with progress and metadata matching
Click any video card in Music Videos tab to download. Flow:
1. Search primary metadata source for clean artist/title
2. Fall back to YouTube title parsing if no match
3. Download video via yt-dlp (best quality MP4)
4. Save to configured Music Videos folder as Artist/Title-video.mp4

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

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

- Backend: /api/enhanced-search/source/youtube_videos endpoint with
  search_videos() method on YouTubeClient returning YouTubeSearchResult
- Frontend: Video grid layout with responsive cards, YouTube red tab
  color, proper section hiding when switching between metadata and
  video tabs
- Global search: Full parity with enhanced search video rendering
- No download functionality yet — display only
2026-04-10 23:07:14 -07:00
Broque Thomas
1f0ef08b48 Add Music Videos directory setting for Plex music video support
New configurable path for storing music videos separately from audio
files, following Plex's global music video folder convention.

- Settings: library.music_videos_path (default: ./MusicVideos)
- UI: Music Videos Dir field on Settings Downloads tab with lock/unlock
- Docker: /app/MusicVideos volume mount in Dockerfile and docker-compose
- Added 'library' to settings save whitelist (was missing — music_paths
  also wasn't persisting through main settings save)
- No download functionality yet — path infrastructure only
2026-04-10 22:01:27 -07:00
Broque Thomas
acb4479313 Re-discover tracks with incomplete metadata in playlist pipeline
The discovery worker skipped already-discovered tracks even when their
matched_data was incomplete (missing track_number, release_date, album
ID). These stale discoveries from before the enrichment fix would
persist forever, causing the automation pipeline to keep producing
tracks with no year, no track numbers, and no cover art.

Now treats discovered tracks as undiscovered if they're missing
track_number AND have no release_date or album ID, so the enriched
discovery pipeline fills in the gaps on the next run.
2026-04-10 17:11:42 -07:00
Broque Thomas
94e0671eb4 Update What's New with metadata pipeline and matching engine fixes
Adds two new sections to the version modal covering the Unknown Artist
fix, centralized metadata source selection, Deezer cache fix, sync
completion feedback, Fix Unknown Artists maintenance job, and the
matching engine artist gate improvements.
2026-04-10 14:21:37 -07:00
Broque Thomas
498c22e7c3 Centralize metadata source selection in core/metadata_service.py
All metadata source decisions now flow through get_primary_source() and
get_primary_client() in core/metadata_service.py. Previously 6 different
files reimplemented this logic with inconsistent defaults ('itunes' vs
'deezer') and auth checks, causing bugs when any one was missed.

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

Future changes to source selection only need to update one file.
2026-04-10 12:34:25 -07:00
Broque Thomas
57fc18f994 Respect configured primary source in seasonal, playlists, and explorer
Seasonal discovery, personalized playlists, and playlist explorer all
defaulted to Spotify when authenticated, ignoring the user's configured
primary source. Now they read from config first.

Spotify's related_artists API (no Deezer/iTunes equivalent) is preserved
as a fallback for all users in personalized playlists. Artist discography
endpoint intentionally unchanged — ID-based lookups need the source that
owns the ID.
2026-04-10 11:43:06 -07:00
Broque Thomas
7d21385ce9 Show which tracks failed to match in sync completion toast
When a playlist sync has unmatched tracks sent to wishlist, the
completion toast now shows the specific track names instead of just
a count. Uses warning style so it stands out. The unmatched track
list is included in the sync state result so it's available for
both live status polling and notification history.

Addresses #272 — silent sync failures where users couldn't tell
which tracks out of 150+ failed to match their Plex library.
2026-04-10 11:04:21 -07:00
Broque Thomas
dd5291456b Fix playlist pipeline discovery data loss and Unknown Artist bug
Discovery workers now respect the user's configured primary metadata
source instead of always using Spotify when authenticated. This
completes the intent of commit 3c211ea.

The core fix addresses data loss in the discovery→sync→wishlist→download
pipeline: the Track dataclass strips album metadata to a plain string,
losing album ID, track_number, release_date, and images. Discovery
workers now enrich results via get_track_details() to recover this data.
Deezer's get_track_details() cache validation was incorrectly trusting
search-result cache (which lacks track_position), returning track_number=0.

Also fixes wishlist download processing where albums without IDs couldn't
map to artists, and the fallback read 'artist' (singular) instead of
'artists' (plural), always producing "Unknown Artist".

Includes a one-time migration to purge stale discovery cache entries.
2026-04-10 10:23:43 -07:00
Broque Thomas
959bca2b8d Add Run Script action to automation engine
New automation action that executes user scripts from a dedicated
scripts/ directory. Available as both a DO action and THEN action.
Scripts are selected from a dropdown populated by /api/scripts.

Security: only scripts in the scripts dir can run, path traversal
blocked, no shell=True, stdout/stderr capped, configurable timeout
(max 300s). Scripts receive SOULSYNC_EVENT, SOULSYNC_AUTOMATION,
and SOULSYNC_SCRIPTS_DIR environment variables.

Includes Dockerfile + docker-compose.yml changes for the scripts
volume mount, and three example scripts (hello_world.sh,
system_info.py, notify_ntfy.sh).
2026-04-09 18:20:29 -07:00
Broque Thomas
963a003ca0 Set playlist poster image on Plex/Jellyfin/Emby after sync
After a successful playlist sync, if the source playlist has cover
art (Spotify, Tidal, Deezer, etc.), the image is downloaded and
uploaded as the playlist poster on the media server. Plex uses
uploadPoster(), Jellyfin/Emby uses POST /Items/{id}/Images/Primary.
Navidrome skipped (no playlist image API). Failure is silent — sync
result unchanged. Automation-triggered syncs and playlists without
images are unaffected.
2026-04-09 10:08:18 -07:00
Broque Thomas
1e69d813e6 Update What's New and Help docs with recent changes
Added dead file fix options, Deezer ARL rehydration, and album
data caching to the Fixes & Improvements section and Help docs.
2026-04-09 09:44:15 -07:00
Broque Thomas
163498e142 Update What's New modal and Help docs for all recent changes
What's New: Added Deezer user playlists, Qobuz token auth, streaming
source artist gate, download history provenance, and comprehensive
fixes section covering artist name casing, future album skip, track
number fix, Emby sync, discovery fix fallback, and all new settings.

Help docs: Updated Qobuz auth to mention token option, added Music
Library Paths, Replace Lower Quality, and HiFi Instance Health to
Other Settings section.
2026-04-08 13:30:21 -07:00
Broque Thomas
f603f92868 Add configurable music library paths for file resolution
New setting in Settings > Library lets users add folder paths where
their music files live. The file resolver checks these paths when
looking for library files, solving Docker path mismatches and multi-
folder libraries. Required for tag writing, streaming, and orphan
detection when the media server reports paths that differ from what
SoulSync can see. Docker users mount their music folder(s) with
read-write access and add the container-side path. Default is empty
— existing users see no change.
2026-04-08 13:24:15 -07:00
Broque Thomas
c6bebd5e09 Add opt-in setting to replace lower quality files on import
New toggle in Settings > Library: "Replace lower quality files on
import". When enabled, if a track already exists in the library at
a lower quality tier (e.g. MP3) and a higher quality version (e.g.
FLAC) is imported from staging, the existing file is replaced.
Comparison uses the existing QUALITY_TIERS system (lossless > opus/
ogg > m4a > mp3). When disabled (default), existing behavior is
unchanged — existing tracks are always kept. Also applies to regular
downloads that land on an existing file.
2026-04-08 11:32:09 -07:00
Broque Thomas
e1c5ae0eb5 Fix discovery fix search fallback and add Deezer search endpoint
Discovery fix modal now tries the user's active metadata source
first, then falls back through all available sources (Spotify,
Deezer, iTunes) in sequence. Previously hardcoded Spotify with no
fallback, leaving users without Spotify stuck on "Searching...".

New /api/deezer/search_tracks endpoint exposes the existing
DeezerClient.search_tracks() method for the fix modal. Same
request/response format as Spotify and iTunes endpoints.
2026-04-08 11:04:22 -07:00
Broque Thomas
3d96752087 Add Qobuz auth token login as CAPTCHA bypass alternative
Qobuz added reCAPTCHA to their login endpoint, blocking automated
email/password auth for new users. Token login lets users paste
their X-User-Auth-Token from the browser DevTools after logging in
manually. Added to both Connections and Downloads tabs with
instructions. Existing email/password flow completely unchanged.
Backend validates token via user/get API and saves the session
identically to email/password login.
2026-04-08 10:37:38 -07:00
Broque Thomas
e1f7b8f5cc Add HiFi API instance health check to Settings
New "Check All Instances" button in Settings > Downloads > HiFi
shows each configured instance with color-coded status: green for
fully working (can download), orange for search-only (downloads
fail), red for offline/SSL error/timeout. Helps users understand
why HiFi downloads aren't working when community instances go down.
2026-04-08 09:58:55 -07:00
Broque Thomas
ba7eb1c5e7 Remove debug test activity feed message from startup 2026-04-08 07:48:18 -07:00
Broque Thomas
bd45e89515 Fix playlist sync tracks always tagged as track number 01
When downloading individual tracks from a playlist sync, the album
detection fallback hardcoded track_number to 1 instead of using the
value already available in the download context from the playlist
metadata. Now checks original_search.track_number and track_info
.track_number before falling back to 1. Full album downloads and
Spotify API lookup paths are completely unchanged.
2026-04-08 07:45:11 -07:00
Broque Thomas
31518a3ef3 Add Deezer user playlists tab via ARL authentication
New "Deezer" tab on sync page shows authenticated user's playlists,
identical to the Spotify tab pattern — same card layout, details
modal with track list, sync button, and download missing tracks
flow. Existing URL import renamed to "Deezer Link" tab (unchanged).

Backend: get_user_playlists() and get_playlist_tracks() on the
download client fetch via public API with ARL session cookies.
Album release dates batch-fetched for $year template variable.
Three new endpoints: arl-status, arl-playlists, arl-playlist/<id>.

Frontend: cards use Spotify-identical HTML structure with live sync
status, progress indicators, and View Progress/Results buttons.
Downloads reuse openDownloadMissingModal with zero modifications.
Track data cached on first open, instant on subsequent clicks.
2026-04-07 12:47:15 -07:00
Broque Thomas
e65b6bab67 Add metadata source filter to library and fix Discogs enrichment
Library page: new dropdown filter to show artists matched or unmatched
to any metadata source (Spotify, MusicBrainz, Deezer, Discogs, etc).
Select "No Discogs" to find artists needing manual Discogs matching.
Filter applied as WHERE clause on the source ID columns.

Discogs enrichment: added to valid_services whitelist, _enrichment_locks,
and _run_single_enrichment handler. The Enrich button was returning an
error when Discogs was selected from the dropdown.
2026-04-07 09:29:48 -07:00
Broque Thomas
d597123a40 Add Discogs to enrichment service whitelist
Discogs was missing from valid_services, _enrichment_locks, and
_run_single_enrichment handler. The Enrich button on the enhanced
library page returned "service must be one of" error when Discogs
was selected. Added handler using _process_item pattern matching
other batch-style workers, with guard against track-level enrichment.
2026-04-07 08:45:01 -07:00
Broque Thomas
23b80a0077 Add database maintenance UI with VACUUM and incremental vacuum
Settings > Advanced now shows database size, free pages, and auto-
vacuum mode. Two actions: Compact Database (full VACUUM to reclaim
dead space) and Enable Incremental Vacuum (one-time setup for
automatic page reclamation). Both have confirmation dialogs warning
about lock time on large databases. Info refreshes on Advanced tab
switch and after each operation.
2026-04-06 22:57:52 -07:00
Broque Thomas
06defcfa3d Fix streaming source matching and global search download bubbles
Streaming matching: add artist gate rejecting candidates with artist
similarity below 0.4, raise threshold to 0.60, block fallback to
Soulseek filename matcher for Tidal/Qobuz/HiFi/Deezer. Fix single-
char artist containment bug where normalize_string strips non-ASCII
(e.g. "B小町" → "b") causing "b" to match any artist containing
that letter. Fixed in both score_track_match and the Soulseek scorer.
YouTube and Soulseek matching behavior unchanged.

Global search: add registerSearchDownload() calls to _gsClickAlbum
and _gsClickTrack so downloads create bubble snapshots on dashboard
and search page, matching the enhanced search standard.

Global search escaping: add _escAttr() helper to handle newlines in
album/artist names that broke inline onclick string literals.
2026-04-06 22:15:21 -07:00
Broque Thomas
410bddd102 Redesign download history with collapsible entries and full provenance
Entries are now compact cards that expand on click to reveal source
details. Shows expected vs downloaded title/artist with red mismatch
highlighting. Source artist column added to DB. Streaming track IDs
extracted from the id||name filename pattern. File and ID always on
their own line to avoid edge-case misplacement.
2026-04-06 19:33:02 -07:00
Broque Thomas
f8fbcb507c Add source provenance and AcoustID result to download history
Track original source filename, track ID, and AcoustID verification
result for every download. Helps debug wrong-file downloads from
streaming sources like Tidal. Each column migrated independently
for crash safety. Frontend shows source detail line and color-coded
AcoustID badge per entry. Button renamed to "Download History".
2026-04-06 16:18:58 -07:00
Broque Thomas
3c211eaac8 Make Spotify a selectable metadata source instead of auto-override
Spotify was automatically the primary metadata source whenever
authenticated, ignoring the user's fallback dropdown selection. Now:

- Dropdown renamed "Fallback Source" → "Primary Source" with Spotify
  added as an option alongside iTunes, Deezer, Discogs
- User's selection drives search results, discovery page, status display
- Enhanced search uses selected primary source first, others as tabs
- Discovery page filters by selected source's artist IDs
- Spotify auth still works for playlists, followed artists, enrichment
- Default is 'deezer' — users who want Spotify select it explicitly
2026-04-06 11:27:49 -07:00
Broque Thomas
db5bdb9c59 Fix Opus/AAC missing cover art when source FLAC has no embedded pictures
FLAC → Opus/AAC conversion only tried to read embedded art from the
source FLAC. If art was only in cover.jpg (not embedded), the lossy
copy got no art. Now falls back to cover.jpg in the same directory.
2026-04-06 10:14:08 -07:00
Broque Thomas
03564ed026 Enable AcoustID verification for all download sources
Previously skipped for Tidal/Qobuz/HiFi/Deezer as "trusted API sources"
but streaming APIs can return wrong versions (live, remix, cover).
AcoustID now runs for every download source when enabled.
2026-04-06 09:53:54 -07:00
Broque Thomas
91f662646b Fix Spotify OAuth scope mismatch + auth validation in callbacks (#253)
Added user-follow-read scope to all 5 SpotifyOAuth instances in
web_server.py (was only in spotify_client.py). Fixed OAuth callbacks
using is_authenticated() instead of is_spotify_authenticated() — the
former returns True with iTunes/Deezer fallback, masking auth failures.

Credit: kettui (PR #253) identified both issues.
2026-04-06 08:20:26 -07:00
Broque Thomas
d16aaab0aa Fix empty artist folder created at base level with custom path templates
When using templates like "albums/$albumartist/$album/$track - $title",
post-processing created Transfer/ArtistName/ before computing the
template path, leaving an empty folder. _build_final_path_for_track
handles all directory creation based on the template — the premature
makedirs was redundant and incorrect.
2026-04-05 22:05:29 -07:00
Broque Thomas
51a433a558 Fix duplicate artists in search, per-artist name refresh, global search track click
- search_artists() now filters by active media server — no more duplicate
  results from Plex/Jellyfin/Navidrome showing the same artist 3 times
- Per-artist Sync button re-fetches artist name from media server, catches
  renames (e.g., Plex changing "Kendrick Lamar" to "eastside k-boy")
- Global search track click opens download modal directly instead of
  navigating to enhanced search page (matches enhanced search behavior)
2026-04-05 20:41:43 -07:00
Broque Thomas
a2b9e32d04 Add download source tracking to library history modal
New download_source column on library_history table records which source
(Soulseek, Tidal, Qobuz, HiFi, YouTube, Deezer) each track was downloaded
from. Extracted from context username during post-processing.

Frontend shows source badge alongside quality badge on each download entry.
Source breakdown bar below tabs shows per-source totals with color-coded
chips (e.g., "Soulseek: 847 | Tidal: 203"). Includes DB migration for
existing installs. Existing entries show quality only (source is NULL).
2026-04-05 18:03:46 -07:00
Broque Thomas
7a24431e46 Redesign watchlist linked artist section with per-source match controls
Replaced single "Change" button with per-source rows showing match
status for each provider (Spotify, Apple Music, Deezer, Discogs).
Each row has Fix/Match button that searches that specific source API,
plus clear button to remove individual matches.

- Per-source search uses _search_service (same as enrichment modal)
- Backend: added Discogs to valid providers, empty ID clears match
- Fixed provider validation to accept 'discogs' alongside others
- Clear sets DB column to NULL instead of rejecting empty string
2026-04-05 14:20:35 -07:00
Broque Thomas
d4ce345ae2 Backfill all metadata source IDs during manual watchlist scan
Manual scan path was only backfilling the active provider (e.g., only
Spotify IDs if Spotify was active). Now matches the auto-scan behavior:
backfills iTunes, Deezer, Spotify (if auth'd), and Discogs (if token)
for all watchlist artists before scanning begins.
2026-04-05 14:09:14 -07:00
Broque Thomas
8344cf2c7a Decouple wishlist processing and watchlist scanning — allow concurrent execution
Removed cross-guards that blocked wishlist downloads while watchlist scans
ran (and vice versa). Downloads use bandwidth, scans use API calls —
different resources. The per-call rate limiter handles any API contention.

- Automation engine: each handler now has self-only guard (no cross-block)
- _process_wishlist_automatically: removed watchlist scanning check
- Pipeline Phase 4: removed watchlist scanning check
- Manual watchlist scan endpoint: removed wishlist processing check

Users with large watchlists (6+ hour scans) will now see downloads
starting immediately instead of waiting for the scan to finish.
2026-04-05 13:25:44 -07:00
Broque Thomas
8ba1585646 Paginate wishlist modal to prevent browser crash on large wishlists (5K+)
Initial load fetches 200 tracks instead of all. "Load More" button at
bottom shows (200 of 5,000) and loads next batch on click. Backend now
returns total count alongside limited results for both album and singles
categories. Rendering logic unchanged — just operates on smaller sets.
2026-04-05 13:15:24 -07:00
Broque Thomas
5cecb46788 Fix spotipy deprecation warning for get_access_token(as_dict=True)
Removed as_dict=True from all three OAuth callback sites. Spotipy will
return token string directly in future versions — our code only checks
truthiness so both dict and string work.
2026-04-05 12:43:26 -07:00
Broque Thomas
e674a79c88 Persist API call history, record rate limit events, fix Spotify re-auth issues
API Call Tracker:
- Save/load 24h minute-bucketed history + events to database/api_call_history.json
- Persists across server restarts via atexit + signal handler hooks
- New record_event() for rate limit bans (called from _set_global_rate_limit)
- New get_debug_summary() for Copy Debug Info — 24h totals, peak cpm with
  timestamp, per-endpoint breakdown, and last 20 rate limit events
- Fixed race condition: events iteration now inside lock during save

Spotify Rate Limit Mitigation:
- Enrichment worker: max_pages=5 on get_artist_albums (was unlimited — artist
  with 217 albums caused 22 paginated API calls, now capped at 5)
- Enrichment worker: inter_item_sleep raised from 0.5s to 1.5s

Spotify Re-Auth Fix:
- Both OAuth callbacks (port 8008 + 8888) now clear rate limit ban AND
  post-ban cooldown after successful re-auth — Spotify usable immediately
  instead of stuck on Deezer fallback for 5 minutes
- Auth cache invalidated on both global client and enrichment worker client
2026-04-05 12:36:58 -07:00
Broque Thomas
9f7fe27e7c Add Playlist Pipeline automation + wishlist badges + Discogs enrichment modal + hub cleanup
Playlist Pipeline — single automation that runs the full playlist lifecycle:
refresh → discover → sync → download missing. Replaces 4-automation signal
chains. Phase-aware progress display (Phase 1/4, 2/4, etc.), guard function
prevents concurrent runs, fire-and-forget wishlist at end. Re-sync loop
catches newly downloaded tracks on next scheduled run.

- New action type 'playlist_pipeline' with handler, blocks endpoint config,
  builder UI (playlist select, process all, skip wishlist checkboxes),
  help modal, result display map, and Hub template
- Removed 3 redundant Hub templates (Release Radar, Discovery Weekly,
  Playlist Auto-Sync) — all replaced by the pipeline
- Fixed sync completion polling (status is 'finished' not 'complete')
- Fixed refresh handler progress hijack (null out _automation_id)
- Fixed matched_tracks field access from sync_states result

Also in this commit:
- Wishlist badges on enhanced search and global search tracks (amber)
- Discogs added to manual enrichment modal search + artist/album dropdowns
- Profile PIN forgot recovery on profile selection dialog
2026-04-05 11:34:50 -07:00
Broque Thomas
32cc7cbd5e Fix artist info modal failing on watchlist map nodes with NULL IDs
Watchlist map nodes used w.get('key', '') which returns None when the
DB column is NULL (key exists with None value). None serialized to JSON
null, which is falsy in JS, causing 'No source ID' throw for every
artist click.

- Changed to `w.get('key') or ''` for all ID fields (coerces None→'')
- Added discogs_id to watchlist nodes (was missing entirely)
- Removed hard throw when no source ID — falls back to name-based lookup
- Added console.error logging for future diagnosis
2026-04-04 23:59:02 -07:00
Broque Thomas
d34924e238 Apply same match validation to streaming download sources as Soulseek
Tidal, Qobuz, HiFi, and Deezer results were blindly taking the first
API result with minimal validation. Now all streaming sources use
score_track_match() — same 60% title / 30% artist / 10% duration
weighting as Soulseek, plus version detection penalties.

- web_server.py get_valid_candidates(): replaced loose title-sim check
  with matching engine scoring, version penalty for live/remix/acoustic
- download_orchestrator.py: optional expected_track param enables
  scoring in search_and_download_best (backward compatible)
- sync_service.py: passes spotify_track for validation
- Fixed wrong class name (MusicMatchingEngine not MatchingEngine)
2026-04-04 22:01:29 -07:00
Broque Thomas
58ee8d8a8a Fix cover.jpg not using Cover Art Archive during wishlist processing
cover.jpg was always written from Spotify/iTunes URL (640x640) when the
first track in an album reached _download_cover_art before MusicBrainz
lookup completed. Later tracks with MBID skipped because file existed.

Fix: when cover.jpg exists but is small (<200KB) and we now have a CAA
MBID, attempt to upgrade it with the high-res CAA version. If CAA fetch
fails, keep existing cover — no pointless overwrites.

Also adds Forgot PIN recovery to the profile selection PIN dialog,
reusing the same credential verification flow as the launch lock screen.
Backend reset endpoint now accepts profile_id parameter.
2026-04-04 21:40:21 -07:00
Broque Thomas
4439873542 Backfill missing watchlist artist images during scan
Runs at scan start after ID backfill — zero API calls, all DB lookups:
1. Metadata cache artist image (any source)
2. Deezer direct URL from stored deezer_artist_id
3. Deezer ID from metadata cache by name (even if not on watchlist row)
4. Album art fallback (iTunes artists have no artist images)

Also fixes update path for artists with no external IDs — falls back
to direct row ID update instead of silently matching zero rows.
2026-04-04 21:26:25 -07:00
Broque Thomas
48a9de8861 Artist Map: image proxy, caching, on-the-fly explorer, genre uncap, dated changelogs
- Image proxy endpoint (/api/image-proxy) for canvas CORS — allowlisted CDNs,
  browser-like UA for Deezer, 24h cache headers. Direct CORS first, proxy fallback.
- Server-side 5-min cache on all artist map endpoints with auto-invalidation
  on watchlist add/remove, scan complete, and new MusicMap discoveries.
- Explorer fetches similar artists from MusicMap on-the-fly when none stored,
  saves to DB for instant future visits. Validates artist names against
  Spotify/iTunes API before loading map — rejects gibberish with 404.
- Genre map per-genre cap removed (was 300 backend, 400 frontend).
- Center node in Explorer uses type 'center' not 'watchlist' — no longer
  misidentified as a watchlist artist.
- Error overlay auto-dismisses after 2.5s and returns to Discover page.
- Helper What's New restructured with dated sections (April 4/3/2/1, March),
  trimmed from ~80 to ~38 entries, date headers styled as purple dividers.
- Version modal updated with Artist Map section and recent fixes.
2026-04-04 21:18:08 -07:00
Broque Thomas
c336604b71 Fix hero slider + recommended modal returning 0 artists
get_top_similar_artists now accepts require_source parameter to filter
by source ID in SQL. Previously fetched 200 artists then post-filtered,
but cycling logic (last_featured ASC) rotated artists without IDs to
the front, causing all 200 to be filtered out.

Both /api/discover/hero and /api/discover/similar-artists now pass
require_source=active_source so only artists with valid IDs are returned.
2026-04-04 12:19:29 -07:00
Broque Thomas
d349754a93 Artist Map: cache backfill, constellation effects, related artists, polish
- Metadata cache backfill: batch-lookup all node names across all sources
  to fill missing IDs, images, and genres
- Source-aware navigation: View Discography passes correct source to
  artist page so non-active-source artists load correctly
- Constellation hover effect: 800ms delay, fade in/out animation, dim
  overlay with glowing connection lines to related artists
- Click ripple animation on node selection
- Related artists list in info modal with clickable navigation
- Rich tooltip with artist photo, name, genres on hover
- Removed node dragging (caused visual desync with offscreen buffer)
- Performance: cached constellation lookups, lighter cache query
  (no raw_json), canvas.width for proper DPR overlay coverage
2026-04-04 11:31:25 -07:00
Broque Thomas
cfac226eed Add Artist Map — force-directed constellation of watchlist + similar artists
Visual canvas map on Discover page showing watchlist artists as large
anchor bubbles surrounded by their similar artists, sized by relevance.

Layout: golden angle spiral for watchlist nodes with push-apart guarantee,
spiral packing with spatial grid collision detection for similar artists.
Offscreen buffer rendering for smooth pan/zoom (single drawImage blit).

Features:
- 320px watchlist bubbles, similar sized 25-55% by rank/occurrence
- Search bar with instant filter + animated zoom-to-node
- Tooltip with artist photo, name, genre tags on hover
- Touch support: single finger pan, pinch zoom, tap to click
- Zoom +/- buttons and fit-to-screen with smooth 250ms animation
- Click opens artist info modal (same as Your Artists)
- Loading overlay with image count progress
- Async image loading via createImageBitmap (non-blocking)
2026-04-04 10:22:15 -07:00
Broque Thomas
58d8e830c6 Your Artists on Discover + Deezer OAuth + MB Lookups Manager + Explorer improvements + bug fixes
YOUR ARTISTS (major feature):
- Aggregates liked/followed artists from Spotify, Tidal, Last.fm, Deezer
- Matches to ALL metadata sources (Spotify, iTunes, Deezer, Discogs)
- DB-first matching: library → watchlist → cache → API search (capped)
- Image backfill from Spotify API for artists missing artwork
- Carousel on Discover page with 20 random matched artists
- View All modal with search, source filters, sort, pagination
- Artist info modal: hero image, matched source badges, genres, bio,
  listeners/plays from Last.fm, watchlist toggle, view discography
- Auto-refresh with loading state on first load, polls until ready
- Deduplication by normalized name across all services

DEEZER OAUTH:
- Full OAuth flow: /auth/deezer + /deezer/callback
- Settings UI on Connections tab (App ID, Secret, Redirect URI)
- Token stored encrypted, auto-included in API calls
- get_user_favorite_artists() for liked artists pool

SERVICE CLIENTS:
- Spotify: added user-follow-read scope + get_followed_artists()
- Tidal: get_favorite_artists() with V2/V1 fallback
- Last.fm: get_authenticated_username() + get_user_top_artists()

FAILED MB LOOKUPS MANAGER:
- Manage button on Cache Health modal
- Browse/filter/search all failed MusicBrainz lookups
- Search MusicBrainz directly and manually match entries
- Optimized cache health queries (11 → 4 consolidated)
- Dashboard cache stats now poll every 15s

EXPLORER IMPROVEMENTS:
- Discover button on undiscovered playlist cards
- Status badges: explored/wishlisted/downloaded/ready
- Auto-refresh during discovery via polling
- Redesigned controls: prominent Explore button, icons

BUG FIXES:
- Fix album artist splitting on collab albums (collab mode fed
  album-level artists instead of per-track)
- Fix cover.jpg not moving during library reorganize (post-pass sweep)
- Fix cover.jpg missing when album detection takes fallback path
- Fix wishlist auto-processing toast spam (was firing every 2s)
- Fix media player collapsing on short viewports
- Fix watchlist rate limiting (~90% fewer API calls)
- Configurable spotify.min_api_interval setting
- Better Retry-After header extraction
- Encrypt Last.fm and Discogs credentials at rest
- Add $discnum template variable (unpadded disc number)
2026-04-03 22:39:05 -07:00
Broque Thomas
20582c531b Add $discnum template variable for unpadded disc number
$disc gives zero-padded "01", $discnum gives raw "1". Useful for
Plex-style naming like "102 - Track.flac" (disc 1 track 02).

Added to both _get_file_path_from_template and _get_file_path_from_template_raw
at all 6 replacement sites. $discnum always replaced before $disc to
prevent partial matching. No existing behavior changed.
2026-04-03 13:44:08 -07:00
Broque Thomas
8b58434c17 Explorer improvements: discover from Explorer, status badges, auto-refresh
1. Discover button on undiscovered playlist cards — triggers discovery
   directly from Explorer instead of redirecting to Sync page. Button
   changes to "Open" to reopen modal after closing.

2. Status badges on playlist cards: checkmark (in library), heart
   (wishlisted), star (fully discovered), percentage (needs discovery).
   Meta line shows "N in library · M wishlisted" counts.

3. Auto-refresh: polls every 5s during active discovery to update cards.
   WebSocket listener for discovery:progress events. Cards refresh when
   discovery completes.

4. Explored tracking: playlists get green checkmark badge after tree is
   built (session-only, resets on reload).

Backend: new get_mirrored_playlist_status_counts with fail-safe design —
core discovery counts use simple reliable queries, library/wishlist
counts are best-effort extras that won't break discovery detection.

Card layout redesigned: badges inline with playlist name, discover
button below meta text, no more absolute positioning overlaps.
2026-04-03 13:00:04 -07:00
Broque Thomas
93fb082172 Add Failed MB Lookups manager + optimize cache performance
New feature: Failed MusicBrainz Lookups management modal accessible
from Cache Health. Browse all failed lookups with type filter tabs,
search bar, pagination. Click any entry to search MusicBrainz and
manually match — saves MBID at 100% confidence. Clear individual
entries or bulk clear all.

Backend: 4 new endpoints — failed-mb-lookups list, mb-entry delete,
musicbrainz/search (artist/release/recording), mb-match save.

Performance: Cache health stats consolidated from 11 queries to 4
using CASE expressions. Added partial index on musicbrainz_cache for
failed lookups. Dashboard cache stats now poll every 15s instead of
single fire-and-forget fetch. Failed MB type counts cached on frontend,
only re-fetched after mutations.

Also includes: library reorganize now moves cover.jpg via post-pass
sidecar sweep, and changelog updates.
2026-04-03 11:38:50 -07:00
Broque Thomas
fd09cad83e Fix missing cover.jpg when album detection takes fallback path
When _detect_album_info_web couldn't find a good Spotify match, the
fallback dict lacked album_image_url — so _download_cover_art silently
skipped the download even though the URL was available in the context's
spotify_album object.

Now _download_cover_art accepts optional context parameter and falls
back to spotify_album.image_url or spotify_album.images[0].url when
album_info lacks the URL. All 4 call sites updated to pass context.
2026-04-03 10:29:53 -07:00
Broque Thomas
30d5f76e3d Fix album artist splitting on collab albums and artist name changes
Feed collab mode album-level artists instead of per-track artists so
$albumartist and the album_artist tag are consistent across all tracks
in an album. Fixes media servers (Navidrome/Jellyfin/Plex) showing one
album split under multiple artist names (e.g. KPOP Demon Hunters).

- _build_final_path_for_track: resolve $albumartist from explicit batch
  context or spotify_album.artists, pass album-level _artists_list to
  collab mode instead of per-track artists
- _extract_spotify_metadata: same album-level artists for album_artist
  tag collab resolution
- Wishlist path: pre-compute per-album artist map so all tracks from
  the same album get the same artist context
- Download worker: propagate album artists array in spotify_album_context
2026-04-03 08:50:39 -07:00
Broque Thomas
8c6b7bc014 Update changelogs with Discogs integration and all recent fixes
- Discogs integration (enrichment, fallback source, search, watchlist, cache)
- Track provenance through transcoding (#245)
- spotify_public discovery fix
- Watchlist cross-provider backfill
- Collectors edition matching
- Mobile responsive styles
2026-04-02 21:48:53 -07:00
Broque Thomas
1748a0ebd6 Fix test connection and disconnect messages for Discogs source
- Test connection now shows "Discogs connection successful!" when
  Discogs is the active fallback (was showing "iTunes")
- Spotify disconnect message shows correct fallback name
2026-04-02 21:44:25 -07:00
Broque Thomas
55f7e174d8 Fix all watchlist Discogs gaps
- Add discogs_artist_id to ALL watchlist WHERE clauses + bind params
- Fix artistPrimaryId to include discogs_artist_id
- Fix View Discography — add Discogs source branch
- Fix openWatchlistArtistDetailView — destructure discogs_artist_id
  from response (was causing ReferenceError)
- Discogs source/provider badges + CSS + image fetch + ID resolution
2026-04-02 21:33:41 -07:00
Broque Thomas
f51a8a9ee9 Add Discogs to watchlist UI — badges, provider section, config endpoint
- Watchlist artist list: discogs_artist_id in API response
- Watchlist source badges: Discogs badge on artist cards
- Watchlist config modal: discogs_artist_id in SQL query, WHERE clause,
  response, and linked provider section with badge
- CSS for watchlist-source-discogs and watchlist-provider-badge.discogs
2026-04-02 21:09:11 -07:00
Broque Thomas
930ffbd085 Add Discogs badge to all artist views — fix missing data in queries
- Add discogs_id to library artists SQL SELECT (was missing)
- Add discogs_id to artist detail discography SQL SELECT and service
  IDs loop — fixes hero badges not showing Discogs
- DISCOGS_LOGO_URL constant, badge in library cards, hero, enhanced view
- Match status chip and manual match support for Discogs
2026-04-02 20:03:01 -07:00
Broque Thomas
7ea5eb2c06 Fix Discogs completion: lazy track counts, edition matching, type reclassify
- Fetch real track count from source during completion check when
  total_tracks is 0 (Discogs masters) — one API call per album, runs
  during existing per-album ownership check phase
- Reclassify album cards to single/EP when track count reveals 1-3/4-6
  tracks — updates type label and data attribute in place
- Add collectors edition to album title variation patterns for matching
  "Damn" against "DAMN. COLLECTORS EDITION." in library
- Hide 0/0 fraction when expected_tracks is 0, show proper count when
  fetched
2026-04-02 19:02:23 -07:00
Broque Thomas
69a09826f4 Fix Discogs album lookups — try master ID before release ID
- get_album and get_album_tracks now try /masters/{id} first, fall back
  to /releases/{id} — artist discography returns master IDs which are
  in a different namespace than release IDs
- Fixes wrong album showing in download modal (master ID 3664443 for
  GNX was hitting /releases/3664443 which is a different album)
- Add Discogs source override to all 6 artist/album/track endpoints
- Add discogs_id to _resolve_db_album_id lookup
2026-04-02 18:35:20 -07:00
Broque Thomas
cc95cfcdf2 Wire Discogs as fully featured fallback metadata source
- SpotifyClient: add _discogs lazy-load property, route _fallback to
  DiscogsClient when configured (requires token, falls back to iTunes)
- web_server: _get_metadata_fallback_client returns DiscogsClient when
  selected and token present
- Enhanced search: Discogs added as source tab with NDJSON streaming,
  only available when token configured
- Alternate sources list includes Discogs when token is set
- Frontend: source labels, tab styling, fetch list all include Discogs
- Consistent with iTunes/Deezer pattern — same interfaces, same routing
2026-04-02 17:32:28 -07:00
Broque Thomas
b68aa09469 Add Discogs enrichment worker with full metadata extraction
- New core/discogs_worker.py — background worker enriching artists and
  albums with Discogs metadata following AudioDBWorker pattern exactly
- Artist enrichment: discogs_id, bio, members, URLs, image backfill,
  genre backfill, summary backfill from bio
- Album enrichment: discogs_id, genres, styles (400+ taxonomy), label,
  catalog number, country, community rating, image backfill
- DB migration: discogs columns on artists (id, match_status, bio,
  members, urls) and albums (id, match_status, genres, styles, label,
  catno, country, rating, rating_count)
- Worker initialization with pause/resume persistence
- Status/pause/resume API endpoints
- Integrated into enrichment status system, rate monitor, auto-pause
  during downloads/scans, WebSocket status emission
2026-04-02 16:57:18 -07:00
Broque Thomas
44a8be4469 Add Discogs to Settings connections and rate monitor
- New Discogs section on Settings → Connections with personal token input
- Discogs added as fallback metadata source option alongside iTunes/Deezer
- Token saved to discogs.token config key
- Discogs added to API rate monitor gauges (60/min with auth)
- Help text links to discogs.com/settings/developers for token generation
2026-04-02 16:25:23 -07:00
Broque Thomas
5d8f3bcaec Store original audio details in track provenance (#245)
- Add bit_depth, sample_rate, bitrate columns to track_downloads table
- Read audio info from file via Mutagen when recording provenance
- Source Info popover shows "Audio: 24-bit · 96.0kHz · 2304kbps"
- These values are captured from the original file before transcoding,
  so users can see the original specs even after Blasphemy Mode converts
  FLAC to lossy format
2026-04-02 15:16:49 -07:00
Broque Thomas
c7d6cc21e8 Preserve track provenance through lossy transcoding (#245)
- Update provenance file_path when Blasphemy Mode deletes the original
  FLAC and replaces it with a lossy copy — provenance now points to
  the transcoded file instead of the deleted original
- New update_provenance_file_path() database method
- Non-blocking: wrapped in try/except, never interrupts transcode flow
- Downsample (hi-res → CD quality) is unaffected — replaces in-place
  with same filename, provenance stays valid
2026-04-02 15:14:27 -07:00
Broque Thomas
c67ca40b08 Fix spotify_public playlist refresh overwriting discovery data
- When Spotify is authenticated, spotify_public playlists now use the
  full API instead of the embed scraper — auto-discovers with album art,
  consistent with regular spotify playlists
- When using scraper fallback, no longer sets extra_data on tracks —
  lets preservation code keep existing discovery data instead of
  overwriting discovered=true with discovered=false on every refresh
- Consistent with Tidal/YouTube/Deezer which never set extra_data
- Fixes Discover Weekly showing "not discovered" after overnight refresh
2026-04-02 15:11:07 -07:00
Broque Thomas
bec81cfd8d Add webhook POST then-action for automation engine
- New 'webhook' then-action: sends HTTP POST with JSON payload to any
  user-configured URL (Gotify, Home Assistant, Slack, n8n, etc.)
- Config: URL, optional custom headers (Key: Value per line with
  variable substitution), optional custom message
- Payload includes all event variables as JSON fields
- 15s timeout, errors on 400+ status codes
- Follows exact same pattern as Discord/Pushbullet/Telegram handlers
- Frontend: config fields, config reader, icon, help docs
- Updated changelogs with webhook, M3U fix, orchestrator hardening
2026-04-02 12:22:07 -07:00
Broque Thomas
22552032e2 Harden download orchestrator and surface init failures in debug info
- Each of the 6 download clients initializes independently via
  _safe_init() — one failing client no longer kills the orchestrator
- All methods guarded against None clients with appropriate fallbacks
- Init failures logged at startup and tracked in _init_failures list
- Copy Debug Info shows "Download Client Failures" section when any
  client failed to initialize, or "ALL" if orchestrator itself is dead
2026-04-02 11:40:11 -07:00
Broque Thomas
d5bb0dfaa1 Combine enrichment pills into unified rate monitor cards, debounce idle
- Merge enrichment worker status into rate monitor WebSocket payload
- Hide old enrichment pills — rate monitor cards now show: service name,
  worker status badge, arc gauge, calls/min, 1h/24h counts, budget bar
- Debounce idle detection with 5s grace period — prevents status
  flickering between Running and Idle on every worker cycle
- Responsive grid layout with richer card design
2026-04-02 11:17:04 -07:00
Broque Thomas
93bde811ed Update changelogs with all new features and fixes since last update
- API Rate Monitor, configurable concurrent downloads, streaming search,
  discovery blacklist, Spotify pagination fix, import automation fix,
  track source-info 404 fix, clear match, Tidal auth fix, debug info
2026-04-02 11:00:48 -07:00
Broque Thomas
c0b55e64b4 Fix Tidal auth crash when download orchestrator not initialized
- Add _get_tidal_download_client() helper that checks for None
  soulseek_client before accessing .tidal attribute
- All 3 Tidal download auth endpoints use the helper
- Clear error messages: "Download orchestrator not initialized" or
  "Tidal download client not available" instead of cryptic
  "'NoneType' object has no attribute 'tidal'"
2026-04-02 10:57:53 -07:00
Broque Thomas
0c5bfcae1f Add API rate and Spotify rate limit info to Copy Debug Info
- Backend: include api_rates (per-service calls/min + Spotify endpoints)
  and spotify_rate_limit (active, remaining, trigger endpoint) in debug-info
- Frontend: format API rates table with service name, cpm, limit, percentage,
  and Spotify endpoint breakdown. Show bold warning block when rate limited
  with trigger endpoint, remaining time, and retry-after value
2026-04-02 10:53:09 -07:00
Broque Thomas
559b89353f Add API Rate Monitor dashboard with real-time speedometer gauges
- New core/api_call_tracker.py — centralized tracker with rolling 60s
  timestamps (speedometer) and 24h minute-bucketed history (charts)
- Instrument all 9 service client rate_limited decorators to record
  actual API calls with per-endpoint tracking for Spotify
- 1-second WebSocket push loop for real-time gauge updates
- Modern radial arc gauges with service brand colors, glowing active
  arc, endpoint dot, 0/max scale labels, smooth CSS transitions
- Click any gauge to open detail modal with 24h call history chart
  (Canvas 2D, HiDPI, gradient fill, grid lines, danger zone band)
- Spotify modal shows per-endpoint history lines with color legend
  and live per-endpoint breakdown bars
- Rate limited state indicator — blinking red badge with countdown
  timer appears on gauge card when Spotify ban is active
- REST endpoint GET /api/rate-monitor/history/<service> for chart data
- Responsive grid layout (5 cols desktop, 3 tablet, 2 phone)
2026-04-02 10:46:43 -07:00
Broque Thomas
9bcff9b43d Add configurable concurrent downloads setting
- New "Concurrent Downloads" dropdown on Settings page (1-10, default 3)
- Saved to download_source.max_concurrent config key
- All 6 batch creation sites use configured value instead of hardcoded 3
- Soulseek-only album downloads still use 1 worker (source reuse per user)
- Hybrid/YouTube/Tidal/Qobuz/Deezer albums use full configured concurrency
2026-04-02 09:26:03 -07:00
Broque Thomas
1bb59f3c24 Stream enhanced search source results as NDJSON for progressive rendering
- Source endpoint now streams artists/albums/tracks as separate NDJSON
  lines as each search type completes — iTunes users see artists in ~3s
  instead of waiting 9+ seconds for all 3 rate-limited calls to finish
- Enhanced search _fetchAlternateSource reads stream with ReadableStream
  reader, merges each chunk into source data, re-renders tabs immediately
- Global search uses same streaming pattern via _gsFetchSourceStream
- No data loss: streamed data merges incrementally, primary response
  preserves already-received alternate source data
2026-04-02 08:48:19 -07:00
Broque Thomas
4786dc21ec Route import through automation engine and show per-track progress
- Remove direct request_scan() calls from album and singles import —
  emit batch_complete through automation engine instead, matching the
  same chain as download batches (scan → DB update)
- Show current track name in import queue status display instead of
  just processed/total count
2026-04-02 08:34:01 -07:00
Broque Thomas
68b4aace68 Fix track source-info/redownload 404 and add clear-match (#237, #236)
- Change <int:track_id> to <track_id> on 5 library track endpoints —
  Jellyfin uses GUID strings, int converter rejected them with 404 (#237)
- Add PUT /api/library/clear-match endpoint — sets service ID to NULL
  and match status to not_found, allowing users to undo wrong matches (#236)
- Add "Clear Match" button in the manual match modal for all services
- Add bottom padding to .page to prevent floating buttons (bell, help)
  from overlapping track action buttons at page bottom (#237)
2026-04-02 07:56:42 -07:00
Broque Thomas
b194e1e15b Add discovery artist blacklist — block artists from all discovery playlists
- New discovery_artist_blacklist table with NOCASE name matching
- Filter blacklisted artists from all 6 discovery pool queries, hero
  endpoint, and recent releases via SQL subquery and Python set check
- Name-based filtering means one block covers all sources (Spotify/iTunes/Deezer)
- Hover any discovery track row → ✕ button to quick-block that artist
- 🚫 button on Discover hero opens management modal with search-to-add
  (powered by enhanced search) and list of blocked artists with unblock
- CRUD API: GET/POST/DELETE /api/discover/artist-blacklist
- Updated changelogs
2026-04-01 23:35:56 -07:00
Broque Thomas
7acf7a7d80 Expose MusicBrainz cache in UI — browse, clear, and unified health display
- Add MusicBrainz to Cache Browser: stats pill, source filter, dedicated
  browse endpoint, cards with matched/failed status indicators
- Add Clear MusicBrainz and Clear Failed MB Only to cache clear dropdown
- Move MusicBrainz into Cache Health "By Source" bar chart alongside
  Spotify/iTunes/Deezer instead of isolated metric row
- Rename ambiguous "Failed Lookups" to "Failed MB Lookups" in summary cards
- Add browse-musicbrainz and clear-musicbrainz API endpoints
- Add musicbrainz_total/musicbrainz_failed to cache stats response
- Add Global Search Bar and MusicBrainz cache to changelogs
2026-04-01 23:12:16 -07:00
Broque Thomas
800727299f Fix library playback from search — album art, double-fire, path resolution
- Strip cloned inline onclick on global search play button swap to prevent
  simultaneous stream search + library play
- Include album thumb_url in library-check response and resolve relative
  Plex paths to full URLs with base URL + token
- Pass album art through to playLibraryTrack from both global and enhanced
  search library check handlers
- Add Plex music library locations as candidate dirs in _resolve_library_file_path
- Remove debug console.log from _gsDeactivate
2026-04-01 22:56:06 -07:00
Broque Thomas
0f0ec3acb8 Bump version to 2.2, update changelogs with all new features
Version 2.2 includes: Wing It mode, Server Playlist Manager, Track
Redownload with source info, redesigned notifications, Spotify API
caching improvements, hybrid download fix, discovery progress fix,
YouTube Topic suffix fix, CAA art toggle, Genius search improvements,
auto-pause enrichment during downloads, download blacklist system,
and download provenance tracking.

Updated: SOULSYNC_VERSION, version button, helper.js WHATS_NEW key,
docker-publish.yml default version.
2026-04-01 20:46:42 -07:00
Broque Thomas
9556fc9b5c Add Wing It mode — download or sync without metadata discovery
Wing It bypasses Spotify/iTunes/Deezer matching and uses raw track
names directly. User chooses Download or Sync from a choice dialog.

Download: opens Download Missing modal with force-download-all
pre-checked. wing_it flag skips wishlist for failed tracks.

Sync: new POST /api/wing-it/sync endpoint runs _run_sync_task with
raw track dicts. Live inline sync status display on the LB card
using the same progress elements as normal sync. Unmatched tracks
skip wishlist via _skip_wishlist flag on sync_service.

Button in three places:
- Next to "Start Discovery" in all discovery modals (fresh phase)
- Next to "Download Missing"/"Sync" after discovery (discovered phase)
- Next to "Download" on ListenBrainz cards (Discover page)

Fixed force-download toggle ID, sync progress field names
(total_tracks/matched_tracks not total/matched). All changes
purely additive — normal flows unaffected.
2026-04-01 19:16:51 -07:00
Broque Thomas
119840fa5d Redesign toast notification system with bell button and history panel
Complete replacement of the old bottom-center stacking toast system:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Each source gets its own column with results grouped and sorted by
confidence. Visual confidence bars, format badges, and source-specific
metadata (Soulseek username/slots). Best overall match auto-selected.
2026-04-01 13:53:57 -07:00