From 6d5420371005970891dd7c69b374d877c52a27e4 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 28 May 2026 08:17:43 -0700 Subject: [PATCH] Bump version to 2.6.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _SOULSYNC_BASE_VERSION → 2.6.4 - helper.js: '2.6.4' unreleased → 'May 28, 2026 — 2.6.4 release' - .github/workflows/docker-publish.yml default version_tag → 2.6.4 - pr_description.md: rewrite for 2.6.4 with #721 as the headline patch, 2.6.3 fixes carried forward unchanged (2.6.3 was bumped on dev but never reached main / docker, so 2.6.4 is the first release to ship this batch) --- .github/workflows/docker-publish.yml | 4 +- pr_description.md | 156 +++++++++++++++++---------- web_server.py | 2 +- webui/static/helper.js | 2 +- 4 files changed, 102 insertions(+), 62 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 65014e2a..cc63693b 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -9,9 +9,9 @@ on: workflow_dispatch: inputs: version_tag: - description: 'Version tag (e.g. 2.6.3)' + description: 'Version tag (e.g. 2.6.4)' required: true - default: '2.6.3' + default: '2.6.4' jobs: build-and-push: diff --git a/pr_description.md b/pr_description.md index 83fda10a..6b9f791d 100644 --- a/pr_description.md +++ b/pr_description.md @@ -1,76 +1,116 @@ -## Summary +# SoulSync 2.6.4 — Merge `dev` → `main` -Adds torrent and usenet as release-oriented download sources backed by Prowlarr and the configured downloader clients. These sources can download full releases, stage the resulting audio files, and let SoulSync match/import the requested tracks through the existing post-processing pipeline. +Patch release on top of 2.6.3. Headline: -This PR also improves the torrent/usenet user experience with clearer release-download progress, source-aware service labels, library history provenance, and safer staged-release matching for album files that include featured-artist or bonus-track filename noise. +- **#721 — Usenet album bundles stuck on "downloading release" when SAB History flips before storage lands.** Reported by @IamGroot60 against 2.6.3, validated on the `fix/usenet-bundle-save-path-handoff` branch, merged via PR #723. -## Scope +Everything from 2.6.3 also rolls in unchanged (was bumped on dev but never tagged / published to main / docker, so this is the first time these changes reach users). -- Adds torrent and usenet plugin support for release downloads. -- Adds album-bundle staging for single-source torrent/usenet album downloads. -- Keeps hybrid album downloads on per-track-capable sources by excluding torrent/usenet from hybrid album per-track searches. -- Allows torrent/usenet in hybrid for non-album track downloads, such as playlist singles and wishlist tracks. -- Selects the requested audio file from completed release folders instead of importing the first audio file blindly. -- Reports live album-bundle progress to the Downloads page and download modal. -- Records torrent/usenet provenance in library history. -- Adds UI polish for release-first download states. +--- -## Behavior Gates +## 2.6.4 — the patch -- Users who do not select torrent or usenet as a download source should not hit the new download paths. -- Single-source `torrent` / `usenet` album downloads use the release staging flow. -- Hybrid album downloads skip torrent/usenet and continue through the existing per-track source chain. -- Hybrid non-album downloads may use torrent/usenet when they are included in the hybrid order. +### #721 — Usenet album bundle stuck on "downloading release" when SAB History flips before `storage` lands +Follow-up to the 2.6.3 queue→history handoff fix (#706). 2.6.3 covered the gap where SAB removes a job from the queue before adding it to history. **2.6.4** covers a second-stage gap: SAB flips `status` to `Completed` in History a few seconds **before** its post-processing writes the final `storage` field. -## Notable Implementation Details +Pre-fix: `poll_album_download` saw the first `Completed` read with `save_path=None` and bailed. The bundle plugin marked the batch failed, but the UI froze on the last `downloading progress=0.61` emit because the terminal `failed` emit never registered (renderer holds the last-known progress). -- Torrent/usenet release downloads use private per-batch staging folders under the configured album-bundle staging root. -- Post-processing receives the real source label (`torrent` / `usenet`) so library history no longer falls back to `Soulseek`. -- Staging matching strips only conservative noise like `(feat. Artist)` and `(Bonus Track)` while preserving meaningful version text like `remix`, `extended`, `live`, and `acoustic`. -- When a staged release does not contain a requested track, the task is marked not found instead of repeatedly searching/downloading the same release. -- Completed release downloads expose all discovered audio files so post-processing can choose the best matching track. +- **`poll_album_download`**: separate transient counter for "completed but no save_path." Tolerates up to `transient_miss_threshold` (default 5) consecutive reads in that state — gives SAB ~10s to land the path. When it arrives, return normally. When it doesn't, fail loudly with an explicit error pointing at the missing field. +- **Sticky save_path**: earlier `downloading` reads with a non-empty `save_path` (qBit / Transmission set this from the start) remain cached. So torrent flows aren't affected by the retry path. +- **SAB adapter (`_parse_history_slot`)**: widened the save_path fallback chain — `storage` → `path` → `download_path` → `dirname`. Covers SAB version differences (older builds populated `path`) and forks that expose `download_path` or `dirname`. Whitespace-only values skipped. `incomplete_path` intentionally NOT in the chain — it'd bypass the retry window and point at the in-progress staging dir. +- **Diagnostic**: loud debug log when none of the known fields land, dumping the slot keys so we can grow `_HISTORY_SAVE_PATH_KEYS` if a fork ships a novel field name. -## Testing +**9 new tests**: +- `test_album_bundle.py` (3): late-save_path arrival recovers; threshold-exhausted fails cleanly; sticky save_path keeps torrent flows working. +- `test_usenet_client_adapters.py` (6): each fallback field tier, whitespace-only skip, all-empty returns None, `incomplete_path` ignored. -Manually tested: +132 album-bundle + usenet tests pass. Strictly additive — zero impact on users whose SAB returns `storage` on the first Completed read. -- Torrent-only album download for `good kid, m.A.A.d city (Deluxe)`. -- Torrent playlist/single-track flow. -- Album-bundle progress in the download modal and Downloads page. -- Torrent source labeling in library history for new imports. -- Staging match behavior for featured-artist filenames and bonus-track labels. -- Quarantine behavior for wrong-version matches. +--- -Automated tests run during development: +## Everything else from 2.6.3 (carried forward) -```bash -.venv/bin/python -m pytest \ - tests/downloads/test_downloads_status.py \ - tests/test_album_bundle_dispatch.py \ - tests/downloads/test_downloads_staging.py \ - tests/test_torrent_usenet_plugins.py -``` +### Fixes -```bash -.venv/bin/python -m pytest \ - tests/downloads/test_downloads_validation.py \ - tests/test_manual_pick_no_auto_retry.py \ - tests/downloads/test_downloads_post_processing.py \ - tests/downloads/test_downloads_task_worker.py \ - tests/imports/test_import_side_effects.py -``` +**#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. On the common slskd config `directories.downloads.username = true`, files land at `//` — 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. -Focused checks also passed for: +- 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). +- Bundle poll detects "slskd reports Completed but local file can't be resolved past a 45s grace window" → exits early with explicit log line pointing at the likely `soulseek.download_path` mismatch. +- Misleading `"(0 tracks, quality=)"` log on the preflight-reuse path fixed. +- **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). -- staged release feature suffix matching -- bonus-track title matching -- wrong-version separation -- private torrent album staging miss handling -- torrent/usenet history source labels +**Auto-Sync ListenBrainz pipelines stuck on `Refreshing:` for 5+ minutes.** +Refresh path ran `_maybe_discover` inline AND Phase 2 ran the same matching engine via `run_playlist_discovery_worker`. LB tracks discovered twice; refresh-side run blocked with zero progress emission. Also: LB manager only exposed `update_all_playlists` (refreshing one playlist re-pulled all 12+ cached playlists). Also: LB adapter had a silent `except Exception: pass` masking real API failures. -## Reviewer Notes +- Pipeline sets `skip_discovery=True` on refresh config; Phase 2 handles discovery with proper progress emits. +- New `LBManager.refresh_playlist(mbid)` targeted refresh. +- LB adapter logs exceptions with traceback at warning level + returns `None`. +- **12 new tests**. -- This is intentionally gated behind torrent/usenet source selection, but it is still a new release-oriented download path and should be considered beta/experimental for first release. -- Remote downloader setups need SoulSync to be able to read the downloader save path. Local/all-in-one setups should be the easiest path. -- Existing library history rows that were previously recorded as `Soulseek` are not backfilled by this PR. -- Release matching is naturally fuzzier than track-native sources, so reviewer focus should stay on false positives, version handling, and staged-file selection. +**Wishlist: harden Spotify backfill — poisoned `tn=1` can't mask a lean album.** +Spotify-API backfill that hydrates `release_date` / `total_tracks` was coupled to the "track_number missing" branch, so a poisoned default-1 track_number short-circuited it. Lifted to `core/downloads/track_metadata_backfill.py` with split concerns — track-number resolution keeps its precedence chain; album hydration runs whenever `release_date` / `total_tracks` missing, independent of track_number. 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=''`. **24 new tests**. + +**Wishlist: fix three regressions causing all imports to land as track 01.** +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. Track_number resolution chain lifted into `core/imports/track_number.py:resolve_track_number` with 18 unit tests. + +**Wishlist: only engage album-bundle when several tracks from the same album are missing.** +New `core/wishlist/album_grouping.py`. 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.** + +**Album-bundle staging: clean Soulseek copies + sweep orphans at startup.** +Cleanup gate extended to include `soulseek` (was torrent/usenet only). New `sweep_orphan_album_bundle_staging` runs once at server boot. **12 new tests**. + +**Usenet album poll: tolerate SAB queue→history handoff (#706).** + +**Discogs: strip artist disambiguation suffixes everywhere (#634).** + +**Library: Enhanced / Standard view toggle persists per browser.** + +**Fix popup: manual matches survive Playlist Pipeline runs.** + +**Fix popup: artist + track fields no longer surface unrelated covers.** + +### UX overhauls + +**Dashboard enrichment panel — equalizer-bar redesign.** 11 speedometer tiles → 11 vertical VU-meter equalizer bars in one symmetric flex row. Brand-logo avatar disc above each bar (Spotify/Apple Music/Deezer/Last.fm/Genius/MusicBrainz/AudioDB/Tidal/Qobuz/Discogs/Amazon with initial-letter CDN-fail fallback); peak-flash on cpm step-up; rolling counter; glass-surface reflection puddle. Last.fm circle-clipped; Tidal/Qobuz/Discogs/Amazon inverted to white silhouettes. + +**Auto-Sync manager — full visual overhaul.** Selector-based override layer (zero JS/HTML changes). Every surface inside the modal restyled to match the dashboard's glassy / accent-radial aesthetic. + +**Auto-Sync — weekly board cards now match the hourly board.** Same Run-now button, unschedule X, next-run countdown, health badge. Weekly cards now draggable between day columns. + +**Auto-Sync sidebar — brand logo on each source-group header.** + +**Sync page tabs — brand-logo chips with active label pill.** 14 tabs collapsed from cramped labeled pills to circular brand-logo chips; active tab swells into a pill with its label inline. `Link` variants (Spotify Link / Deezer Link / iTunes Link) carry a small chain-link badge bottom-right. + +### Architectural lifts + +**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. + +**Auto-Sync schedule types — weekday + time.** New Weekly Board tab on the Auto-Sync manager. + +**iTunes / Apple Music link import.** New iTunes Link tab on the Sync page. + +--- + +## Test plan + +- [x] 132 album-bundle + usenet tests pass (the new #721 path) +- [x] 488 downloads tests pass (full suite) +- [x] ~90 new unit tests across the cycle, including 9 new for #721 +- [x] Smoke: dashboard equalizer renders w/ brand logos, peak-flash on cpm increase +- [x] Smoke: Auto-Sync manager renders glass overhaul, hourly + weekly cards both have action rows +- [x] Smoke: Sync page tab strip renders as logo chips; active expands; Link variants show chain-link badge +- [ ] Live: @IamGroot60 to re-test Forty Licks usenet bundle on dev (build with the #721 fix applied) +- [ ] Live: Soulseek album download on a username-subdir slskd config completes cleanly (#715, user-validated post-merge) +- [ ] Live: bundle staging dir cleaned on completion (user-validated post-merge) + +--- + +## Post-merge checklist + +- [ ] Tag `v2.6.4` on `main` +- [ ] Trigger `docker-publish.yml` with `version_tag: 2.6.4` to push `boulderbadgedad/soulsync:2.6.4` + `ghcr.io/nezreka/soulsync:2.6.4` (default already updated) +- [ ] Discord release announcement (auto-fired by the workflow) +- [ ] Reply on #721 with the 2.6.4 release link diff --git a/web_server.py b/web_server.py index ca4d2015..450c0ee6 100644 --- a/web_server.py +++ b/web_server.py @@ -40,7 +40,7 @@ logger = setup_logging(_log_level, _log_path) # App version — single source of truth for backup metadata, system-info, update check, etc. # Semver: MAJOR.MINOR.PATCH. Bump at each dev→main release. -_SOULSYNC_BASE_VERSION = "2.6.3" +_SOULSYNC_BASE_VERSION = "2.6.4" def _build_version_string(): """Append short commit hash to version when available (e.g. 2.35+abc1234).""" diff --git a/webui/static/helper.js b/webui/static/helper.js index 1a40fd6e..043c9b50 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3414,7 +3414,7 @@ function closeHelperSearch() { // release time and add a real `date:` line at the top of the version block. const WHATS_NEW = { '2.6.4': [ - { unreleased: true }, + { date: 'May 28, 2026 — 2.6.4 release' }, { title: 'Fix: Usenet album bundle stuck on "downloading release" when SAB History flips before storage lands (#721)', desc: 'follow-up to the 2.6.3 queue→history handoff fix. The poll now correctly handles the second-stage gap: SAB flips a job\'s ``status`` to ``Completed`` in History a few seconds before its post-processing pipeline writes the final ``storage`` field. Pre-fix the bundle poll returned ``None`` on the first ``Completed`` read with no save_path, the plugin marked the batch failed, and the UI froze on the last 61% progress emit. ``poll_album_download`` now tolerates up to ``transient_miss_threshold`` consecutive "completed but no save_path" reads, so SAB gets a window to finish writing the path. When it lands, the poll resolves normally. If the path never lands past the threshold, the poll fails loudly with an explicit error pointing at the missing save_path field instead of letting the UI sit on "downloading 61%" until the 6-hour deadline. Also widened the SAB adapter\'s ``_parse_history_slot`` save_path fallback chain to try ``storage`` → ``path`` → ``download_path`` → ``dirname`` → ``incomplete_path`` (last resort), so SAB version / fork variations resolve cleanly. Whitespace-only values are skipped. 9 new unit tests pin every case: late-save_path arrival, sticky save_path from earlier downloading emits, threshold-exhausted failure, plus 6 SAB-adapter field-fallback variants. Closes #721.', page: 'downloads' }, ], '2.6.3': [