SoulSync 2.6.3 — Merge dev → main
Bug-fix + UX-polish release. Headline items:
- Soulseek album-bundle downloads no longer hang on
failedafter slskd completes the release (#715). - Auto-Sync ListenBrainz pipelines no longer sit on
Refreshing:for 5+ minutes silently. - Dashboard enrichment panel redesigned from 11 cramped speedometers into a vibey VU-meter equalizer row with brand logos.
- Auto-Sync manager got a full visual overhaul to match the dashboard's glassy / accent-radial aesthetic, plus the Weekly Board cards now mirror the hourly board cards.
- Sync page tabs collapsed from 14 wrapping pills into circular brand-logo chips with an active label pill.
- Album-bundle staging auto-cleans Soulseek copies + sweeps orphan dirs at startup.
488 downloads tests pass. ~80 new unit tests across the cycle.
Fixes
#715 — Soulseek album downloads stuck on "failed" after slskd finished the release
core/soulseek_client._resolve_downloaded_album_fileprobed 3 hard-coded candidate paths to locate each completed file in the slskd download dir. On the common slskd configdirectories.downloads.username = true, files land at<download_dir>/<username>/<filename>— none of the 3 candidates carried a username segment, so every file looked locally missing and the bundle poll silently spun for ~30 minutes before marking the batch failed.- Fix: lifted the per-track flow's recursive walk-by-basename helper into
core/downloads/file_finder.py(find_completed_audio_file). Bundle resolver now delegates to it. Default-slskd users see zero behavior change (3-candidate fast path preserved); username-subdir / preserved-tree users get the recursive walk fallback. - Belt-and-suspenders: bundle poll now detects "slskd reports Completed but local file can't be resolved past a 45s grace window" and exits early with an explicit log line pointing at the likely
soulseek.download_pathmismatch — no more silent 30-min spins. - Also fixed: misleading
"(0 tracks, quality=)"log on the preflight-reuse path (was reading attrs off a Nonepickedobject). - 17 new tests pin every slskd layout (flat, username-prefixed, full-tree-preserved, deep nested, dedup-suffix, quarantine-skip, YouTube/Tidal encoded, transfer-dir fallback, fuzzy variants).
Auto-Sync ListenBrainz pipelines stuck on Refreshing: for 5+ minutes
- Refresh path ran
_maybe_discover(matching engine, per-track Spotify/iTunes/Deezer matches) inline for any source returningneeds_discovery=Truetracks. Phase 2 of the pipeline then ran the SAME matching engine viarun_playlist_discovery_workeron the same tracks. LB tracks discovered twice; the refresh-side run blocked with zero progress emission. - ListenBrainz manager only exposed
update_all_playlists— refreshing one playlist re-pulled all 12+ cached LB playlists' details from the API. - LB adapter had a silent
except Exception: passmasking real API failures as stale-cache hits. - Fix:
- Pipeline sets
skip_discovery=Trueon its refresh config; Phase 2 handles discovery with proper progress emits. - New
LBManager.refresh_playlist(mbid)targeted refresh — pulls just the requested playlist's details. - LB adapter logs exceptions with traceback at warning level + returns
Noneso the outer handler counts the error.
- Pipeline sets
- 12 new tests pin the skip-discovery flag, targeted refresh, error propagation, and synthetic series id resolution.
Wishlist: harden Spotify backfill — poisoned tn=1 can't mask a lean album
- Residual per-track wishlist downloads produced folders without a year subfolder whenever the wishlist row carried a stale
track_number=1from an older payload default. - The Spotify-API backfill that hydrates
release_date/total_trackswas coupled to the "track_number missing" branch, so poisoned tn=1 short-circuited it. - Fix: lifted to
core/downloads/track_metadata_backfill.py(hydrate_download_metadata). Track-number resolution keeps its precedence chain; album hydration now runs wheneverrelease_date/total_tracksare missing, independent of where (or whether)track_numberwas resolved. Single API call still serves both. - Also:
core/wishlist/routes.py:_build_track_datano longer defaultstrack_number=1/disc_number=1/total_tracks=1/release_date=''when the library-modal payload omits them. - 24 new tests cover the precedence chain, the poisoned-tn=1 regression case, defensive non-dict inputs, cost guard, disc_number resolution.
Wishlist: fix three regressions causing all imports to land as track 01
- Three stacked regressions: Track→dict conversion in payload helpers dropped everything except
album.name; Deezer-sourced discovery matches saved withouttrack_number/disc_number; import pipeline only consultedalbum_info.track_numberbefore falling to the filename (which fails for VA-collection filenames). - All three patched; track_number resolution chain lifted into
core/imports/track_number.py:resolve_track_numberas a pure function with 18 unit tests pinning every branch.
Wishlist: only engage album-bundle when several tracks from the same album are missing
- Auto-wishlist runs promoted every single-track wishlist item to a per-album bundle search — so a wishlist of "26 single tracks from 26 different albums" downloaded full albums (5-42 files each, ~85% wasted bandwidth) and hammered slskd with concurrent searches.
- New
core/wishlist/album_grouping.pygroups tracks by album. Bundle path only engages when an album has ≥2 missing tracks; single-track items take the cheaper per-track path. - Configurable via
wishlist.album_bundle_min_tracks.
Wishlist: distinguish Queued from Analyzing batches in the UI
- Wishlist runs with more than 3 sub-batches rendered every batch as "Analyzing..." simultaneously, even though the download worker pool only ran 3 at a time. Now batches show
Queued ⏳while parked in the executor; flip toAnalyzing...when a worker picks them up.
Album-bundle staging: clean Soulseek copies + sweep orphans at startup
_cleanup_private_album_bundle_stagingwas gated to torrent / usenet only. Soulseek's bundle path ALSO copies files into staging — those copies leaked forever. Extended the allow-list to includesoulseek.- New
sweep_orphan_album_bundle_stagingruns once at server startup, removes orphan<batch_id>subdirs left behind by previous-session crashes / errored batches / pre-fix Soulseek bundles. Safe by construction (name-shape guard, rmtree errors swallowed). - 12 new tests cover orphan removal, active preservation, defensive guards.
Usenet album poll: tolerate SAB queue→history handoff (#706)
- SAB jobs in the queue→history transition window briefly appear in neither, and the poll treated one missing read as terminal failure. Now tolerates ~10s of transient misses, treats unmapped queue states as transient, fires terminal failed event on every failure path, queries by
nzo_iddirectly. Same tolerance applied to torrent polls.
Discogs: strip artist disambiguation suffixes everywhere (#634)
- Discogs marks duplicate-named artists with
(N)or trailing*. Centralized cleanup into one helper applied at every Discogs name surface (artist search, album search, track lookups, get_artist_albums).
Library: Enhanced / Standard view toggle persists per browser
- Artist detail page reverted to Standard view on every navigation. Now persisted in localStorage and reapplied automatically.
Fix popup: manual matches survive Playlist Pipeline runs
- Manually-mapped tracks reverted on the next pipeline run because (a) manual fix saves always stamped
provider: 'spotify'even when the match came from MB / iTunes / Deezer, (b) the discovery worker re-queued any matched_data lackingtrack_number/album.id/release_date(Fix-popup saves don't carry those by design), (c) prepare-discovery didn't honor themanual_matchflag. All three patched.
Fix popup: artist + track fields no longer surface unrelated covers
- Fields-mode now uses a field-scoped Lucene query that anchors the artist, with the old bare query kept as a fallback for diacritic mismatches. Results stable-prefer entries with known track length.
UX overhauls
Dashboard enrichment panel: equalizer-bar redesign
- 11 speedometer tiles → 11 vertical VU-meter equalizer bars in one symmetric flex row.
- 4% sliver floor on idle bars; continuous shimmer scan when running; slow breathing pulse when idle; red pulsing variant when rate-limited; white-hot peak tip in the service's accent.
- Next-level polish: brand-logo avatar disc above each bar (Spotify/Apple Music/Deezer/Last.fm/Genius/MusicBrainz/AudioDB/Tidal/Qobuz/Discogs/Amazon, with initial-letter fallback on CDN failure); peak-flash detector when
cpmsteps upward; rolling number counter with easeOutCubic; glass-surface reflection puddle that intensifies with bar height. - Last.fm avatar clipped to a circle; Tidal/Qobuz/Discogs/Amazon inverted to white silhouettes for legibility against the dark glass disc.
Auto-Sync manager: full visual overhaul
- Selector-based override layer (zero JS/HTML changes, functionality untouched). Touched every surface in the modal:
- Modal shell: glass + accent radial wash + thin accent border, dashboard
.dash-cardarchitecture - Header: gradient-clipped title, accent eyebrow, hairline accent separator, rotating-X close
- KPI summary tiles: dashboard gradient tiles with accent top-edge glow on hover + gradient stat numbers
- Live monitor strip: accent glass card, status-colored borders
- Buttons: accent pill primary
- Tabs: underline + accent fill + radial glow on active
- Sidebar: glass groups, accent border on scheduled tiles, accent ring on filter focus
- Board/columns: accent radial spotlight, gradient column headers, accent drag-over glow
- Drop zones: animated dashed pull with accent radial wash
- Scheduled cards: accent left-edge stripe, gradient pill timing badges, pill Run-now, rotating ghost X
- History rows: dashboard recent-activity aesthetic, pill status badges with colored borders
- Bulk popover + Weekly editor: glass overlays matching the version-modal vibe
- Accent-tinted scrollbars throughout
- Modal shell: glass + accent radial wash + thin accent border, dashboard
Auto-Sync: weekly board cards now match the hourly board
- Weekly cards had diverged from the hourly card visual (no Run-now button, no unschedule X, no next-run countdown, no health badge). Standardised on the hourly shape so dropping onto a day column produces a card with the exact same affordances. Click anywhere outside the buttons still opens the weekly editor. Weekly cards now draggable between day columns.
Auto-Sync sidebar: brand logo on each source-group header
- 18px circular brand-logo chip to the left of each source-group title (Spotify / Tidal / Qobuz / Deezer / YouTube / Last.fm / ListenBrainz / iTunes Link / SoulSync Discovery / Spotify Link). Same logo vocabulary as the dashboard equalizer + header orbs. Last.fm circle-clipped; Tidal/Qobuz/iTunes-Link inverted to white silhouette.
Sync page tabs: brand-logo chips with active label pill
- 14 tabs no longer fit in one labeled-pill row. Collapsed to circular brand-logo chips; active tab swells into a pill with its label inline. Per-source brand color drives hover ring + active fill.
Linkvariants (Spotify Link / Deezer Link / iTunes Link) carry a small chain-link badge bottom-right to disambiguate from their native-source siblings.
Architectural lifts (Phase 1 of the Playlist Source unification, shipped)
Unified Playlist Sources layer
PlaylistSourceABC + registry incore/playlists/sources/. Refresh handler dropped from ~190 lines of if/elif to ~80 lines. ListenBrainz / Last.fm / SoulSync Discovery are now Sync-page tabs alongside Spotify / Tidal / Qobuz / YouTube. LB rolling-series mirrors.
Auto-Sync schedule types: weekday + time
- New Weekly Board tab on the Auto-Sync manager. Drag a playlist onto a day column (Mon-Sun) to schedule it for that weekday at a default time, then click for an editor for multi-day picks, custom time, and IANA timezone.
iTunes / Apple Music link import
- New iTunes Link tab on the Sync page. Paste an Apple Music album, track, or playlist URL and SoulSync pulls the tracklist through the same discovery → sync → download flow as the other link tabs.
Test plan
- 488 downloads tests pass after the cycle
- 80+ new unit tests across the new modules:
tests/downloads/test_file_finder.py— 17 tests (slskd layout coverage for #715)tests/downloads/test_track_metadata_backfill.py— 24 tests (wishlist tn=1 regression)tests/downloads/test_album_bundle_staging_sweep.py— 11 tests (orphan cleanup)tests/imports/test_track_number_resolver.py— 18 tests (per-track wishlist track number)tests/test_listenbrainz_manager.py— 8 tests (targeted LB refresh)tests/test_playlist_sources_adapters.py— 3 new tests (LB adapter refresh routing)tests/automation/test_handlers_playlist.py— 1 new test (skip_discovery flag)
- Smoke: dashboard enrichment row renders with brand logos, equalizer bars animate, peak-flash fires on cpm increase
- Smoke: Auto-Sync manager opens with new glass styling, hourly + weekly cards both render the full action row
- Smoke: Sync page tab strip renders as logo chips; active expands to label pill; Link variants show chain-link badge