Release: dev → main, 2.6.1 → 2.6.2
Cycle release bundling 85 commits since the 2.6.1 cut on main. Version string bumped to 2.6.2 (commit 26eeb1e9). Bundles the unreleased 2.6.2 block and the in-flight 2.6.3 entries currently sitting on dev — both ship under the 2.6.2 tag.
Headline changes
New Sync-page tabs (4)
The Sync page absorbs four sources that previously lived only on the Discover page or had no first-class entry point at all. Each tab uses the same discovery → mirror → Auto-Sync flow as Spotify / Tidal / Qobuz / YouTube.
- ListenBrainz — lists For You / My Playlists / Collaborative LB playlists. Once mirrored, LB playlists participate in Auto-Sync schedules + pipeline automations like any other source.
- Last.fm Radio — sibling tab to ListenBrainz. Mirrors auto-trim when the Last.fm Radio cache rotates so old radios don't pile up.
- SoulSync Discovery — personalized SoulSync Discovery playlists (decade mixes, hidden gems, popular picks, daily mixes, discovery shuffle). Clicking a card regenerates + mirrors under a stable synthetic id so the same mirror updates in place every Auto-Sync refresh.
- iTunes / Apple Music Link — paste an Apple Music album / track / playlist URL. Handles the current SPA token shape; token gets scraped from the JS bundle on first use, cached for 6 hours, refetched on 401 if Apple rotates mid-session.
Backed by a new unified PlaylistSource adapter layer (core/playlists/sources/) — refresh handler shrunk from a ~190-line if/elif chain to ~80 lines that delegate to the source registry. Each source also implements a discover method on the same interface so LB / Last.fm tracks land already matched against Spotify / iTunes via the shared engine.
Auto-Sync manager overhaul
Top-to-bottom revamp of the Playlist Auto-Sync modal:
- Performance: modal open dropped from ~1.5s to ~280ms (batched
get_all_mirrored_playlist_status_countsinstead of N per-playlist queries). Playlist sync itself is way faster on partial-match playlists thanks to a lazy per-artist track pool (4+ min → seconds on a 30-track playlist with 9 matches). - Bulk scheduling: each source group in the sidebar gets a Bulk button — schedule every playlist at once (1h / 2h / 4h / 8h / 12h / 16h / 24h / 48h / 72h / weekly, or custom interval). "Unschedule all" clears a source's schedules.
- Custom interval columns: schedules with non-standard intervals (6h, 36h, etc.) now render as their own dashed-border column instead of disappearing because they didn't match a hardcoded bucket.
- Filters + failure indicators: sidebar gets a playlist search input. Scheduled cards show a red ! badge when the last three pipeline runs failed, yellow ⚠ for at least one. Run history tab gains All / Errors / Completed filter pills, "Load more" pagination, and a "Run pipeline again" button inside the expanded detail panel.
- Restyle: the whole modal now matches the rest of the app — standard SoulSync gradient + accent-tinted border, KPI summary as inset stat tiles, auto-fill pipeline monitor, underline-style tabs, dense dark-card schedule board sidebar. Picks up the user's chosen accent color instead of hardcoded sky-blue.
- Fixes: hung-forever-on-loading-schedule bug (300k-track libraries hit a
COLLATE NOCASEjoin that couldn't use indexes — ~3000x speedup by dropping NOCASE),in libraryalways-showing-0 bug (query referenced a removedtracks.artistcolumn), timezone offset on countdowns (~8h on Pacific time), run-history modal hardening.
Wishlist + album-bundle improvements
- Wishlist albums-cycle now uses per-album release-first searches: instead of dumping every missing track into one big batch and running ~50 per-track Soulseek / Prowlarr searches, the albums cycle splits into per-album sub-batches at submission time. Each engages the existing slskd / torrent / usenet album-bundle release-first flow with the album's context.
- Cycle toggle fires once per run, not per sub-batch: each wishlist invocation now stamps every sub-batch with a shared run id; cycle flip + completion event waits for the LAST sibling to finish.
- Wishlist download modal tracks across every album: backend merges every sibling sub-batch's tasks + analysis results into the original batch id (re-indexes track positions globally, aggregates phase across siblings).
- Album-bundle downloads all landing as track 1 — the staging-file reader was using the auto-import's filename extractor that defaults to 1 when no NN- prefix is present. Now uses a strict extractor that returns 0 when no explicit prefix exists, so the downstream resolver correctly falls through to Spotify metadata.
- Whole-album downloads ending up "failed" with empty queues (#700) — staging match now pulls the trailing-title segment when a bare track-number is present between " - " delimiters so slskd filename patterns match cleanly.
- Album tracks getting requeued as singles by the wishlist (#698) — album batches now carry
source_type='album', source-context preservesalbum_context/artist_context, fallbacks default toalbuminstead ofsingle.
Fix popup / mirrored playlist back-sync
- Manual matches survive Playlist Pipeline runs — three layered fixes: provider stamping no longer hardcodes
'spotify'(uses the actual source the popup matched against), prepare-discovery's drift check honoursmanual_match, and the discovery worker short-circuits onmanual_matchbefore the incomplete-data re-discovery branch. Pre-fix: manual fixes worked once, then flipped back to "Provider Changed" on the next Pipeline cycle. - MusicBrainz Fix popup: artist + track fields no longer surface unrelated covers — split-field input goes through a strict
recording:"X" AND artist:"Y"query that anchors the artist. Falls back to bare query for diacritic mismatches (BjorkvsBjörk). Results stable-prefer entries with known length so the canonical 3:04 sibling sits above the 0:00 duplicate.
Library + UX
- Enhanced view toggle persists per browser — artist detail no longer reverts to Standard every time. Saved in localStorage scoped per profile so multiple admin profiles keep different defaults.
- Quick Actions redesigned as asymmetric bento with signature animations and softer flow lines.
- Redownload Album button on enhanced artist view restored (#699) — silently broken when script.js got split into 17 domain modules.
Quarantine / candidates
- Songs stuck in quarantine loop when picking a different candidate (#701) — manual picks via the candidates modal now skip AcoustID for that one post-process pass (matching the Approve-on-restored-quarantine path). Integrity + bit-depth gates still run.
Telegram
- Thread id support on Telegram notifications (community PR #594).
Code health
- New
core/discovery/manual_match.pyextracts manual-fix provider derivation + provider-drift checks out ofweb_server.pyroute handlers — pinned by 12 unit tests. - New
core/playlists/sources/adapter layer (PlaylistSource ABC + registry). - Auto-Sync extracted into
webui/static/auto-sync.jswith node:test contract for its helpers. - Mirrored playlist pipeline lifted into
core/playlists/pipeline.py(was inline inweb_server.py). - Pre-existing ruff lint debt cleared (5 errors blocking CI) — duplicate
urlparseimport removed, fourtry/except/passblocks tagged with# noqa: S110 — reasonper existing convention, onezip()annotated withstrict=False.
Test plan
- Full pytest: 4067 passed, 0 failed.
-
python -m ruff check .clean. - Live API smoke against local dev server for the MB Fix popup case (
Coffee Break+Zeds Dead) — canonical6e2d4a70recording with length 184000ms ranks first; Björk diacritic case (Bjork+Army of Me) returns 10/10 Björk recordings via strict-empty → bare fallback.
Closes
#594, #697, #698, #699, #700, #701, #708 (community feedback batch)