SoulSync 2.5.0 — Release
dev → main. Minor bump (new features + fixes, no breaking changes).
Summary
Three new features, eleven fixes, one packaging change.
New features
- Tidal Favorite Tracks as a virtual playlist — favorited tracks (Tidal's "My Collection") now show up alongside real playlists on the sync page, same treatment Spotify gets for "Liked Songs". Reporter
yug1900on issue #502 located the working endpoint after the prior/v2/favoritesattempt returned empty data. Newcollection.readOAuth scope; existing tokens hit a 401 once and the sync page surfaces a "reconnect Tidal to enable" placeholder card with a hint pointing at Settings. New Disconnect button +prompt=consenton the OAuth flow so re-auth actually picks up new scopes. - Manual search in the failed-track candidates modal — when a download fails or returns "not found", the modal now has a search bar. Type any query, hit search, get fresh results from the configured download sources without restarting the whole download flow. Source picker is smart: single-source mode shows a label, hybrid mode shows a dropdown with "all sources" default. Results stream in via NDJSON as each source completes. Manual picks tagged with
_user_manual_pickso the auto-retry monitor leaves them alone — failure surfaces to the user instead of getting silently fallen back. - Discover section controller — every section on the discover page (recent releases, your artists, your albums, seasonal, fresh tape, archives, etc.) was reimplementing the same lifecycle by hand. ~30 sections all subtly drifting — different empty messages, different error handling, different sync-status icons, no consistent error toast. Lifted the lifecycle into a shared
createDiscoverSectionControllerfactory. Renderers stay per-section because section data shapes legitimately differ (album cards vs artist circles vs playlist tiles vs track rows); the controller is the wrapper, not a forced visual abstraction. 32 node-test pinning the controller contract.
Fixes
- Manual import: stop writing "Unknown Artist / album_id / 0 tracks" garbage (issue #524,
radoslav-orlov) — click handler droppedsource+album_name+album_artistfrom the match POST. Backend then guessed source via primary-source priority chain, all returned None, fell through to a failure-fallback dict with the album_id as the title. - Multi-disc albums no longer lose half the tracks — caught while testing #524 with Mr. Morale & The Big Steppers. Quality-dedup keyed on
track_numbercollapsed multi-disc releases to one disc's worth of files BEFORE the matcher ran. Track-number scoring bonus also fired across discs causing the wrong file to "win" the match. Both fixed by switching to(disc_number, track_number)tuples. - Auto-import: SoulSync standalone library now gets full server-quality rows — context dict had no
sourcefield, sorecord_soulsync_library_entrycouldn't pick the right source-id column. Every auto-imported track landed with NULL onspotify_track_id/deezer_id/ etc., and watchlist scans re-downloaded them on the next pass. Plus genre-tag aggregation onto artists row, ISRC/MBID type hardening, album duration as album total (not first-track duration), and conservative re-import UPDATE path that fills empty columns without clobbering populated ones. - AcoustID scanner: multi-artist songs no longer flagged as wrong (
foxxify) — scanner used rawSequenceMatcheragainst the primary artist while AcoustID returns the full credit. Lifted into sharedcore/matching/artist_aliases.py::artist_names_matchwith credit-token splitting on common separators. - AcoustID scanner: compilation albums no longer flag every track (
skowl) — scanner SQL joinedartistsviatracks.artist_id(album artist, not per-track).tracks.track_artistcolumn was already populated correctly by every server scan + auto-import path. Switched the SELECT toCOALESCE(NULLIF(t.track_artist, ''), ar.name). - Cross-script artist names no longer quarantine files (issue #442,
afonsog6) — Hiroyuki Sawano vs 澤野弘之, Sergey Lazarev vs Сергей Лазарев, etc. Verifier compared expected vs actual with raw_similarity(0% — no shared chars) and never consulted MusicBrainz aliases. Newcore/matching/artist_aliases.pyhelper +artists.aliasescolumn populated by MB enrichment + multi-tier resolver (library DB → cache → live MB) so the verifier finds aliases for un-enriched artists too. - Library Reorganize: stop leaving orphan audio files behind + hint for Unknown-Artist rows (
foxxify) — lossy-copy users hadtrack.flacANDtrack.opusside-by-side at the source; reorganize moved the canonical, left the orphan, and the empty-folder cleanup never fired. Plus the placeholder-metadata rows from the pre-#524 manual-import bug couldn't be relocated and emitted a misleading "run enrichment first" hint. Both addressed in the same PR. - Plex: library scan trigger no longer fails on non-English section names (issue #535,
adrigzr) —trigger_library_scanignored the auto-detectedself.music_libraryand calledlibrary.section("Music")with hardcoded English fallback. Música / Musique / Musik / Musica / 音乐 / موسيقى all hitNotFound. - Search for match: no more karaoke / cover / "originally performed by" junk at the top (issue #534,
radoslav-orlov) — newcore/metadata/relevance.pyreranks results locally with cover/karaoke/tribute penalties + exact-artist-match boost + variant-tag penalty (skipped when user explicitly typed the variant). Applied at deezer + itunes + spotify search-tracks endpoints. - Deezer cover art no longer looks blurry (
tim) — Deezer's API returnscover_xlURLs at 1000×1000 but the underlying CDN serves up to 1900×1900 by rewriting the size segment in the URL path. New_upgrade_deezer_cover_urlhelper mirrors the spotify scdn / iTunes mzstatic upgrade pattern. - Discover: stop showing undownloadable tracks — five discovery_pool selection methods had no
WHERE source_id IS NOT NULLgate. User clicked download on a track with no source IDs → silent failure. Lifted into shared_select_discovery_trackswith the gate hard-coded so every public method inherits it. - Discover: source-aware popularity, library dedup, SQL genre filter —
popularitythresholds were spotify-shaped (0–100); deezer writes its rank value (often six-digit integers). New_get_popularity_thresholds(source)returns per-source values. Genre filter pushed down into SQL viaLIKEplaceholders. Discovery selectors now exclude tracks the user already owns via correlatedNOT EXISTSsubquery.
Packaging
- Stop docker image bloat from auto-downloaded ffmpeg — yt-dlp / pydub probe for ffmpeg at import time and download a static binary if the system one isn't found. Container image grew accordingly. Added an explicit
is_availablegate so the auto-download path stays disabled in container builds.
Files / scope
50+ commits, 9 merged PRs (#525, #531, #532, #536, #539, #540, #541, #542, #543, #544, #545, #546). See webui/static/helper.js '2.5.0' block in WHATS_NEW for full per-feature breakdown.
Test plan
-
pytestfull suite green on tip of dev (2639 tests at last run, +94 since 2.4.3) - No new ruff lint findings
- Token scope verified manually after Tidal disconnect+reauth — includes
collection.read - Reorganize tested on a multi-format album (.flac + .opus side-by-side) — both files end up at destination
- Manual import via search modal verified writes correct artist/album/track to library DB
- Reviewer smoke test: load sync page, verify Tidal Favorite Tracks card appears with real count
- Reviewer smoke test: open the failed-track candidates modal, verify manual search bar appears + returns results
- Reviewer smoke test: discover page loads all sections without error toasts
Version
_SOULSYNC_BASE_VERSION bumped from 2.4.3 → 2.5.0. Sidebar, version modal, update check, backup metadata all read from this constant.
Reporters credited
yug1900 (#502), radoslav-orlov (#524, #534), afonsog6 (#442), adrigzr (#535), foxxify (Discord — reorganize, AcoustID multi-artist), skowl (Discord — AcoustID compilation), tim (Discord — Deezer cover art), bafoed (#499, #500 from prior 2.4.3 line).