github Nezreka/SoulSync 2.5.0
Version 2.5.0

3 hours ago

SoulSync 2.5.0 — Release

devmain. 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 yug1900 on issue #502 located the working endpoint after the prior /v2/favorites attempt returned empty data. New collection.read OAuth 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=consent on 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_pick so 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 createDiscoverSectionController factory. 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 dropped source + album_name + album_artist from 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_number collapsed 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 source field, so record_soulsync_library_entry couldn't pick the right source-id column. Every auto-imported track landed with NULL on spotify_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 raw SequenceMatcher against the primary artist while AcoustID returns the full credit. Lifted into shared core/matching/artist_aliases.py::artist_names_match with credit-token splitting on common separators.
  • AcoustID scanner: compilation albums no longer flag every track (skowl) — scanner SQL joined artists via tracks.artist_id (album artist, not per-track). tracks.track_artist column was already populated correctly by every server scan + auto-import path. Switched the SELECT to COALESCE(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. New core/matching/artist_aliases.py helper + artists.aliases column 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 had track.flac AND track.opus side-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_scan ignored the auto-detected self.music_library and called library.section("Music") with hardcoded English fallback. Música / Musique / Musik / Musica / 音乐 / موسيقى all hit NotFound.
  • Search for match: no more karaoke / cover / "originally performed by" junk at the top (issue #534, radoslav-orlov) — new core/metadata/relevance.py reranks 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 returns cover_xl URLs 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_url helper 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 NULL gate. User clicked download on a track with no source IDs → silent failure. Lifted into shared _select_discovery_tracks with the gate hard-coded so every public method inherits it.
  • Discover: source-aware popularity, library dedup, SQL genre filterpopularity thresholds 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 via LIKE placeholders. Discovery selectors now exclude tracks the user already owns via correlated NOT EXISTS subquery.

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_available gate 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

  • pytest full 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.32.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).

Don't miss a new SoulSync release

NewReleases is sending notifications on new releases.