Single track ownership check was calling check_track_exists without
server_source, matching against all servers instead of the active one.
Album and EP checks already passed server_source correctly — this was
the only missing spot. Affects all server types.
Previously reused existing plex/jellyfin artist IDs, causing soulsync
tracks to be invisible on the library page (filtered by server_source).
Now always creates soulsync-specific artist and album records with
server_source='soulsync', avoiding PK collisions with hash suffixes.
Fourth server option on the Connections tab with SoulSync logo and
'Standalone' label. Config panel shows Transfer folder path and
Verify Folder button. Test connection counts audio files in the
Transfer folder. Settings save/load properly detects soulsync toggle.
New 'soulsync' media server option manages the library directly from
the filesystem, bypassing Plex/Jellyfin/Navidrome entirely.
Two paths populate the library:
1. Downloads/imports write artist/album/track to DB immediately at
post-processing completion, with pre-populated enrichment IDs
(Spotify, Deezer, MusicBrainz) so workers skip re-discovery
2. soulsync_client.py scans Transfer folder for incremental/deep scan
via DatabaseUpdateWorker (same interface as server clients)
New files:
- core/soulsync_client.py: filesystem scanner implementing the same
interface as Plex/Jellyfin/Navidrome clients. Recursive folder scan,
Mutagen tag reading, artist/album/track grouping, hash-based stable
IDs, incremental scan by modification time.
Modified:
- web_server.py: _record_soulsync_library_entry() at post-processing
completion, client init, scan endpoint integration, status endpoint,
web_scan_manager media_clients dict, test-connection cache updates
- config/settings.py: accept 'soulsync' in set_active_media_server,
get_active_media_server_config, is_configured, validate_config
- core/web_scan_manager.py: add soulsync to server_client_map
Dedup: checks existing artist/album by name across ALL server sources
before inserting to avoid duplicates. Enrichment IDs only written when
the column is empty (won't overwrite existing data).
Race condition: scanner re-scanned folders while post-processing was
still moving files, causing partial matches and ghost failures. Now
tracks in-progress paths and skips them on subsequent scans.
Coverage penalty fix: individual tracks that match at 80%+ confidence
now auto-import even when overall album coverage is low (e.g. 2 of 18
tracks present). Previously low coverage killed the entire import.
Import page: stats bar, filter pills, Scan Now, Approve All, Clear
History (clears imported + failed), live scan progress.
Album delete now shows a smart delete dialog with two options:
- Remove from Library (DB only, files untouched)
- Delete Files Too (removes DB records AND deletes audio files from
disk, cleans up empty album folder)
Backend /api/library/album/<id> DELETE now accepts ?delete_files=true
parameter, resolves each track's file path, and removes files before
deleting DB records. Reports files_deleted and files_failed counts.
The ffmpeg failure log was truncated to 200 chars which only showed the
version banner, hiding the actual error message. Now strips the banner
preamble and shows up to 500 chars of the real error with exit code.
Also cleans up 0KB output files left behind when ffmpeg fails.
Soulseek results from "Various Artists", "VA", "Unknown Artist", and
"Unknown Album" folders are now rejected before scoring. These
compilation folders rarely contain properly tagged files for the target
artist.
Clearing the wishlist now also cancels any active wishlist download
batch and resets the auto-processing flag, so downloads don't keep
running after the source tracks are removed.
Split Downloads page into main list (left) and batch panel (right).
Each active batch gets a color-coded card with artwork thumbnail,
progress bar, per-track status with download percentages, and
expandable track list. Download rows get matching color indicators.
- Click batch name to open its download/wishlist modal
- Filter icon narrows main list to one batch with clear banner
- Collapsible panel toggle for full-width list view
- Completed batches fade out after 15 seconds
- 7-day batch history with source type color dots
- Artwork fallback shows colored initial when no art available
- Per-track progress: download %, spinner for searching, proc label
- source_page column on sync_history for UI origin tracking
- /api/downloads/all includes batch summaries and per-track progress
- /api/downloads/batch-history endpoint for history queries
- Responsive layout, overflow-x hidden to prevent scroll flicker
Priority 0 query (artist + album + title) was gated behind a download
mode check that excluded Soulseek, the source that benefits most from
it. Soulseek searches match against file paths where users organize as
Artist/Album/Track — without the album name, ambiguous artist names
could match wrong-artist results (e.g. "Bleakness" as an album folder
instead of an artist). Removed the mode gate so all sources get the
most specific query first.
Dashboard stats (every 10s) and download status endpoint were
unconditionally calling slskd transfers/downloads API, causing
connection timeout spam for users with a slskd URL configured but
using YouTube/Tidal/etc as their download source. Now checks both
download source mode and status cache before making the API call.
Adds April 17 entries for Auto-Import, Wishlist Nebula, automation group
management, bidirectional artist sync, provider-agnostic discovery, live
sidebar badges, and critical source ID embedding fix. Version modal
reorganized to lead with current features and summarize earlier v2.2 work.
Full auto-import pipeline: background worker watches the staging folder,
identifies music using embedded tags → folder name parsing → AcoustID
fingerprinting, matches files to metadata source tracklists, and
processes high-confidence matches through the existing post-processing
pipeline automatically.
Worker: AutoImportWorker with start/stop/pause/resume, configurable
scan interval (default 60s), confidence threshold (default 90%), and
auto-process toggle. Processes one folder per cycle, alphabetical
order. Disc folder detection, stability checking, content hash dedup.
Confidence gate: 90%+ auto-processes silently, 70-90% queued as
pending review with approve/dismiss actions, <70% flagged for manual
identification. Track matching uses weighted algorithm (title 45%,
artist 15%, track number 30%, album tag 10%).
Database: auto_import_history table tracks every scan result with
folder hash, match data JSON, confidence, status, timestamps.
API: 7 endpoints — status, toggle, settings (GET/POST), results
(filtered/paginated), approve, reject.
UI: Auto tab on Import page with enable toggle, confidence slider,
scan interval selector. Live result cards with album art, confidence
bar (green/yellow/red), status badges, match stats. 5-second polling.
Artist Sync button on enhanced library page now does true bidirectional
sync: Phase 1 pulls new albums/tracks from the media server using the
DatabaseUpdateWorker in deep scan mode (preserves enrichment), Phase 2
removes stale DB entries for files no longer on disk. Works for Plex,
Jellyfin, and Navidrome. Toast shows +albums, +tracks, -stale counts.
Repair jobs tab redesigned: 2-column grid layout with glass gradient
cards, accent top line on hover, hover lift effect, job description
text below name, running state with pulsing accent bar. Responsive
to single column under 900px.
Fixed deezer_artist_id → deezer_id column name on artists table lookup.
The artists table uses 'deezer_id' but the enhanced library artist
lookup was querying 'deezer_artist_id' (the watchlist_artists column
name). Fixed to use the correct column name.
Watchlist scanner: empty discography (no new releases in lookback) was
treated as API failure, causing "Failed to get artist discography" for
artists like Kendrick Lamar who simply had no recent releases. Now
distinguishes None (API failure → try next source) from [] (success,
no new tracks). Spotify backfill now uses the authenticated client
instance instead of creating a fresh unauthenticated one.
Wishlist nebula: album remove now sends album_name (API updated to
accept album_name as fallback alongside album_id). Track remove
re-renders the nebula after deletion. Toned down processing pulse
animation.
Updated test to verify fallback triggers on API failure (None), not
on empty results.
Full automation page upgrade with group management and drag-and-drop:
Backend: batch_update_group() and bulk_set_enabled() DB methods, new
PUT /api/automations/group and POST /api/automations/bulk-toggle endpoints.
Group headers: rename (inline edit), delete (choice dialog — keep
automations or delete all), bulk toggle (enable/disable all in group).
Actions appear on hover, styled as small icon buttons.
Drag and drop: non-system cards are draggable between group sections.
Drop zones show dashed accent border feedback. Collapsed sections
auto-expand on 500ms drag-hover. System/Hub sections dimmed during drag.
dragenter counter pattern handles child element bubbling.
Delete group dialog: glass card modal with three options — keep
automations (move to My Automations), delete everything, or cancel.
The MusicBrainz consistency change referenced 'context' inside
_embed_source_ids(), but that variable was never passed to the function.
Every download since that commit silently skipped ALL source ID tags
(Spotify, MusicBrainz, Deezer, AudioDB, Tidal, Qobuz, Last.fm, Genius)
with the error 'name context is not defined' caught as non-fatal.
Fix: pass context from _enhance_file_metadata to _embed_source_ids,
with None default for backward compatibility.
Adaptive card on the Dashboard showing library state with four modes:
- No server: gold accent, directs to Settings
- Disconnected: gold warning with troubleshooting guidance
- Empty library: blue accent with prominent Scan Now button
- Healthy: green accent with stats grid (artists/albums/tracks/DB size),
Refresh button (incremental) and Deep Scan button (full re-check)
Stats displayed as mini cards with individual icons. Animated glow orb,
gradient accent top line, shimmer progress bar during scans. Deep scan
added to /api/database/update endpoint (deep_scan flag) — re-checks
every track, adds new ones, removes stale, preserves enrichment data.
Confirmation dialog explains what deep scan does before starting.
Soulseek source files often carry the uploader's MusicBrainz IDs from
different releases. When post-processing skipped tag clearing (missing
spotify_album context in wishlist batches, or enhancement exceptions),
these conflicting IDs persisted and caused Navidrome to split one album
into multiple entries.
Added _wipe_source_tags() — a lightweight emergency tag wipe that clears
all tags without writing new ones. Called in every failure/skip path:
stream processor exception, playlist mode exception, verification worker
missing context, and verification worker exception. Idempotent and
wrapped in try/except so it never interferes with the existing flow.
When the DB stored a path the resolver couldn't map to a local file
(common with Navidrome virtual paths or Docker path mismatches), file
deletion was silently skipped — the DB record was removed but the file
stayed on disk with no indication to the user.
Now logs the resolution failure with the stored path, returns a
file_error in the API response, and the frontend shows a warning toast
explaining the file wasn't deleted plus a second toast with the specific
reason (e.g. Navidrome 'Report Real Path' instructions).
Keep the weekly Last.fm radio generation step in the web watchlist scan post-processing chain so the higher-level scan behavior stays intact after moving the scan loop into the shared scanner core.
Bring placeholder tracklist skipping back into the shared watchlist scan path, and centralize the DB-only artist image backfill helper so both web scan entrypoints reuse the same logic.
Drop the legacy watchlist scan entrypoints that are no longer used by the web scan flow, and keep the live refresh path pointed at the shared scanner helper.
Move the shared watchlist scan loop into core/watchlist_scanner.py so web_server.py only handles triggers, locks, progress, and post-scan orchestration.
Manual and scheduled watchlist scans now share the same scanner-side core, while the web entrypoints keep profile selection and automation progress updates.
Recording MBIDs are now pulled from the matched release tracklist instead
of independent match_recording() searches, guaranteeing the recording ID
is consistent with the selected release. Batch-level artist name is used
for release cache keys so all tracks hit the same preflight-cached entry
even when Soulseek metadata spells the artist differently. A post-batch
consistency pass (run_album_consistency) rewrites album-level tags on all
files after the batch completes — the safety net that prevents Navidrome
album splits even when per-track lookups drift.
New POST /api/v1/request endpoint accepts a search query from external
sources (Discord bots, Home Assistant, curl) and triggers the
search-match-download pipeline asynchronously. Returns a request_id
for status polling via GET /api/v1/request/<id>. Optional notify_url
for callback on completion.
Also adds webhook_received trigger type and search_and_download action
type to the automation engine, so users can build custom flows like
"when webhook received → search & download → notify Discord".
Includes info panel in Settings showing endpoint URL and curl example.
Two bugs: (1) 'wishlist' was missing from the settings save whitelist,
so the toggle silently reset to ON on every page reload. (2) The
wishlist cleanup function unconditionally removed tracks sharing the
same name+artist regardless of album, ignoring the allow_duplicates
setting. Now when allow_duplicates is on, the dedup key includes the
album name so same song from different albums can coexist.
Prints all SOULSYNC_* environment variables before OAuth servers start,
helping diagnose reported Unraid issue where callback port env var is
present in the container but not seen by the Python process.
Auth instruction pages and log messages now use the actual configured
callback port instead of hardcoding 8888. Added startup logging that
prints whether SOULSYNC_SPOTIFY/TIDAL_CALLBACK_PORT env vars were
detected, helping diagnose Unraid/Docker env var issues. Also fixes
uses_main_port detection for custom callback ports and moves the
wishlist button handler to global init so it works on all pages.
Explored status was stored only in frontend memory; on reload the badge
disappeared because the API never returned it. Added explored_at column
to mirrored_playlists (auto-migrated), written when build-tree completes,
and read back via SELECT * so the badge survives page refreshes.
- Pass playlist image_url to _run_sync_task from all source-specific sync
start handlers (Deezer, Tidal, Spotify public, YouTube, automation mirror)
— previously only the /api/sync/start endpoint passed it
- Fix plex_client.set_playlist_image: use uploadPoster(url=) instead of
uploadPoster(data=) which is not a valid PlexAPI argument
- deezer_client: use picture_xl > picture_big > picture_medium fallback
for better cover art resolution
- tidal_client: extract image_url in get_playlist() from JSON:API
relationships (was only extracted in metadata-only listing)
- parse_youtube_playlist: capture playlist thumbnail from yt-dlp result
- Add visible logging for image upload attempts and outcomes
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
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
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.