Phase 1c.2.1 splits each wishlist invocation into per-album sub-
batches so the album-bundle dispatch can engage once per album.
Side effect: the completion handler ``finalize_auto_wishlist_completion``
ran end-of-run logic (cycle toggle + state reset + automation
event emit) once per BATCH, so a 2-album run fired the cycle
toggle twice + emitted two ``wishlist_processing_completed``
events. The cycle landed at the right value either way but the
state machine had become per-batch instead of per-run.
Fix: reify "wishlist run" as a first-class concept via a shared
``wishlist_run_id`` UUID. Generated once per wishlist invocation
in both the auto- and manual-wishlist paths, stamped on every
sub-batch row in ``download_batches``.
``finalize_auto_wishlist_completion`` now reads the completing
batch's ``wishlist_run_id`` and, when present, scans
``download_batches`` for siblings still in pre-terminal phases.
If any sibling is still active, the per-batch summary records
but the cycle toggle + state reset + automation emit are
deferred. Only the last completing sibling fires the run-level
finalization. Legacy single-batch runs (no run_id field) keep
their toggle-immediately behavior — back-compat by absence.
The run_id also lays groundwork for frontend grouping (one
logical row in the Downloads view per wishlist run instead of N
sibling rows), but that UX work is deferred.
3 new tests in ``test_processing.py`` pin: defer-when-siblings-
active, toggle-when-last-sibling-done, back-compat-without-run_id.
1 new assertion in ``test_automation.py`` confirms all sub-batches
of one auto-wishlist invocation share the same run_id. 309 tests
across wishlist + automation suites green.
Notes: dispatch concurrency unchanged — sub-batches still run via
the shared download worker pool. Slskd serializes per-uploader at
its own layer (same uploader = automatic queue, different
uploaders = legit parallel), so SoulSync-side serial enforcement
would duplicate work the right layer already handles.
Auto-wishlist's "albums" cycle used to dump every missing album
track into one batch and run per-track Soulseek / Prowlarr searches
for each (~50 searches for a typical scan). The album-bundle
dispatch (introduced in 2.5.9 for explicit album downloads) was
gated on ``is_album_download=True`` + populated
``album_context``/``artist_context``, none of which the wishlist
batch ever set — so wishlist runs always took the per-track flow
even when 12 missing tracks all belonged to the same album.
Fix: split wishlist albums-cycle tracks into per-album sub-batches
at submission time. Each sub-batch carries its own album context,
trips the existing dispatch gate, and engages one slskd / torrent
/ usenet album-bundle search per album. Tracks the helper can't
group (no album metadata, no artist) fall through to a residual
per-track batch.
- New ``core/wishlist/album_grouping.py``:
``group_wishlist_tracks_by_album(tracks)`` returns
``WishlistGroupingResult(album_groups, residual_tracks)``.
Pure function — extracts album_id (or name-normalized fallback)
+ primary artist + album context from each track's nested
spotify_data, buckets, and threshold-promotes. Independent of
runtime state so it can be unit-tested without the wishlist
executor.
- ``core/wishlist/processing.py``: when ``current_cycle ==
'albums'``, run the grouping helper, submit one batch per album
with ``is_album_download=True`` + the group's album/artist
context, then a single residual batch for orphans. Singles
cycle path unchanged.
- 9 new tests in ``test_album_grouping.py`` pin the bucketing
contract (empty / single album / multi album / orphan / threshold
/ nested payloads / no-id fallback / no artist).
- 2 new tests in ``test_automation.py`` exercise the per-album
split end-to-end through ``process_wishlist_automatically``:
multi-album batch → two sub-batches each with album context;
mixed orphan + real album → one bundle batch + one residual.
1099 tests across wishlist + imports + downloads + automation +
playlist-sources + staging-provenance + track-number-repair
suites green. WHATS_NEW entry added under 2.6.3.
Now when an auto-wishlist scan finds 12 missing tracks from
Ryoto's "Cha-La Head-Cha-La", it runs ONE slskd / Prowlarr
album-bundle search for the release instead of 12 per-track
searches.
- remove the redundant wishlist-service injection from the runtime wrappers
- keep the package owning its own singleton service access
- simplify the route runtime API and update the wishlist tests to match
- add direct service and presence coverage
- pin resolver, processing, route, and payload edge cases
- keep wishlist package extraction safe for future refactors
- add core/wishlist as the home for wishlist payload, resolution, state, processing, reporting, and selection helpers
- move wishlist-specific tests into tests/wishlist alongside the new package layout
- keep web_server.py and the import/search callers as thin adapters for now