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.
- 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
Implements get_track_details, get_album, get_artist, get_artist_albums,
get_track_features, is_authenticated, and reload_config to match the
iTunes/Deezer client interface. Each method tries a specific request
type first then falls back to search if the server doesn't support it.
Preserves existing get_album_tracks (List[Track]) for backward compat.
Post-processing now extracts release year from MusicBrainz, Deezer,
Tidal, Qobuz, and Spotify context (first source wins). Writes
ORIGINALDATE and DATE tags to file, and backfills the album year
in the DB if currently missing. Fixes Library Reorganize showing
blank years for Tidal-only downloads.
Also raises Library Reorganize API year lookup cap from 50 to 200.
- 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
- 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
Worker checked self.client.sp (non-None even without Spotify auth due
to fallback) instead of is_spotify_authenticated(). Searched via
iTunes/Deezer fallback, got numeric IDs, rejected them all with
warnings. Now sleeps when Spotify isn't authenticated instead of
making pointless fallback searches.
Pending count queries included NULL-ID rows that _get_next_item filters
out, so pending stayed > 0 even when no processable items remained.
Workers reported running instead of idle, UI never turned green. Added
AND id IS NOT NULL to _count_pending_items across all 9 workers to
match the _get_next_item filter.
Workers would endlessly match the same track because UPDATE WHERE id =
NULL matches 0 rows in SQL. Added AND id IS NOT NULL to all enrichment
queries (individual, batch EXISTS, and batch fetch) across all 9
workers. Also added process-level guard for belt-and-suspenders safety.
Fix Deezer get_track → get_track_details method name mismatch.
New setting: Min Completion % (default 0 = disabled). Skips albums
where the user has fewer than N% of tracks — filters out playlist
imports where a single track exists but the full album was never
intended to be downloaded. Catches real failed downloads (6/12)
while ignoring incidental singles (1/12).
Skip refresh attempt if client_id/secret not configured — clears stale
tokens to stop retry loop permanently. Add 5-minute backoff after
failed refresh to prevent hammering Tidal API every 2 seconds.
Wishlist entries from auto-fill were missing album images, album ID,
track number, disc number, and total tracks. Downloads would have no
cover art and wrong file organization. Now includes full album context
matching the standard wishlist data format.
When suffix path matching fails, reads file tags (title+artist) and
checks against DB tracks. Prevents false orphan detection from path
mismatches in Docker where DB paths differ from filesystem paths.
Only runs for files that fail suffix matching — zero overhead in
normal cases.
- Only emit batch_complete automation event when successful_downloads > 0
- Remove int() cast on album_id in _fix_incomplete_album — Plex/Jellyfin
use hash/text IDs that SQLite stores as-is in INTEGER columns
- Use string comparison for album_id equality check
Finding was created with entity_id=None (file-based) but fix handler
required entity_id for DB update. Rewrote handler to work with file_path
as primary — writes corrected track number to file tags, renames file
if needed, updates DB path. Also stores total_tracks in finding details
for correct tag writing.
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
- 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
Was Spotify-only — users without Spotify got zero results. Now queries
albums with any source ID (spotify_album_id, itunes_album_id, deezer_id)
and uses the matching API client for track count and missing track lookup.
Falls back gracefully across sources with client-type detection.
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.
_sanitize_context_values passed empty strings through _sanitize_filename
which converts '' to '_' (empty-name fallback). Template $album ($year)
became Album (_) instead of Album () which the cleanup regex couldn't
match. Now preserves empty strings so the existing () cleanup works.
- Add _fix_path_mismatch handler so fixing Library Reorganize dry-run
findings actually moves files (was returning 400 with no handler)
- Add path_mismatch to fixable_types and _execute_fix handler map
- Add recent_releases and wishlist_tracks as year sources in
_load_album_years to cover more playlist-synced tracks missing years
- Add sys and json imports needed by new code
- Add single_album_redundant to fixable_types in bulk_fix_findings so
Fix All actually includes these findings (Fix Selected worked, Fix All
silently returned 0)
- Expand version keyword regex from 9 to 25 terms (remastered, deluxe,
unplugged, etc.) to reduce false positives in Single/Album Dedup
- Add word boundary anchors to prevent substring matches (e.g. "live"
inside "Alive", "edit" inside "Meditate")
- Cast similarity thresholds to float for config type safety
The SQL HAVING clause filtered on local track count instead of expected
track count, excluding albums with fewer than 3 local tracks from the
scan entirely. Now fetches all Spotify-matched albums and filters by
expected track count in the loop.
- 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
EPs are no longer classified as singles — removing EP tracks would
break the release and conflict with album completeness checks.
Only actual singles (album_type='single' or unknown type with <=2
tracks) are flagged as redundant when the same song exists on an album.
Tracks on different albums (e.g., same song on a studio album and a
compilation) are no longer flagged as duplicates by default, keeping
albums complete. Setting is enabled by default and configurable via
the job settings UI.
Playlist-synced tracks often have no year in file tags or the DB albums
table. Now checks discovery_pool release_date as a second DB source,
then batch-lookups remaining missing years from the user's active
metadata source (Spotify/iTunes/Deezer) capped at 50 API calls.
Multiple code paths built wishlist album data as a bare string instead
of a dict with images/album_type/total_tracks, causing wishlist entries
to lose album name and artwork. Fixed in: Beatport fallback conversion,
sync normalization, _ensure_spotify_track_format, and wishlist service
track reconstruction. Now uses track name as album fallback and always
includes required album dict keys.
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.
Deep scan passed skip_existing_tracks=True which skipped calling
insert_or_update_media_track entirely for known tracks, so stale
file paths were never refreshed from the media server. Now always
calls insert_or_update (which safely uses UPDATE for existing tracks,
preserving enrichment data) so file paths stay in sync.
Normalization was stripping parenthetical content before comparison,
so 'title' and 'title (xxx remix)' both became 'title' and always
matched at 1.0 regardless of the user's threshold setting.
_get_source() hardcoded 'itunes' as fallback, so seasonal content was
stored with source='itunes' but API endpoints queried source='deezer' —
resulting in empty discover page seasons. Now reads the configured
fallback source from metadata_service.
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.
- Add null guard for soulseek_client in test-connection endpoint (#190)
- Fix single_album_dedup query: use al.record_type and al.track_count
(al.album_type and al.total_tracks don't exist on main albums table)
- Fix library reorganize missing year: pre-load album years from DB as
fallback when file tags lack the date field (common with playlist syncs)
- 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
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.
Both clients have their own Track class separate from iTunes. The
album preference commit added album_type/total_tracks to from_*_track()
constructors but not to the class definitions, crashing all Deezer and
Spotify discovery searches.
Scans library for tracks on singles/EPs that also exist on a full album,
letting users clean up fragmented libraries. Includes version-tag safety
(live, acoustic, remix etc. won't cross-match) and verifies the album
version still exists before removing a single.
Add album_type and total_tracks fields to Track dataclass, populate from
Spotify/iTunes/Deezer API responses, and apply a small tiebreaker bonus
(+0.02 for albums, +0.01 for EPs) in all matching loops so album versions
win when confidence scores are otherwise equal.
Deezer search results (cached without release_date, track_position, isrc)
were being served by get_track_details as cache hits, preventing the actual
/track/{id} API call that returns complete data. This caused $year template
variable to remain empty even with the discovery worker fix in place.
trigger_library_scan and is_library_scanning were no-ops assuming
Navidrome auto-detects new files. Navidrome supports startScan and
getScanStatus via the Subsonic API, so use them like Plex and Jellyfin.
- DB path update uses suffix matching when exact match fails, so server
paths (/mnt/musicBackup/...) are found and updated after local moves
- Escape LIKE wildcards in artist/album names to prevent false matches
- Move album-level sidecars (cover.jpg, folder.jpg, etc.) alongside audio
- Post-pass sweep moves all remaining sidecars from emptied directories
- Multi-disc albums place album art in album root, not disc subfolders
Use config_manager for transfer path (same source as playback) instead of
separate DB read that silently falls back to ./Transfer under contention.
Abort scan if transfer folder doesn't exist to prevent mass false findings.
- personalized_playlists._get_active_source() now returns 'deezer' when
configured instead of always falling back to 'itunes'
- Add deezer_track_id to _build_track_dict() for discovery pool tracks
- Include album_deezer_id and artist_deezer_id in get_discovery_recent_albums()
response — fixes "No deezer album ID available" error when clicking cards
- Skip Spotify library section entirely when Spotify is not authenticated
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().
Previously only 502/503/504 triggered instance rotation. A 500 from one
instance (e.g. triton.squid.wtf choking on a specific query) would stop
the search entirely instead of trying the next instance.
Users can now choose between iTunes/Apple Music and Deezer as their free
metadata source in Settings. Spotify always takes priority when authenticated;
the fallback handles all lookups when it's not.
Core changes:
- DeezerClient: full metadata interface (search, albums, artists, tracks)
matching iTunesClient's API surface with identical dataclass return types
- SpotifyClient: configurable _fallback property switches between iTunes/Deezer
based on live config reads (no restart needed)
- MetadataService, web_server, watchlist_scanner, api/search, repair_worker,
seasonal_discovery, personalized_playlists: all direct iTunesClient imports
replaced with fallback-aware helpers
Database:
- deezer_artist_id on watchlist_artists and similar_artists tables
- deezer_track_id/album_id/artist_id on discovery_pool and discovery_cache
- Full CRUD for Deezer IDs: add, read, update, backfill, metadata enrichment
- Watchlist duplicate detection by artist name prevents re-adding across sources
- SimilarArtist dataclass and all query/insert methods handle Deezer columns
Bug fixes found during review:
- Similar artist backfill was writing Deezer IDs into iTunes columns
- Discover hero was storing resolved Deezer IDs in wrong column
- Status cache not invalidating on settings save (source name lag)
- Watchlist add allowing duplicates when switching metadata sources
Add minimum 60% title similarity gate to match_recording() — prevents
artist bonus + MB score from pushing unrelated titles past the confidence
threshold (e.g. "Sweet Surrender" matching "Answers" by same artist).
New MBID Mismatch Detector repair job reads embedded MusicBrainz recording
IDs from audio files, verifies them against the MusicBrainz API, and flags
tracks where the MBID points to a different song. Fix action strips the bad
MBID tag so media servers like Navidrome fall back to correct file tags.