Cover art lookup now honors an explicit prefer_source first,
falls back to the runtime primary metadata source when unset,
and uses the shared source priority for the remaining fallbacks.
Use the shared metadata source priority when resolving album IDs,
album searches, and tracklists in track number repair.
Keeps Deezer and iTunes ahead of Spotify where configured, while
still allowing the job to fall back through other supported sources.
Unknown artist resolution now uses the shared metadata source priority and only filters to the sources that can actually participate in this job. Deezer and iTunes remain direct lookup sources, while Hydrabase can now join the title-search path when it is the configured priority source.
Spotify was being called for album/artist data fetching across multiple
background workers and the Artists page search even when the user had
Deezer or iTunes set as their primary metadata source. Being authenticated
for playlist sync was treated as permission to use Spotify for everything.
- watchlist_scanner: add _spotify_is_primary_source() that checks both
auth and primary source config; use it for all album/artist data fetching
(discovery pool, recent album caching, playlist curation, similar artist
ID matching, proactive ID backfill). _spotify_available_for_run() is kept
for sync_spotify_library_cache which must run regardless of primary source
- repair_jobs/metadata_gap_filler: gate Spotify ISRC lookup on primary
source being 'spotify'; MusicBrainz lookup unaffected
- repair_jobs/unknown_artist_fixer: replace hardcoded spotify_client with
source-aware client selection — primary source ID tried first, each ID
matched to its correct client (fixes latent bug passing Deezer IDs to
Spotify)
- web_server.py /api/match/search: Artists page search was hardcoded to
spotify_client.search_artists(); now uses _get_metadata_fallback_client()
so results come from the configured primary source
Adds a new Last.fm Radio section to the Discover page that lets users
search a track on Last.fm, generate a similar-tracks playlist, and run
it through the existing discovery/download/sync pipeline. Also generates
playlists automatically from top listening history during watchlist scans
(max once per week).
- core/lastfm_client.py: Add get_similar_tracks() using track.getsimilar
- core/listenbrainz_manager.py: Add save_lastfm_radio_playlist() with
deterministic MBID (MD5 seed), cleanup limit of 5 for lastfm_radio type
- web_server.py: Add /api/lastfm/configured, /api/lastfm/search/tracks,
/api/lastfm/radio/generate, /api/discover/listenbrainz/lastfm-radio;
fix playlist['name'] KeyError in discovery worker that was resetting
phase back to 'fresh' after completion
- core/watchlist_scanner.py: Add _generate_lastfm_radio_playlists() with
weekly throttle, called at end of scan_all_watchlist_artists()
- webui/index.html: Add #lastfm-radio-section above ListenBrainz section,
hidden unless Last.fm API key is configured
- webui/static/script.js: Search/generation/card-load functions; fix
discovery modal labels (Last.fm Radio vs ListenBrainz), description
update on completion, belt-and-suspenders completion handling inside
updateYouTubeDiscoveryModal; fix album/duration display for tracks
without metadata; music note SVG placeholder for missing art
- webui/static/style.css: Styles for search bar, dropdown, result rows
Centralize the ordered metadata source list and source-priority helper so album completeness and the repair worker follow the same Deezer/iTunes-first fallback order. This also removes the last duplicate priority logic from the touched repair paths.
Album completeness and any other repair job now uses the centralized source/client helpers instead of a worker-local Spotify client or override plumbing
- This keeps source selection aligned with the configured primary provider and removes the last Spotify-only special case from the job path.
This change ultimately is a step towards further centralizing the Spotify client access and the associated `is_spotify_authenticated` check.
- Currently these look-ups are done all over the place in different feature implementations directly, but moving forward, any feature that uses `get_primary_client` or `get_client_for_source` to access the Spotify client, won't have to duplicate any rate-limiting or auth checks as long as these getters are used
After a page refresh JS state is wiped, so currentTrack is null and the
player widget stays hidden even though the backend is still streaming.
The backend already includes track_info (name/artist/album/image_url) in
every tool:stream push. The 'ready' case in both handlers now calls
setTrackInfo() when no track is loaded, which unhides the player and
populates title/artist before startAudioPlayback() runs.
The sidebar player was a poor use of vertical real estate and created the
collapsed-state layout issues. The mini player is now a fixed 360px widget
at bottom-right (above the bell/help buttons), matching the convention of
most streaming apps.
Changes:
- Removed media player from sidebar; sidebar spacer now pushes support/version
section to bottom as before
- New .mini-player-body horizontal layout: album art | track info | controls
- Added prev/next skip buttons (mini-nav-btn) with same skip logic as the
Now Playing modal; updateNpPrevNextButtons() now syncs both sets
- .media-player.idle now display:none (widget hides entirely when no track)
- Progress bar is a flush full-width line at the top of the widget
- Volume slider kept as hidden DOM element for JS compatibility; volume is
set via the Now Playing modal
- Toast container moved up to bottom:174px to stay above the mini player
- expand-hint button updated to four-corner expand icon, opens NP modal
- All volumeSlider and click-exclusion references updated for null-safety
The stream 'stopped' backend event was calling clearTrack() in both the
WebSocket handler (updateStreamStatusFromData) and the polling handler
(updateStreamStatus), which added the .idle class and collapsed the player
to zero height. This fired whenever audio ended naturally or when
transitioning between queue items (playQueueItem calls stopStream() to reset
backend state before loading the next track).
The explicit stop button (handleStop) already calls clearTrack() directly, so
the 'stopped' event handlers don't need to — and shouldn't.
New core/replaygain.py module uses FFmpeg's ebur128 filter (already a
project dependency) to analyze integrated loudness and true peak, then
writes ReplayGain 2.0 tags (-18 LUFS reference) to MP3 (TXXX frames),
FLAC/OGG/Opus (Vorbis comments), and M4A/MP4 (freeform atoms).
Three analysis modes in the enhanced library view:
- Per-track RG button: synchronous single-track analysis (~1-3 s)
- Album "ReplayGain" button: background job writing both track gain
and album gain (mean LUFS across all album tracks) to every file
- Bulk bar "ReplayGain" button: batch track-gain for selected tracks
read_file_tags() in tag_writer.py extended with four new optional keys
(replaygain_track_gain/_peak, replaygain_album_gain/_peak) so existing
RG values surface in the tag-preview diff view. Purely additive — no
existing endpoints or DB schema changed.
Singles could not be saved as a flat file (e.g. "$artist - $title")
because the frontend blocked any template without a "/" and the
backend path builder treated an empty folder_path as falsy, falling
through to the hardcoded nested-folder structure.
Frontend: removed the must-include-slash validation for single
templates only (album templates still require it).
Backend: changed condition from `if folder_path and filename_base`
to `if filename_base` so an empty folder_path is handled correctly
as a flat drop into the transfer root.
Hardcoded ports 8888/8889 conflict when SoulSync runs behind Gluetun or
other containers that claim those ports. Introduce SOULSYNC_SPOTIFY_CALLBACK_PORT
and SOULSYNC_TIDAL_CALLBACK_PORT env vars (defaulting to 8888/8889) so
users can remap without rebuilding the image.
docker-compose.yml exposes the vars with comments explaining how to keep
the port mappings in sync with the redirect URI in Settings → Connections.
Track per-quality-tier failure reasons across all failure paths (stream
error, empty manifest, download exception, stub file, MP4 extraction
failure) and include them in the exhausted-tiers log message so failures
are diagnosable from logs.
When HiRes is configured with no fallback and all tiers are exhausted,
log an actionable hint directing the user to enable Quality Fallback.
Surface Tidal-specific error messages in the UI task on retry
exhaustion: distinguishes HiRes-unavailable (with actionable guidance)
from general Tidal auth/quality failures, rather than showing the
generic Soulseek error string.
track_downloads stores local Windows paths but tracks table stores
server-side paths (Plex/Jellyfin). Both the track_id lookup (NULL
due to failed auto-link at insert time) and exact file_path fallback
were failing.
Added filename-suffix LIKE matching as a final fallback in
get_track_source_info, plus a back-link so the track_id gets written
back for fast future lookups. Also improved the auto-link in
record_track_download to use the same suffix matching when exact path
fails.
M3U entries now resolve actual file paths from the DB instead of
synthesising a fake 'Artist - Title.mp3' string that no media server
could use. Adds optional M3U Entry Base Path setting (Downloads tab)
so servers requiring absolute paths (e.g. /mnt/music) can be supported.
- New POST /api/generate-playlist-m3u endpoint: per-artist batch DB
lookups with fuzzy title matching, prefixes entry_base_path when set
- autoSavePlaylistM3U and exportPlaylistAsM3U now call the new endpoint
- M3U Entry Base Path input added below Music Videos Dir in settings,
follows path-input-group pattern with Unlock button and autosave
Album completeness and downstream repair flow now follow the configured
primary provider first, with Discogs and Hydrabase support added alongside
existing Spotify, iTunes, and Deezer paths.
Keep spotify_track_id for compatibility while preserving source-aware track
IDs for provider-neutral handling.
Server Playlists was filtered to only show playlists matching mirrored_playlists entries,
but Discover syncs are stored in sync_history (not mirrored_playlists), so they were
excluded. Adds GET /api/sync/history/names returning distinct synced playlist names,
and includes those in the filter alongside mirrored playlists.
_try_staging_match() built a minimal context missing spotify_artist,
spotify_album, is_album_download, and has_clean_spotify_data. Post-
processing returned early at the missing-spotify_artist guard and the
copied file was left at the transfer root with its original filename.
Now mirrors the sync modal worker's context-building: uses
_explicit_album_context/_explicit_artist_context when available
(artist-page album downloads), falls back to track.album/track.artists
for playlists and sync modal. track_number and disc_number are also
forwarded so multi-disc albums land in the correct Disc N/ subfolder.
import_album_process never computed total_discs, so spotify_album.total_discs
always defaulted to 1 in _build_final_path_for_track. Now pre-computes
total_discs from the matched tracks before the per-track loop.
Builds a new Your Albums section on the Discover page that aggregates
saved/liked albums from all connected services, mirroring the Your Artists
pattern. Deezer works via both OAuth and ARL.
- tidal_client: add get_favorite_albums() with V2/V1 API fallback
- deezer_client: add get_user_favorite_albums() via OAuth (user/me/albums)
- deezer_download_client: add get_user_favorite_albums() via ARL session
- music_database: add liked_albums_pool table (deduped by artist::album
normalized key), upsert_liked_album, get_liked_albums,
get_liked_albums_last_fetch, clear_liked_albums
- web_server: GET /api/discover/your-albums (ownership-checked, paginated),
GET /api/discover/your-albums/sources, POST /api/discover/your-albums/refresh,
_fetch_liked_albums background worker (Spotify + Tidal + Deezer OAuth/ARL)
- frontend: Your Albums section with source selector cog, album grid reusing
spotify-library-card styles, search/filter/sort/pagination, download missing
button, auto-refresh poll on first load
Also fix: Deezer greyed out in Your Artists sources when using ARL — connection
check now accepts ARL auth (deezer_dl.is_authenticated()) in addition to OAuth,
and _fetch_and_match_liked_artists falls back to ARL client for artist fetching.
Add get_user_favorite_artists(limit=200) to DeezerDownloadClient to fetch a user's favorite artists via the public API using an ARL-authenticated session (paginated, error-handled, returns deezer_id, name, image_url).
Update web_server to treat Deezer as connected if either OAuth or ARL is authenticated, and to fetch favorite artists from OAuth client when available or from soulseek_client.deezer_dl (ARL) otherwise. Fetched artists are upserted into the database and appropriate log/console messages and counters are updated.
Three issues fixed:
1. Plex add-track used delete+recreate (Playlist.create) which was
unreliable — switched to addItems() which atomically appends the
track without touching existing playlist items.
2. After a successful add, the UI only did an optimistic local update.
On reopen the automatic matcher ran fresh and couldn't connect the
manually selected track to the source slot, making it look unfixed.
Now both add and replace re-fetch the compare view from the server
so the matcher sees the actual updated Plex state.
3. Matching algorithm was too strict for common title variants. Added
_norm_title() which strips feat./ft., remaster/remastered, and
edition qualifiers before comparison — so "Boy 1904" matches
"Boy 1904 (2019 Remaster)" and "Float Away" matches "Float Away
(feat. Flamingosis & Eric Benny Bloom)". Display titles unchanged.
get_playlist_tracks() had no Limit or StartIndex params, so Jellyfin
defaulted to ~100 items per response. This caused the Server Playlists
comparison to show most tracks as missing even though they were present.
Now paginates in batches of 1000 until a partial batch signals the last page.
Spotify album art: replace the 4-char size segment after '0000ab67616d'
with '82c1' to request the original uploaded master (up to 2000px+).
Applied via _upgrade_spotify_image_url() in Track, Artist, and Album
dataclass constructors and as a catch-all in _download_cover_art.
Scoped to the ab67616d album art prefix only — artist images use a
different prefix (ab676161) where the trick does not apply.
iTunes/Apple Music: replace '100x100bb' with '3000x3000bb' in all
artworkUrl100 replacements across Track, Artist, Album, and the
get_album images arrays. Also applied as a catch-all in _download_cover_art.
Deezer already uses cover_xl at its maximum — no changes needed there.
The status cache's source field was being set by _get_metadata_fallback_source(),
which internally calls is_spotify_authenticated() a second time via module import.
Any inconsistency between that path and the direct auth check already performed
would return 'deezer', poisoning the cache — and since the WebSocket takes over
from the HTTP poll, it never corrected itself.
Now reads config directly and reuses the single auth probe result.
Gear button next to View All opens a sources modal letting users pick
which connected services (Spotify, Tidal, Last.fm, Deezer) contribute
artists to the Your Artists carousel. Setting saved via standard
/api/settings endpoint under discover.your_artists_sources.
- GET /api/discover/your-artists/sources returns enabled config + which
services are currently connected
- _fetch_and_match_liked_artists skips sources not in the enabled list
- Disconnected services shown dimmed and non-interactive in modal
- Saving with nothing selected blocked with error toast
- Remove z-index from .sidebar-header (fixes artist map overlap)
- Add padding-bottom to #automations-list-view (search bar overlap fix)
Add a 10vh bottom padding rule for #automations-list-view in webui/static/style.css to provide extra spacing at the bottom of the automations list and prevent content from being obscured by fixed UI elements (e.g., footer).
Add padding-bottom: 10vh to #settings-page .settings-content so the
bottom section is not obscured by the floating search bar overlay.
Closes#292 (item 3)
Replace div badges with data-url/onclick handlers by semantic <a> elements (with href, target="_blank" and rel="noopener noreferrer") for clickable artist badges, keeping non-clickable badges as divs. Update CSS to target .artist-hero-badge and unify hover/image rules instead of relying on data-url attribute, preserving visual behavior and removing pointer cursor for non-clickable divs. Also remove rendering of the server_source badge from the artist meta panel. These changes improve accessibility, security, and maintainability of badge markup and styling.
Add an Import nav button after Downloads (new SVG/icon) and remove the duplicate Import button that was located after Stats. This adjusts the sidebar navigation order (Downloads → Import → Library → ...) and removes the redundant element in webui/index.html.
The fallback test (used when no audio files exist in the library) sends
a dummy fingerprint to the AcoustID API. The API correctly rejects the
dummy fingerprint but this is not error code 4 (invalid key), so the
test was returning False instead of True. Any non-code-4 error from the
fallback means the API key was accepted — only code 4 means a bad key.
Instrument web_server.py to record Spotify API usage by calling core.api_call_tracker.api_call_tracker.record_call before various Spotify SDK calls. Added local imports and record_call invocations around artist, playlist, playlist_tracks_page, current_user_saved_tracks, artists_batch, track and related endpoints across functions such as get_artist_image, get_playlist_tracks, watchlist_artist_config, enrich_similar_artists, get_your_artist_info, get_artist_map_explore, import_album_process and import_singles_process. This enables per-endpoint monitoring and helps with rate-limit tracking while keeping the import local to avoid top-level dependency issues.
Deezer and iTunes defaulted to 50 albums max, silently truncating large
discographies. Deezer now paginates (100 per page) up to 200. iTunes
raised to 200 (single call). All callers in web_server.py updated to
use the new defaults instead of hardcoding limit=50.
Also adds diagnostic logging for allow_duplicates album comparison
to help debug inconsistent singles behavior.
The wishlist table has a UNIQUE constraint on spotify_track_id, so
INSERT OR REPLACE silently overwrote the existing entry when the same
track appeared on a different album (same Spotify track ID). Now uses
a composite key (track_id::album_id) when allow_duplicates is on and
the base track ID already exists, allowing both versions to coexist.
Only affects users with allow_duplicates enabled.
Singles like "idol" weren't added when the same song existed on a
different album because check_track_exists used album-aware matching
that found the track via album name. Now skips the album hint when
allow_duplicates is on so matching is title+artist only, then compares
album names ourselves with a strict 0.85 threshold. Only affects users
with allow_duplicates enabled.
One-time migration deletes metadata cache entries where artist_name is
null, empty, 'unknown', 'unknown artist', etc. The cache gate now
prevents new junk entries, but existing poisoned data from before the
fix needs cleaning so users don't keep getting Unknown Artist from
stale cache hits.
The setting only affected wishlist dedup but the watchlist scanner's
library check still skipped tracks by title+artist regardless. Now
when allow_duplicates is enabled, the scanner compares album names
and only skips if the same album matches. Same song on a different
album is allowed through to the wishlist.
search_artists now uses unidecode_lower() and _normalize_for_comparison()
so 'Tiesto' finds 'Tiësto'. Track search already had this — artist
search was the only gap. No change to stored data, only the comparison.
Version bump to 2.3 with rewritten What's New modal covering all
changes since v2.2. Docker publish workflow default updated.
Sidebar improvements:
- Header stays pinned at top while nav and player scroll beneath it
- Media player collapses to compact single-line when no track is
playing, expands to full size when playback starts
Fixes:
- Server playlists endpoint Plex Tag object crash (getattr fix)
- Server playlists tab auto-refreshes after download completion
- Fixed dead code syntax error in archived version notes
sync-tab-content had overflow:hidden which clipped long content like
the file import preview table and server playlist editor. Changed to
overflow-y:auto so all sync tabs scroll when content exceeds the
container height.
Deezer's API returns a contributors array with all credited artists on
a track, but only the primary artist field was used. Now extracts all
contributor names into the artists array for feature/collab tracks.
Only affects the per-track ARTIST tag (TPE1) — album artist, folder
paths, and matching are unchanged. Falls back to single primary artist
when contributors field is absent (search results) or has only one
entry.