Commit graph

624 commits

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

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

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

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

Same showToast(message, type, helpSection) signature — all 842
callers unchanged. Deduplication preserved. Updated version modal
and helper What's New.
2026-04-01 16:41:41 -07:00
Broque Thomas
89b5178838 Fix Download Discography on library page
Three issues fixed:
1. Stale discography data from a previous Artists page search was used
   instead of fetching for the current library artist. Now detects
   library page context and forces fresh fetch when names differ.
2. Enhanced view may not be loaded, so metadata IDs (spotify/itunes/
   deezer) are fetched from the enhanced endpoint on demand.
3. Fixed undefined spotifyId reference — now uses properly scoped
   metadataArtistId variable.

Falls back to name-based search when no metadata IDs are available.
Better error message directs users to Artists page as alternative.
2026-04-01 16:26:25 -07:00
Broque Thomas
77e4671236 Fix Download Discography button on library artist page
The button called openDiscographyModal() which expected discography
data in artistsPageState — but the library page never populated it.
Now fetches discography on-demand from /api/artist/<id>/discography
when called from the library page, using the artist's DB ID and name.
2026-04-01 15:57:26 -07:00
Broque Thomas
b4ed200633 Filter sync history to only show playlist syncs, not album downloads
Dashboard Recent Syncs and Sync page history were showing album
downloads, wishlist processing, and redownloads alongside actual
playlist syncs. Now filters to only show entries with sync_type
'playlist' (or no type for legacy entries).
2026-04-01 15:41:28 -07:00
Broque Thomas
27aca53339 Add blacklist viewer on Settings tools page
New tool card shows blocked source count. "View Blacklist" opens a
modal listing all blacklisted sources with track name, filename,
username, service icon, and time ago. Each entry has a remove button
to unblock. Empty state explains how to blacklist from Source Info.
2026-04-01 15:35:07 -07:00
Broque Thomas
04bf2c5c4f Fix redownload progress bar visibility
Bar was using var(--accent) which can be dark/invisible against the
modal background. Now uses bright green gradient with glow shadow.
Also thicker (8px) and queries DOM fresh each poll tick to prevent
stale references.
2026-04-01 15:19:29 -07:00
Broque Thomas
19538233fd Group track action buttons (info, redownload, delete) into single cell
Three separate table cells with fixed widths replaced by one compact
cell with a flex group. Buttons are 24px each, 2px gap, fade in on
row hover. Removes ~70px of wasted horizontal space per track row.
2026-04-01 15:12:28 -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
76a76c206e Make redownload modal buttons sticky at bottom
Step 1 and Step 2 action buttons (Cancel, Search/Download) now render
in a sticky footer outside the scrollable body — always visible
regardless of how many results are shown. Footer has backdrop blur
and top border separator matching the modal glass theme.
2026-04-01 14:01:51 -07:00
Broque Thomas
0fd6be3239 Stream download source results progressively via NDJSON
Step 2 of the redownload modal now streams results as each download
source responds instead of waiting for all sources to finish. Tidal/
YouTube/Qobuz columns appear instantly while Soulseek searches.

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

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

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

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

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

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

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

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

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

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

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

Also handles edge case where user manually resumes a worker during
downloads — adds to override set so the loop doesn't re-pause it.
Override resets when downloads finish so next download session re-pauses.
2026-04-01 09:09:33 -07:00
Broque Thomas
982ca77501 Make Cover Art Archive album art opt-in instead of default (#232)
CAA art can be higher resolution (1200x1200+) but quality is
inconsistent — some releases have cellophane-wrapped photos or
low-quality scans. Spotify/iTunes/Deezer art is lower res (640x640)
but consistently clean and official.

New toggle: Settings → Post-Processing → "Use MusicBrainz Cover Art
Archive for album art" (off by default). Applies to both embedded
art and cover.jpg downloads.
2026-04-01 07:59:40 -07:00
Broque Thomas
34c8b1bb50 Add Server Playlist Manager with dual-column compare editor
New "Server Playlists" tab (default on Sync page) lets users compare
mirrored playlists against their media server and fix match issues.

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

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

Features:
- Staggered card entrance animation, delete button on hover
- Filter bar: All/Matched/Unmatched/Downloaded
- Color-coded confidence badges (green/amber/red)
- Unmatched tracks show "→ Wishlist" status
- 32px album art thumbnails per track row
- Auto-refreshes every 30 seconds on dashboard
- Falls back gracefully for old syncs without track_results
2026-03-31 15:26:46 -07:00
Broque Thomas
cfe2ab7dec Add toggle to disable auto-clearing slskd search history
Users who keep manual searches in slskd as reminders were losing
them when SoulSync auto-cleaned at 200+ entries. New toggle in
Settings → Downloads → Soulseek: "Auto-clear slskd search history"
(on by default, preserving current behavior). When disabled, both
the hourly cleanup automation and the full cleanup step skip the
search history maintenance.
2026-03-31 13:03:37 -07:00
Broque Thomas
0bc6abd683 Allow duplicate tracks across albums with settings toggle
Same song from different albums was blocked from entering the
wishlist by a name+artist dedup check. Added toggle in Settings →
Library → File Organization: "Allow duplicate tracks across albums"
(on by default). When enabled, the dedup is skipped — different
album versions of the same song can coexist in the wishlist for
complete discography downloads. The UNIQUE constraint on track ID
still prevents the exact same track from being added twice.
2026-03-31 12:16:57 -07:00
Broque Thomas
8369109ea0 Fix crypto copy buttons and correct ETH address
ETH address was wrong in the support modal. Also fixed clipboard
copy failing on HTTP (Docker) — navigator.clipboard requires HTTPS.
Added textarea fallback for insecure contexts, and shows the address
in a toast as last resort if both methods fail.
2026-03-31 11:13:09 -07:00
Broque Thomas
9189378364 Fetch Tidal tracks on click with loading overlay before discovery modal
With metadata-only listing, tracks aren't pre-loaded. Now shows
loading overlay while fetching tracks via /api/tidal/playlist/<id>,
then dismisses overlay and opens discovery modal. Overlay is hidden
at every exit point (error, empty, success) to prevent it from
blocking the modal.
2026-03-31 11:03:13 -07:00
Broque Thomas
8bb2729dc3 Fix Tidal playlists showing 0 tracks and broken auto-mirror
The metadata-only optimization broke two things:
1. Cards showed 0 tracks because tracks were no longer in the listing
2. Auto-mirror skipped all playlists because tracks array was empty

Fix: cards render instantly from metadata, then tracks are fetched
per-playlist in the background via /api/tidal/playlist/<id>. As each
playlist's tracks arrive, the card count updates and the playlist
is auto-mirrored. Also tried multiple V2 attribute names for track
count (numberOfTracks, numberOfItems, etc.) and fixed the card DOM
selector for count updates.
2026-03-31 09:33:52 -07:00
Broque Thomas
2990b571b4 Add Clear Cache & Use Fallback button to Spotify settings
Always-visible button in Spotify API section that clears the OAuth
token cache, pauses enrichment, and switches to the configured
fallback metadata source. Also fixed the dashboard service card to
show the actual active source name (Spotify/iTunes/Deezer) instead
of always showing "Spotify" with an amber fallback indicator.
2026-03-30 17:20:33 -07:00
Broque Thomas
69346ec313 Add Playlist Explorer — visual discovery tree for expanding playlists
New dedicated Explorer page with interactive node graph visualization.
Users select a mirrored playlist, choose Albums or Discographies mode,
and the app builds a branching tree: playlist root → artist nodes →
album nodes → track nodes. Supports all metadata sources (Spotify,
iTunes, Deezer) with source-aware discovery cache integration.

Features:
- Streaming NDJSON builds tree progressively as artist data arrives
- Circular artist nodes with photos, rounded album nodes with art
- SVG bezier connections that draw in on completion, fade on hover
- Click artist to expand albums, double-click album for track listing
- Single-click albums to select, Select All/Deselect for bulk ops
- Wishlist confirmation modal with per-album progress (NDJSON streaming)
- Artist nodes glow when any of their albums are selected
- Playlist picker with source tabs, discovery % gate (50% minimum)
- Zoom (scroll/pinch/buttons), pan (right/middle-drag), fit-to-view
- Metadata cache for discographies and album track listings
- Owned album detection from library database
- Fallback track-name matching when album names are missing
2026-03-30 17:13:30 -07:00
Broque Thomas
c5652b0d4b Add API call counts and Spotify budget to dashboard service chips
Enrichment chips now show live activity: 24h call count for all
services and daily budget usage (used/3,000) with gradient progress
bar for Spotify. Tracking is centralized in _get_enrichment_status
using cumulative stat diffs over a rolling deque — no worker files
modified. Added section header, "Configure →" label for unconfigured
services, and full 1h/24h breakdown in tooltips.
2026-03-30 13:20:22 -07:00
Broque Thomas
32adc66fe3 Show all services on dashboard with click-to-configure (#219)
Dashboard now displays all enrichment services as live-status chips
below the core service cards. Each chip shows Running, Idle, Paused,
Stopped, or Not Configured state with color-coded left border accents.
Unconfigured services appear dimmed with dashed borders — clicking any
configurable chip navigates to Settings → Connections and scrolls to
the relevant service section.

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

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

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

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

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

If nothing is checked, downloads the full category (same as before).
Other callers of openDownloadMissingWishlistModal are unaffected —
the new selectedTrackIds parameter defaults to null.
2026-03-29 21:43:48 -07:00
Broque Thomas
3c47281ce5 Redesign Watch All Unwatched as polished preview modal
Replaces the fire-and-forget button with a premium modal that shows
exactly which artists will be added before confirming. Features:

- Glassmorphic modal with stat cards, two-column artist grid, search
  filter, collapsible ineligible section, and loading spinner
- Source-aware filtering: only shows artists with the active source's
  ID (Spotify/iTunes/Deezer) as eligible
- Frontend and backend both paginate at 400 to avoid SQLite variable
  limit (SQLITE_MAX_VARIABLE_NUMBER=999) that silently broke queries
  above ~500 artists
- Backend source detection aligned with frontend — uses only the
  active source's ID, falls back to configured metadata source
2026-03-29 18:12:42 -07:00
Broque Thomas
98463e5d43 Fix library maintenance path fixes failing silently (#207)
fix_finding() was using a potentially stale transfer_folder that was
only refreshed during scheduled job runs, not on manual fix attempts.
Now re-reads the transfer path from config before each fix, matching
the same logic used by _run_next_job().

Also surfaces fix failure reasons to the user — bulk fix now logs each
failure with finding ID and error, and the frontend toast shows the
actual error message instead of just "X failed".
2026-03-29 16:25:36 -07:00
Broque Thomas
213736e168 Fix manual match storing iTunes/Deezer IDs in Spotify ID columns (#213)
When Spotify falls back to iTunes/Deezer (auth failure, rate limit, API
error), the manual match modals were storing numeric fallback IDs in
spotify_*_id columns, breaking Spotify links and metadata fetching.

Fix detects the actual provider by inspecting result IDs (alphanumeric =
Spotify, numeric = fallback) rather than checking auth status, which can
be misleading during rate limit bans. The frontend now passes the real
provider to the storage endpoint so IDs land in the correct column.
2026-03-29 16:04:22 -07:00
Broque Thomas
b5b03a2b86 Add Download Discography feature on artist detail page
New "Download Discography" button in artist hero section opens a modal
showing the full catalog — albums, EPs, and singles — with filter
toggles, select/deselect all, and per-album owned/missing indicators.

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

Backend: NDJSON streaming endpoint POST /api/artist/<id>/download-discography
- Fetches tracks per album via active metadata client
- Adds to wishlist with dedup (no slow fuzzy matching)
- Streams one JSON line per album as it completes
- Works on both Artists search page and Library artist detail page
2026-03-27 22:15:05 -07:00
Broque Thomas
326bb548ce Add per-artist Sync button on enhanced library view
New "Sync" button in the enhanced view header validates an artist's
library entries against files on disk. Removes stale tracks (missing
files), cleans empty albums, and updates track counts.

- POST /api/library/artist/<id>/sync endpoint
- Checks each track's file_path via _resolve_library_file_path
- Empty album cleanup checks ALL tracks (not just this artist's)
  to avoid deleting albums shared with other artists
- Toast shows results: stale removed, albums cleaned, or "All files
  verified" if everything checks out
- Auto-refreshes enhanced view when changes are made
2026-03-27 15:36:08 -07:00
Broque Thomas
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
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
835ddcdbd5 Fall back to stream source when library file not found on disk
When clicking play on an "In Library" track, if the file can't be
resolved on disk (e.g., media server path not accessible from
SoulSync), silently falls back to streaming via the configured
stream source instead of showing an error.
2026-03-26 19:48:03 -07:00
Broque Thomas
9fcbd323a5 Add stream source setting, auto-update yt-dlp on container start
Stream source:
- New setting in Settings → Downloads: "Stream / Preview Source"
- Options: YouTube (instant, default) or Active Download Source
- YouTube streams require no auth and are instant
- If active source is Soulseek, automatically falls back to YouTube
- Uses direct client search (bypasses orchestrator's download mode)
- Config key: download_source.stream_source

Docker:
- entrypoint.sh now runs pip install -U yt-dlp on every container
  start, so Docker users always have the latest yt-dlp without
  rebuilding the image
2026-03-26 19:27:35 -07:00
Broque Thomas
8e41feaade Fix Navidrome playlist sync truncation, clarify discovery label
Navidrome fix:
- createPlaylist and other write operations now use POST instead of
  GET. Large playlists (161+ tracks) exceeded URL length limits when
  all songId params were in the query string, causing silent truncation
  (e.g., only 6 of 161 tracks added). POST sends params as form body
  with no size limit.
- Write operation timeout bumped to 30s (was 10s)
- _WRITE_ENDPOINTS set defines which Subsonic endpoints use POST

UX fix:
- Mirrored playlist cards now show "161/161 discovered on Spotify"
  instead of just "161/161 discovered" — clarifies that discovery
  means metadata matching, not library ownership
2026-03-26 14:15:20 -07:00
Broque Thomas
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
1e9abb588c Add clickable artist name link in download modal hero subtitle
The artist name in album download modals is now a clickable link
that navigates to the Artists page with that artist's discography.
Uses the correct source-specific artist ID from the album data.
Works on enhanced search, artists page, and discover page modals.
Excluded from playlist, wishlist, and default contexts where the
subtitle isn't an artist name.
2026-03-26 08:22:52 -07:00
Broque Thomas
77b6d4927c Add View Discography button to watchlist artist detail overlay
Clicking the button closes the watchlist modal and navigates to the
Artists page with the artist's discography loaded. Uses the correct
source ID based on the active metadata source (Spotify/Deezer/iTunes).
2026-03-26 08:01:31 -07:00
Broque Thomas
f6709c7cc3 Add library ownership badges to enhanced search results
Search results now show "In Library" badges on albums and tracks
that already exist in the user's library. Badges appear with a
staggered fade-in animation after results render (non-blocking).

- Backend: /api/enhanced-search/library-check endpoint builds
  owned album/track sets in 2 queries, O(1) lookups per result
- Frontend: async call after render, 30ms stagger per badge
- Tracks in library get play button rewired for direct playback
  from media server instead of searching download sources
- Fixed enhanced search album card text not visible (info div
  now absolute-positioned with gradient overlay)
- Download manager panel hidden by default for more search space
2026-03-25 19:39:19 -07:00
Broque Thomas
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
89cfea0fe7 Add per-source quality fallback toggle for streaming downloads (#187)
Each streaming source (Tidal, Qobuz, HiFi, Deezer) now has an "Allow
quality fallback" checkbox in Settings. When disabled, the source only
tries the exact quality selected — if unavailable, it skips and lets
the orchestrator try the next source. Default is ON (current behavior).
2026-03-24 11:42:47 -07:00