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.
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
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.
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.
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
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".
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.
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
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
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).
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
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
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.
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
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
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.
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.
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).
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
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.
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)
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.
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).
Modal now uses a fixed 600px height from open — results scroll within
a dedicated area instead of growing the modal and pushing inputs up.
This eliminates the layout shift that caused accidental result clicks.
Other fixes:
- Input fields now have labels (Track, Artist)
- Overlay dismiss uses mousedown with stopPropagation to prevent
accidental close when clicking near inputs
- Reduced results from 50 to 20 for faster response
- Clean minimal design matching app style
- Mobile: full-screen modal, stacked inputs with 44px touch targets
- Search bar: stripped heavy purple chrome, minimal dark input style
- Dropdown: inline flow instead of overlay, hides page header when active
- Section labels: flat uppercase text, no bordered glass boxes
- Artist cards: full-bleed photo with gradient overlay and name at bottom
(matches library page style), flexbox wrap layout with fixed dimensions
- Album cards: discover-style dark cards in horizontal scroll on desktop,
wrap to 2-per-row on mobile
- Track rows: clean flat list, subtle hover, smaller cover art
- Source tabs: compact pills with per-source accent colors
- Renamed grid classes (enh-artists-grid, enh-albums-grid, enh-tracks-list)
to avoid collision with generic .artists-grid rule
- Mobile: downloads-main-panel min-width:0 fix for 1190px overflow,
cards use calc(50% - 8px) for 2-per-row fill, touch-friendly targets
- Tidal and Qobuz SVG logos inverted on artist detail hero badges
- New Artist Radio button: clears queue, plays random artist track, enables radio
- Play buttons on Last.fm top tracks (hover reveal, resolves from library)
- Fixed inline JS escaping with data attribute delegation
The enhanced view reorganize modal had a hardcoded default path template
instead of loading the user's saved template from settings. Now fetches
the saved album_path template from /api/settings on modal open.
- Stats page: database storage donut chart with per-table breakdown and total size
- Discover page: 5 new sections mined from metadata cache (zero API calls):
Undiscovered Albums, New In Your Genres, From Your Labels, Deep Cuts, Genre Explorer
- Genre Deep Dive modal: artists (clickable → artist page), popular tracks,
albums with download flow, related genre pills, in-library badges
- All cache queries filtered by active metadata source (Spotify/iTunes/Deezer)
- Stale cache entries (404) gracefully fall back to name+artist resolution
- Album cards show "In Library" badge, artist avatars scaled by prominence
- Stats page: full mobile layout with compact cards, charts, ranked lists
- Artist hero: stacked layout, compact image/name/badges, top tracks below
- Enhanced library: meta header/expanded header stack vertically, track table
collapses action columns into bottom sheet popover on mobile
- Automations: builder sidebar collapses, inputs go full width
- Hydrabase/Issues/Help: responsive stacking and compact layouts
- Fix grid blowout: add min-width:0 to stats grid children and overflow:hidden
Integrates play history data into the discovery algorithm:
- Listening profile: _get_listening_profile() builds user's top artists,
genres, play counts, and listening velocity from the last 30 days
- Artist genre cache: pre-built from local DB for O(1) genre lookups
- Release Radar: +10 genre affinity, +15 artist familiarity, -10 overplay
penalty. Weights rebalanced to 45% recency + 25% popularity + bonuses
- Discovery Weekly: serendipity scoring within tiers — boosts unheard
artists in preferred genres, penalizes overplayed artists
- Recent Albums: adaptive time window (21-60 days) based on listening
velocity — heavy listeners get fresher content, casual listeners more
- New "Because You Listen To" sections: personalized carousels based on
user's top 3 played artists via similar artists + genre fallback
- New endpoint: /api/discover/because-you-listen-to with artist images
- Frontend: BYLT sections with artist photo headers on discover page
- All changes gracefully fall back when no listening data exists
Full stats dashboard that polls Plex/Jellyfin/Navidrome for play
history and presents it with Chart.js visualizations:
Backend:
- ListeningStatsWorker polls active server every 30 min
- listening_history DB table with dedup, play_count/last_played on tracks
- get_play_history() and get_track_play_counts() for all 3 servers
- Pre-computed cache for all time ranges (7d/30d/12m/all) rebuilt each sync
- Single cached endpoint serves all stats data instantly
- Stats query methods: top artists/albums/tracks, timeline, genres, health
Frontend:
- New Stats nav page with glassmorphic container matching dashboard style
- Overview cards (plays, time, artists, albums, tracks) with accent hover
- Listening timeline bar chart (Chart.js)
- Genre breakdown doughnut chart with legend
- Top artists visual bubbles with profile pictures + ranked list
- Top albums and tracks ranked lists with album art
- Library health: format breakdown bar, unplayed count, enrichment coverage
- Recently played timeline with relative timestamps
- Time range pills with instant switching via cache
- Sync Now button with spinner, last synced timestamp
- Clickable artist names navigate to library artist detail
- Last.fm global listeners shown alongside personal play counts
- SoulID badges on matched artists
- Empty state when no data synced yet
- Mobile responsive layout
DB migrations: listening_history table, play_count/last_played columns,
all with idempotent CREATE IF NOT EXISTS / PRAGMA checks.
- Add get_artist_top_tracks to Last.fm client (up to 100 tracks)
- Include lastfm_listeners, lastfm_playcount, lastfm_tags, lastfm_bio,
and soul_id in artist detail API response
- New endpoint: /api/artist/<id>/lastfm-top-tracks for lazy loading
- Hero layout: image (160px) | center (name, badges, genres, bio,
listener/play stats, progress bars) | right card (scrollable top
100 tracks from Last.fm)
- Badges 36px with hover lift, bio in subtle card with Read More
toggle, Last.fm tags merged with existing genres
- Numbers formatted: 1234567 → 1.2M
- Graceful degradation: sections hidden when Last.fm data unavailable
Lossy copy now supports MP3, Opus, and AAC (M4A) codecs with a
configurable dropdown in settings. Each codec uses the appropriate
ffmpeg encoder (libmp3lame/libopus/aac) and Mutagen tag writer
(ID3/Vorbis/MP4). Quality tag, filename substitution, and Blasphemy
Mode file cleanup all work per-codec. Backward compatible — existing
configs default to MP3.
Orphan detector: add normalized tag matching that strips parentheticals
and brackets (feat. X, [FLAC 16bit], etc.) and tries first-artist-only
for comma-separated artists. Prevents false orphan flags for tracks
like "The Mountain (feat. Dennis Hopper...)" that exist in DB as
"The Mountain". All lookups remain O(1) set operations.
Orphan fix: replace auto-delete with user choice prompt. Single Fix
and Fix All both show modal asking "Move to Staging" or "Delete".
Move to Staging relocates file to import staging folder for proper
re-import with metadata matching. Fix action flows through API
endpoint → repair_worker.fix_finding → _fix_orphan_file handler.
Staging path uses docker_resolve_path for container compatibility.
SoulID worker generates deterministic soul IDs for all library entities:
- Artists: hash(name + debut_year) — searches iTunes + Deezer APIs,
verifies correct artist by matching discography against local DB
albums via MusicMatchingEngine, pools years from both sources and
picks the earliest. Falls back to hash(name) if no match found.
- Albums: hash(artist + album)
- Tracks: song ID hash(artist + track) + album ID hash(artist + album + track)
Dashboard button with trans2.png logo, rainbow spinner, hover tooltip.
Worker orb with rainbow effect. SoulSync badge on library artist cards.
DB migration adds soul_id columns with indexes to artists/albums/tracks.
Migration version flag auto-resets artist soul IDs when algorithm changes.
1. Fix filename parser pattern order — "01 - Title" now matched
before "Artist - Title", preventing track numbers being treated
as artist names (e.g., "08" no longer becomes the artist)
2. Tag priority over filename parsing — shared _read_staging_file_metadata()
helper reads title, artist, albumartist, album, track_number, disc_number
from Mutagen tags. Only falls back to filename parsing when BOTH title
AND artist tags are empty. Applied to all 3 staging scan sites.
3. Improved match scoring — rebalanced from title(0.5)+tracknum(0.5) to
title(0.45)+artist(0.15)+tracknum(0.30)+album_bonus(0.10). Files
whose album tag matches the selected album get boosted.
4. Auto-group detection — new /api/import/staging/groups endpoint groups
staging files by album+artist tags. Frontend shows "Auto-Detected
Albums" section with one-click search. Match endpoint accepts
optional file_paths filter to scope matching to a specific group.
- Use correct server request types: 'tracks', 'albums', 'artists',
'artist.albums', 'album.tracks' (were singular, caused timeouts)
- Normalize artists to strings (server may send dicts)
- Use native plugin IDs (iTunes/Spotify) instead of soul_id for
album/artist/track IDs so downstream endpoints can resolve them
- Carry soul_id and plugin_id in external_urls for routing
- Pass plugin param from frontend to server for correct client routing
(iTunes vs Deezer vs Spotify) with isdigit() fallback
- Route source=hydrabase to iTunes client for artist images
- Include external_urls in enhanced search API response
- Reduce WebSocket timeout from 15s to 8s
- Remove stale hydrabase.enabled check, use is_connected() directly
- Add hydrabase to frontend alternate source fetch list
- Normalize Hydrabase artists to strings (server may send dicts),
fixing silent crashes that prevented albums/tracks from appearing
- Add debouncedAutoSaveSettings() to moveHybridSource and toggleHybridSource
- Skip unconfigured sources at search time with is_configured() check
- Add get_source_status() to orchestrator, include in settings API response
- Auto-disable unconfigured sources in UI on settings load
Failed tracks had candidates from the initial search but no way to
retry with a different source. Now clickable like not_found tracks
to open the manual match modal.
- Remove redundant enable checkbox — fallback dropdown is the enable
- Hydrabase option only appears in dropdown when connected
- Connect/disconnect dynamically adds/removes dropdown option
- _is_hydrabase_active checks fallback_source == hydrabase (not config toggle)
- Fallback client returns hydrabase_client when selected, iTunes if disconnected
- Auto-reconnect respects fallback selection for dev_mode handling
- hydrabase added to settings save service list for persistence
- Status shows green Connected on page load when auto-connected
Add Hydrabase section to Settings → Connections with enable toggle,
WebSocket URL, API key, auto-connect, and connect/disconnect button.
_is_hydrabase_active() now checks hydrabase.enabled config in addition
to dev_mode — either path activates it. Default disabled, zero change
for existing users. Dev admin page stays behind dev mode password.
Non-admin: 3-tab layout (Music Services | Server | Scrobbling).
Admin: just ListenBrainz, no tabs (unchanged).
Server tab auto-detects active server (Plex/Jellyfin) and shows
library name dropdowns instead of raw ID inputs. Modal has max-height
with scroll, tab bar with accent underline indicator.
Adds Tidal per-profile OAuth with token storage on profile row.
Auth initiation stores profile_id in PKCE state, callback detects
it and stores encrypted tokens per-profile instead of globally.
Personal settings modal now shows Spotify, Tidal, Server Library,
and ListenBrainz sections for non-admin profiles. Admin sees only
ListenBrainz (unchanged). Server library selection wired into
playlist sync via _apply_profile_library.
Full per-profile support: Spotify (credentials + OAuth + playlists),
Tidal (OAuth + token storage), server library (Plex/Jellyfin/Navidrome),
ListenBrainz (existing). All backwards compatible — upgrading users
see zero change.
New pipelines: Startup Recovery (3 automations), Import Pipeline (3),
Weekly Deep Clean (5), Beatport Fresh (1). Fix deploy calling
nonexistent loadAutomationsPage — now calls loadAutomations so the
list updates immediately after deployment.
All 6 playlist sync endpoints now accept download_complete phase so
users can re-sync after downloading. Added Rediscover button to
discovered and download_complete states (YouTube + Beatport). Added
full button set (Sync, Download Missing, Rediscover) for
download_complete which previously showed no buttons at all.
Replace fixed primary/secondary hybrid dropdowns with an ordered list
of all 5 download sources. Users enable/disable each source and reorder
with up/down arrows to set download priority. Sources are tried in
order until one returns results.
- New hybrid_order config field (backward compat with legacy primary/secondary)
- Download orchestrator loops ordered list with per-source error handling
- Sortable source list UI with icons, toggle switches, priority numbers
- Source-specific settings shown for all enabled hybrid sources
- Seamless migration from legacy 2-source to N-source format
Replace 3-column glassmorphic card wall with centered single-column
tabbed interface. Horizontal pill tab bar (Connections, Downloads,
Library, Appearance, Advanced) with category switching.
- Kill glassmorphic cards, accent gradient bars, and box shadows
- Clean section headers with subtle dividers
- Horizontal setting rows (label left, control right)
- Custom styled select dropdowns with SVG arrow
- Quality Profile moved into Downloads tab (conditionally visible)
- Help text wraps to new line below controls
- Path inputs and template inputs properly styled
- Mobile responsive (rows stack, tab bar scrolls)
- Zero functional changes — all element IDs and JS logic preserved
Search results now show switchable tabs for alternate metadata sources.
Primary source renders immediately, alternate sources load in parallel
and tabs appear progressively as each completes.
- New /api/enhanced-search/source/<name> endpoint for per-source queries
- Source-aware routing via ?source= param on discography, album tracks,
album detail, and artist image endpoints (prevents numeric ID
misrouting between iTunes and Deezer)
- Source override stored on artistsPageState for consistent navigation
- Tabs styled with source brand colors, show result counts
- All additive — users who ignore tabs see zero behavioral change
- Add max_peer_queue setting to skip peers with long queues (soft filter
with fallback to unfiltered if all results removed)
- Add download_timeout setting replacing hardcoded 10-minute limit
- Include quality_score (peer health: upload speed, free slots, queue
length) in result ranking — was calculated but never used in sort key
- New UI controls in Soulseek settings section
When >50% of files are flagged as orphans (likely a DB path mismatch),
findings are marked as warnings with mass_orphan flag. Fixing these
requires typing "witness me" to confirm — prevents nuking an entire
library from a false-positive orphan scan.
- Frontend was concatenating Track and Artist inputs into a single
query string, causing Spotify to return mixed results matching
either word in any field. Now sends track and artist as separate
params; backend builds field-filtered query (track:X artist:Y).
- Result limit was silently capped at 10 in spotify_client.search_tracks
via min(limit, 10). Raised to respect requested limit up to
Spotify's API max of 50.
- iTunes fallback endpoint updated with same field-specific params.
- Legacy ?query= param still supported for backward compatibility.
Fixes#194
- Duplicate `spotify` key in saveSettings() object literal caused
second definition (embed_tags/tags) to silently overwrite the first
(client_id/client_secret/redirect_uri), destroying credentials on
every save. Merged into single key.
- authenticateSpotify() and authenticateTidal() now await saveSettings()
before opening auth window, ensuring credentials are persisted.
- Tidal auth now dynamically sets redirect_uri from request host for
LAN/Docker users and stores it in tidal_oauth_state so the callback
token exchange uses the same URI.
Fixes#191
Replace category-based tag settings (10 toggles) with per-tag controls
grouped by source service in an accordion UI. Each of the 11 service
groups (Spotify, iTunes, MusicBrainz, Deezer, AudioDB, Tidal, Qobuz,
Last.fm, Genius, General) has a master toggle that disables all child
tags, with individual toggles for fine-grained control. ISRC and
copyright fallback chains are now per-source toggleable. Genre merge
contributions from each source are independently controllable. All
tags default to enabled for backward compatibility.
Post-processing now writes all 18 MusicBrainz tags that Picard writes:
Release Group ID, Album Artist ID, Release Track ID, Release Type,
Status, Country, Original Date, Media, Barcode, Catalog #, ASIN,
Script, Total Discs (plus the 5 already supported). One cached API
call per album via get_release with recordings include.
New "Tags to Embed" settings section with 10 category toggles (all
enabled by default): MusicBrainz IDs, Release Info, Source IDs, ISRC,
BPM, Mood & Style, Copyright & Label, Genre Merging, URLs, Quality.
Each shows inline description of what it includes.
New dropdown in Soulseek settings lets users filter out slow peers at
search time (Any/1/2/3/4/5/10 Mbps). Passes minimumPeerUploadSpeed
to slskd API in bytes/sec.
Also fixes quality scoring tiers which were using wrong units — old
thresholds (5000, 1000, 500) treated bytes/sec values as kbps,
making speed scoring effectively meaningless. Now uses correct
bytes/sec thresholds based on real peer data.
Dead file Fix button now adds the track to wishlist for re-download
instead of just removing the DB entry. Builds full wishlist-compatible
track data from DB (artist, album, artwork, IDs) so the download
pipeline can process it like any other wishlist item.
- Fix enrichment progress never updating: remove `continue` that skipped
progress_callback for successful tracks in enrich_chart_tracks
- Split chart/extract into two-step flow: extract raw tracks, then enrich
via polling endpoint with live progress overlay updates
- Move Beatport enrichment cache to persistent metadata cache system
- Fix metadata cache detail modal for Beatport (URL entity_ids with slashes)
- Add per-source Clear dropdown (Spotify/iTunes/Deezer/Beatport) to cache browser
- Remove debug logging from enrichment progress tracking
- Add navigation triggers for Beatport bubbles on sync page load, Beatport
tab switch, and rebuild tab activation (mirroring artist bubble pattern)
- Register download bubbles for Beatport releases (albums, EPs, singles)
which were only created for chart/playlist downloads
- Extend modal close cleanup to handle beatport_release_ prefix
When the maintenance worker flags an incomplete album, users can now
click "Auto-Fill" to automatically locate missing tracks in the library,
move/copy them into the album folder, and apply full metadata enhancement
(MusicBrainz, Deezer, cover art, etc.). Singles are moved; tracks from
multi-track albums are copied. Quality gate prevents filling FLAC albums
with lossy files. Tracks not found in library are added to wishlist with
album context for auto-download.
- Persistent download bubble cards on Beatport page and dashboard,
matching the existing artist/search bubble UX with click-to-reopen,
green checkmark on completion, and snapshot persistence
- In-memory enrichment cache (2h TTL, thread-safe) skips re-scraping
when the same chart is clicked twice
- Batch enrichment replaces one-by-one HTTP requests with a single
POST, using WebSocket progress events for overlay updates
- Fix _beatportModalOpening guard blocking modal open after fast
cached enrichment by resetting the flag in openBeatportChartAsDownloadModal
- Hide Browse/My Playlists tabs — Browse is now the only Beatport view
- "Sync to Server" button appears for Beatport chart/playlist downloads
- Starts media server sync via /api/sync/start with live progress bar
- Progress area renders below all modal buttons with matched/failed counts
- Cancel button to abort sync mid-way, auto-cleanup on completion
- New sync_history DB table tracks last 100 syncs with full cached context
- Records history for all sync types: Spotify, Tidal, Deezer, YouTube,
Beatport, ListenBrainz, Mirrored playlists, and Download Missing flows
- Sync History button on sync page with modal showing entries, source
filter tabs, stats badges, and pagination
- Re-sync button: server syncs expand card inline with live progress bar,
matched/failed counts, and cancel button; download syncs open download modal
- Re-syncs update the original entry (moves to top) instead of creating duplicates
- Delete button (x) on each entry with smooth remove animation
- Fix mirrored playlist source detection (youtube_mirrored_ matched youtube_)
- Fix broken server import thumbnails with URL validation
Clicking any Beatport item now opens the download modal directly instead of
going through the discovery flow (scrape → chart card → discovery modal →
Spotify/iTunes matching → download).
Releases (albums/EPs) open as album downloads with full context.
Charts/playlists (Top 100, Featured Charts, DJ Charts, Top 10) open as
playlist downloads with per-track enrichment — each track's individual
Beatport page is visited to get release name, duration, artwork, BPM, key,
genre, and label.
Key changes:
- Add get_release_metadata() and enrich_chart_tracks() to scraper
- Add /api/beatport/release-metadata and /api/beatport/enrich-tracks endpoints
- Rewrite all Beatport click handlers to open download modal directly
- Per-track enrichment with live progress overlay (one-by-one fetching)
- Split combined artist strings so folder paths use primary artist only
- Prevent Beatport IDs from being written to Spotify tag fields
- Add beatport_release_ prefix detection for album download mode
- Support enrich=false query param for frontend-driven enrichment
Pin source info and search inputs at top of modal with independent
results scrolling, increase auto-search delay to 500ms, and add
confirmation dialog before committing a track match.
New toggle under Post-Download Conversion: automatically converts 24-bit
or high sample rate FLAC files to 16-bit/44.1kHz after download, replacing
the original. Uses ffmpeg with temp file + verify + atomic swap for safety.
Runs before lossy copy so MP3s are made from the downsampled version.
Also prevents bit depth strict mode from rejecting files that will be
downsampled anyway.
Root cause: discovery searches return (Track, raw_data, confidence) but
raw_data can be None (Strategy 4 extended search) or have mismatched index.
When raw_data is None, album_obj becomes {} — an empty dict that passes
normalization unchanged, so album name is never populated.
Fix: after extracting album_obj from raw_data, fall back to track_obj.album
(always populated from the SpotifyTrack dataclass) when album_obj has no
name. Applied to all 3 discovery paths (Deezer, Tidal, Spotify Public).
Also handle both string and dict album formats in wishlist UI rendering.
- Switch user automations to 2-column grid layout (matches system automations)
- Add duplicate button on non-system cards with POST /api/automations/<id>/duplicate
- Add search/filter bar (text search + trigger/action dropdowns) shown at 6+ automations
- Add Inspiration section with 8 starter templates that pre-fill the builder
- Add folder-style automation grouping with group_name DB column, dropdown
popover for assignment, collapsible group sections, and builder group input
- Exclude file and beatport playlists from refresh (no external API)
- Hide Spotify library playlists from refresh dropdown when not authenticated
- Add spotify_public refresh handler using public embed scraper via stored URL
- Fix YouTube refresh to use stored description URL instead of hash-based source_id
- API returns source and spotify auth status for frontend filtering
- Query/update watchlist artists by deezer_artist_id in config endpoint
- Return deezer_artist_id in config response and recent albums response
- Add Deezer provider badge (purple) to linked provider section
- Detect Deezer vs iTunes for provider linking using fallback source setting
- Show "X fans" instead of "Pop: 0" for Deezer artist search results
- Include followers count in match/search artist response
- Add deezer_artist_id matching to library enrichment and recent releases queries
- Dark translucent background with backdrop blur instead of bright colored fills
- Animated flowing gradient border using CSS mask-composite technique
- Color-tinted labels and count badges (amber for watchlist, accent for wishlist)
- Shimmer sweep clipped inside button bounds
- Structured HTML: separate icon, label, badge, and shimmer elements
- Badge pulses when count > 0
- Worker orbs: 7s delay before collapsing back after mouse leaves header
Worker buttons shrink to floating colored orbs with physics-based
movement, spark emissions from active workers, connection lines, and
center gravity. Hovering the header expands orbs back to full buttons
with staggered spring animation. Desktop only, toggleable in Settings
under UI Appearance.
Per-job and per-status filtering — respects active toolbar filters so
users can clear e.g. only track number findings or only dismissed items.
Confirmation uses the app's styled modal instead of browser confirm().
Shows which metadata sources each artist is matched to with small
colored badges on the card. Also adds deezer_artist_id to the
watchlist API response and fixes the data-artist-id fallback chain
to include Deezer-only artists.