github Nezreka/SoulSync 2.6.3
Version 2.6.3

7 hours ago

SoulSync 2.6.3 — Merge devmain

Bug-fix + UX-polish release. Headline items:

  • Soulseek album-bundle downloads no longer hang on failed after 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_file probed 3 hard-coded candidate paths to locate each completed file in the slskd download dir. On the common slskd config directories.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_path mismatch — no more silent 30-min spins.
  • Also fixed: misleading "(0 tracks, quality=)" log on the preflight-reuse path (was reading attrs off a None picked object).
  • 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 returning needs_discovery=True tracks. Phase 2 of the pipeline then ran the SAME matching engine via run_playlist_discovery_worker on 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: pass masking real API failures as stale-cache hits.
  • Fix:
    • Pipeline sets skip_discovery=True on 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 None so the outer handler counts the error.
  • 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=1 from an older payload default.
  • The Spotify-API backfill that hydrates release_date / total_tracks was 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 whenever release_date / total_tracks are missing, independent of where (or whether) track_number was resolved. Single API call still serves both.
  • Also: core/wishlist/routes.py:_build_track_data no longer defaults track_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 without track_number/disc_number; import pipeline only consulted album_info.track_number before 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_number as 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.py groups 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 to Analyzing... when a worker picks them up.

Album-bundle staging: clean Soulseek copies + sweep orphans at startup

  • _cleanup_private_album_bundle_staging was gated to torrent / usenet only. Soulseek's bundle path ALSO copies files into staging — those copies leaked forever. Extended the allow-list to include soulseek.
  • New sweep_orphan_album_bundle_staging runs 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_id directly. 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 lacking track_number/album.id/release_date (Fix-popup saves don't carry those by design), (c) prepare-discovery didn't honor the manual_match flag. 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 cpm steps 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-card architecture
    • 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

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. Link variants (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

  • PlaylistSource ABC + registry in core/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

Don't miss a new SoulSync release

NewReleases is sending notifications on new releases.