Duplicate finding detail now shows each version as clickable — user
can choose which to keep instead of relying on auto-selection. Added
track_number as tiebreaker in auto-pick (higher track number wins
over 01, catching leftover duplicates from the playlist sync track
number bug). Track number displayed in the detail view for clarity.
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).
Sidebar media player now shows the SoulSync logo instead of a broken
image icon when no album art is available or when no track is playing.
Default src, onerror fallback, and clear-player paths all use
/static/trans2.png.
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.
Sync rehydration: after loading Deezer ARL playlists, checks each
for active syncs via /api/sync/status and re-attaches polling with
live card updates. Download rehydration: rehydrateModal now handles
deezer_arl_ playlist IDs, and openDownloadMissingModal routes cache
misses to the correct ARL endpoint. Fix All now prompts for dead
file action.
Album data caching: get_playlist_tracks now checks the metadata
cache before fetching album release dates from the Deezer API.
Cache hits are instant, misses are fetched and stored for future
use across all playlists. Import fixed from core.metadata_cache
instead of web_server to avoid circular dependency.
Dead file fix now prompts with two options: Re-download (existing
behavior — adds to wishlist + deletes DB entry) or Remove from DB
(just deletes the dead track record without re-downloading). Works
for both single and bulk fix. Solves the issue where dismissing
dead files didn't remove the underlying track record, causing them
to reappear on every scan.
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.
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.
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.
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.
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.
ARL field now appears on both Connections tab (under Deezer OAuth)
and Downloads tab. Both fields are populated from the same config
key and synced bidirectionally via input listeners. Editing either
field instantly updates the other.
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.
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.
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.
SQLite CURRENT_TIMESTAMP stores UTC but the format lacks a timezone
marker. JavaScript parsed it as local time, causing future-dated
timestamps and negative diffs that always fell into 'Just now'.
Normalize by appending 'Z' to mark as UTC before parsing.
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.
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.
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".
Visual overhaul of the API Configuration section: each service frame
is now a collapsible accordion with brand-colored dots, chevron
indicators, and smooth expand/collapse animations. Includes an
Expand All / Collapse All toggle. No functional changes — all element
IDs, save/load logic, and tab switching preserved.
- 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)
Reversed the flow: user now sees "Move to Staging" vs "Delete" choice
first, instead of the scary "permanently delete N files" dialog. Staging
gets a friendly confirmation. Delete ≤50 gets standard confirm. Delete
>50 still requires witness-me safety gate. Updated prompt text to
clarify staging is safe and reversible. (#252)
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).
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
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.
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
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
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.
- 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.
Full-width section with three mode cards: Watchlist (existing), Genres
(placeholder), and Explorer (placeholder). Dark gradient background
with purple/blue ambient glow, animated dot grid overlay, and polished
card hover effects. Replaces the old small hero button.
Responsive: 3-column desktop, 1-column mobile.
Toolbar redesigned: three-section layout (back+brand / centered search
with icon / tool buttons). Brand icon in accent purple. Zoom buttons in
compact pill group. Tool labels visible on desktop, icon-only on mobile.
Added keyboard shortcuts modal (keyboard icon button): lists all 10
shortcuts with styled kbd elements.
Fixes:
- Mouse wheel zoom min matched to button zoom min (0.02)
- Right-click no longer triggers left-click info modal (button filter)
- Removed external "Open on Spotify" link from context menu
- Search results dropdown centered under search bar
- Keyboard: Escape close, +/- zoom, F fit, S search, H toggle similar
- Right-click context menu: Artist Info, View Discography, Watchlist,
Open on Spotify — glass-style with auto-close
- Filter toggle button + H key: hide/show similar artists for clean
watchlist-only overview
- Removed node dragging (caused visual desync with offscreen buffer)
- Better loading progress text
- Cleanup: keyboard handler removed on close, context menu hidden
- 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
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)
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)
$discnum was functional but missing from the settings hint text,
template validation whitelist, and docs page. Users would see an
"invalid variable" error when trying to use it.
Also fixes wishlist auto-processing toast spam — was firing every 2s
instead of once per auto-processing run.
Explore button moved to its own row below playlist cards with gradient,
search icon, and "Explore Selected Playlist" label. Impossible to miss.
Mode toggle redesigned as pill segmented control with grid/list icons.
Action bar buttons get inline SVG icons (checkmark, square, heart).
Primary buttons use gradient + glow shadow treatment.
Build hint shows "Select a playlist above, then explore" → updates to
"Ready: [name]" when playlist selected. Discovery poller refreshes
cards every 5s while active. Button re-enables as "Open" after modal
launch so user can reopen closed discovery modal.
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.
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.
- 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
- Add discogs_id to manually constructed artist_data dict in
get_library_artists (was in SQL but not in response dict)
- Add Discogs to enrichment coverage circles on artist detail page
- Add Discogs to enhanced artist/album ID badges and match status chips
- All badge locations verified: library cards, artist hero, enhanced view
- 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
- Add _extract_discogs_fields to metadata cache — handles Discogs field
names (title vs name, images array, Artist - Title format)
- Worker uses _fetch_and_cache_artist/_fetch_and_cache_album helpers
that cache raw data while returning it for enrichment
- All search/lookup methods cache results for repeat queries
- Cache browser: Discogs stat pill, source filter, clear button, badge
- Fixes albums showing as 'Unknown' and artists missing images in cache
- Cards reclassified from album to single/EP (via lazy track count)
now physically move from albums-grid to singles-grid
- Singles section auto-shows when cards move into it
- Add collectors edition to album title variation patterns — fixes
"Damn" not matching "DAMN. COLLECTORS EDITION." in library
- Both base-title-to-edition and edition-to-base variations now include
collectors edition alongside deluxe/platinum/special
- 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
- Fix source name mapping so sidebar/dashboard shows 'Discogs' instead
of falling through to 'iTunes'
- Fix album type detection: parse format string from artist releases
endpoint (e.g. "File, FLAC, Single, 320") to correctly identify
singles, EPs, albums, compilations — was defaulting everything to
'single' because track count was 0
- Remove fake track search that returned albums as tracks — Discogs
has no track-level search API, so tracks section is empty (honest)
- Track data available via album tracklists instead
- 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
- Circular button with "dc" logo text, matching exact pattern of
AudioDB/Deezer/Spotify/iTunes/Last.fm/Genius/Tidal/Qobuz buttons
- Spinner animation when active, dimmed when paused, green when complete
- Hover tooltip showing status, current item, and progress stats
- Click to toggle pause/resume with config persistence
- WebSocket status handler updates button state in real-time