SoulSync 2.7.3 — Merge dev → main
patch release on top of 2.7.2. one new feature (Quality Upgrade Finder), a Tidal-discovery completeness fix, and a stack of import / wishlist / quarantine bug fixes.
Headline:
- Quality Upgrade Finder — new findings-based job that finds tracks you own in worse quality than is available and upgrades them (ISRC-first, tiered matching, dedup-skip, duration guard). replaces the old auto-acting Quality Scanner.
- #867 — Tidal playlist discovery capped at ~21 tracks → now walks the whole playlist, and the modal opens instantly instead of freezing ~10s. Reported against 2.7.2.
- #880 — Tidal "Favorite Tracks" mirror truncating to ~98 of 500+ on a transient 429. Reported by @Yug1900.
- #879 — opening Settings reset the whole config to defaults after a failed
GET /api/settings. Reported by @Lysticity (2.7.0–2.7.2). - #877 — Download Discography filters didn't match Artist Detail (dead EPs toggle; no Live/Compilations/Featured). Reported by @HolyFredy.
- #876 — Quarantine duplicates + stale count. Reported workflow pain: many failed attempts at one song, and a stale tab count on open.
- #874 — wishlist re-downloads a removed/cancelled track forever. New TTL'd ignore-list.
- "Track 01" bug — single tracks (esp. Deezer) imported as
01regardless of real album position.
New
Quality Upgrade Finder
Replaces the old auto-acting Quality Scanner with a findings-based repair job: it scans the library for tracks you own in worse quality than a source can provide, surfaces them as findings, and you choose what to upgrade — nothing acts on your library behind your back.
- Tiered structured matching: ISRC-first exact match (using the IDs enrichment already embedded) → album→track → artist+title.
- Best-in-class picker: a direct track-ID tier for exact source matches, a dedup-skip so the same upgrade isn't re-found, and a duration guard so a wrong-but-similarly-named track can never be swapped in.
- Old auto-acting Quality Scanner tool removed.
Fixes
#867 — Tidal playlist discovery only showed ~21 tracks
The discovery walk was capped; it now pages the full playlist. Plus a UX pass: the discovery modal renders before the blocking discovery-start POST and opens in its discovering state, so it no longer freezes the UI for ~10s on a track pre-fetch or leave an empty/loading modal interactable.
#880 — Tidal "Favorite Tracks" mirror captured ~98 of a 500+ collection
The cursor paginator (_iter_collection_resource_ids) bailed on any non-200 — including a transient 429 mid-walk — truncating a 524-track collection to 98. It now retries the same cursor page with backoff (5/10/15/20s, up to 4 attempts), mirroring the playlist paginator. 401/403 still bail + set the reconnect flag; other non-200s still break. 3 regression tests.
#879 — opening Settings reset the entire config to defaults
loadSettingsData() called response.json() without checking response.ok, so a failed GET /api/settings (its error body) was treated as settings → every field fell back to blank → autosave wrote the blanks over the real config. Fix (settings.js): bail before touching any field on a non-ok/error load, set window._settingsLoadFailed, and guard both save paths on it (cleared on next good load). Any load failure now leaves the saved config untouched. Regression test pins redacted_config stays a callable method.
#877 — Download Discography filters didn't match Artist Detail
The modal used the base get_artist_discography (which lumps EPs into singles) and only read {albums, singles} — so its EPs bucket was always empty (dead toggle), with no Live/Compilation/Featured classification. Now uses get_artist_detail_discography (same split Artist Detail uses) + a shared _classifyReleaseContent() adopted by both surfaces so they can't drift; adds Live/Compilations/Featured filters; the download payload is built from visible checked cards. Regression test pins EPs land in their own bucket.
#876 — Quarantine workflow: duplicates + stale count
- Group duplicates + auto-clear: failed attempts at the same song group by their intended target track-id (isrc → source id → uri, name fallback for legacy sidecars) and render as a collapsible group; approving one auto-deletes the sibling alternatives. Hooked so the success-cleanup path is never mistaken for a duplicate.
- Stale count: the quarantine tab badge now shows the real count the moment the modal opens, not a stale
0until you click the tab. - 13 tests incl. the capture-before-approve ordering regression.
#874 — wishlist re-downloads a removed/cancelled track forever
A removed or user-cancelled wishlist track used to be re-added on the next auto cycle (watchlist scan, failed-capture, cancel re-add), so the same release downloaded → failed → re-queued endlessly. New TTL'd ignore-list (core/wishlist/ignore.py + wishlist_ignore table) gates the single add_to_wishlist funnel: softer than the blocklist (expires after 30 days, never blocks a manual force-download), fail-open throughout, manual add bypasses + clears. New "Ignored" view to see/undo. 13 tests incl. the success-cleanup-does-not-ignore regression.
"Track 01" bug — single tracks imported as 01
A single Deezer track is matched via /search/track, which omits track_position, so the import context never carried the real number — then service.py/context.py fabricated a confident track_number=1 that beat the real source. Fix: stop fabricating 1 (default to the 0 "unknown" sentinel), and recover the position from the downloaded file's own embedded tag (mutagen, no network), consulted last — only when metadata + filename both come up empty, so it can never override a value the old resolver produced (strictly additive, no regression for albums). 13 resolver tests incl. the no-regression precedence guards.
Smaller fixes
- #870 — Deezer ARL "resets itself": the connection test ran against the redacted mask instead of the saved token, so a valid ARL read as broken. Tests the saved token now.
- #868 — same-name artist mis-enriched: an artist sharing a name with another got the wrong Standard discography. Disambiguated by owned-catalog overlap during enrichment; a re-match clears the stored id.
- Find & Add: a
Title - Remixsearch now matches the base-titled track in your library. - Normalization: a colon in a title (
T:T) now matches an underscore variant (T_T). - Sidebar UI: frosted-glass header blur, vertically-centered nav count badges, and the My-Accounts / Personal-Settings buttons hidden for admins (who use the global app account).
Earlier in 2.7.x (carried, already shipped)
- 2.7.2 — playlist-folder mirroring, server-playlist M3U export, follow-only watchlist, SoundCloud-link + better YouTube imports, ReplayGain Filler + Empty Folder Cleaner maintenance jobs, HiFi restore-defaults.
- 2.7.1 — download verification (AcoustID fingerprint-checks every download against what you asked for) + an unverified review queue; closed the websocket login-bypass (#852).
- 2.7.0 — multi-user for real: per-profile streaming accounts (My Accounts), opt-in username/password login with recovery, reverse-proxy support.
Test plan
- full imports + wishlist suites green (no regressions from the Track-01 de-poison or the ignore-list)
- 13 quarantine ignore-list tests (#874) incl. success-cleanup regression
- 13 quarantine-grouping tests (#876) incl. capture-before-approve ordering
- 13 track-number resolver tests incl. embedded-tag precedence / no-regression guards
- 3 Tidal 429-retry regressions (#880); settings-redaction regressions (#879); EP-split regression (#877)
-
ruff check .clean - Live: re-mirror Tidal Favorites grabs the full collection (#880)
- Live: Deezer single-track import lands at its real album position (Track-01)
- Live: Quality Upgrade Finder surfaces + upgrades a known lower-quality track
Post-merge checklist
- Tag
v2.7.3onmain - Trigger
docker-publish.ymlwithversion_tag: 2.7.3(default already bumped) →boulderbadgedad/soulsync:2.7.3+ghcr.io/nezreka/soulsync:2.7.3 - Discord release announcement (auto-fired by the workflow)
- Reply on #867 / #880 / #879 / #877 / #876 / #874 / #870 / #868 with the 2.7.3 release link