1. LB Discover page cards now use the dropdown (Download/Sync) instead
of the old single button with full choice dialog
2. Dropdown position auto-flips downward when button is near viewport top
3. Dropdown centered on button instead of right-aligned
4. Sync completion toast now includes playlist name and ⚡ indicator
5. Download modal already shows ⚡ prefix on playlist name as indicator
All changes purely additive — existing flows unaffected.
Live status: updateYouTubeModalSyncProgress was hardcoded to youtube-*
element IDs but each source uses its own prefix (listenbrainz-*, tidal-*,
deezer-*, etc). Now tries all prefixes to find the correct elements.
Fixes Wing It sync progress AND a pre-existing bug where normal LB/Tidal
sync from the modal wouldn't show live progress.
Wing It button added to sync_complete phase so it persists after sync.
Fixed tracks lookup with state.playlist?.tracks fallback. Increased
button size in modal to match other action buttons.
The discoverMetadata (needed for bubble creation) was only set for
playlist IDs starting with discover_lb_, listenbrainz_, or source
SoulSync. Wing It uses wing_it_ prefix which wasn't matched.
Added wing_it_ to the condition so bubbles appear on dashboard
and sidebar during Wing It downloads.
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.
Discovery status polling was skipped when WebSocket was connected
(8 places), assuming socket events would push updates. But no
WebSocket events exist for discovery progress — the table stayed
on "Pending..." forever. Now always polls the status endpoint.
Pre-existing bug, not caused by recent changes.
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.
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.
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.
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).
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.
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.
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.
Artist Radio and Enhance Quality buttons moved from the page header
into the artist hero section (after badges, before genres). Add to
Watchlist stays in the top-right header where it was.
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.
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.
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.
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
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).
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.
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.
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.
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.
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
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
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.
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.
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.
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.
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.
Status text and indicators now use fixed Material Design colors
instead of accent-dependent values — green for running/idle, amber
for paused, red for stopped, dim white for not configured. Readable
regardless of the user's chosen accent color.
normalize_string() was running unidecode on all text, converting
Japanese kanji to Chinese pinyin gibberish (命の灯火 → "tvanimedei").
Now detects CJK characters (kanji, hiragana, katakana, hangul,
fullwidth forms) and skips unidecode for text containing them —
just lowercases instead. Non-CJK text (Latin accents, Cyrillic)
still goes through unidecode normally.
"Believe" was falsely matching "Believe In Me" because SequenceMatcher
gives high scores when the search string is fully contained in the
match. Added a length ratio penalty: when cleaned titles differ in
length by more than 30%, the similarity score is multiplied by the
ratio (min/max length). This crushes prefix/suffix false positives
while leaving exact matches and cleaned variants (remastered, deluxe)
unaffected.
Playlist auto-sync was dropping tracks that failed iTunes/Apple Music
discovery — they never reached the wishlist or download pipeline. Now
undiscovered tracks continue through using available metadata: first
from the spotify_hint (embed scraper data with real Spotify track ID,
name, artists), then from raw playlist fields if a source track ID
exists. Album cover art from the mirrored playlist is included. Only
tracks with no usable ID or name are skipped.
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.
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
Plain (unsynced) lyrics were being saved with .lrc extension despite
having no timestamps, making them invalid for Plex and other players
that expect LRC format. Synced lyrics now write as .lrc, plain lyrics
write as .txt. Both types still get embedded in audio file tags.
Updated all file move/rename operations to handle .txt sidecars
alongside .lrc.
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.
Single path template was missing _artists_list and _itunes_artist_id
context keys, so the collab mode first-artist extraction in
_apply_path_template had nothing to work with — $albumartist resolved
to the full multi-artist string. Added both keys matching the exact
pattern used by album and playlist modes, including the iTunes
spotify_album.external_urls fallback. Updated settings UI hints to
show $albumartist as available for single and playlist templates.
When a user manually matched an artist to a service ID then triggered
enrichment, the worker re-searched by name, failed to find a match,
and overwrote the status back to not_found — despite the ID being
valid. Now both Genius and AudioDB workers check for existing service
IDs before searching by name. If an ID exists (from manual match),
the worker uses it for a direct API lookup to enrich metadata while
preserving the matched status. Added AudioDB lookup-by-ID client
methods for artist, album, and track.
Top-level try/except in do_GET ensures an HTTP response is always sent
— previously, unhandled exceptions caused BaseHTTPRequestHandler to
silently close the connection (ERR_EMPTY_RESPONSE). All callback
logging now uses the app logger instead of print() so output appears
in app.log rather than only Docker stdout. Added health check at / to
verify the callback server is running, and startup now logs the actual
bind address to help diagnose port conflicts.
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.
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.
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.
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
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.
The auth_tidal() endpoint was overriding the user's configured
redirect_uri with one built from request.host. In Docker, request.host
is the container hostname (e.g. "soulsync-webui"), not the external
URL the user configured in settings.
Now checks config_manager for the user's configured redirect_uri first.
Only falls back to request.host dynamic detection if no redirect URI
is configured.