Wishlist modal: surface most-advanced live phase, not least-complete
The sibling-merge aggregator from 7f751202 used "least-complete
phase wins", which made the modal appear frozen during parallel
album bundle downloads. The task table is phase-gated to
downloading/complete/error in downloads.js — so whenever any
sibling was still in album_downloading, the merged phase stayed
there and tasks for the sibling that had advanced past its bundle
never rendered. User reported: both albums downloading on slskd,
modal blank until one completes fully.
Flip the rule: surface the most-advanced live phase so the modal
renders task progress as soon as any sibling reaches it. The
all-siblings-in-album_downloading case still surfaces
album_downloading (bundle progress UI is correct there); error
stays sticky.
Updated WHATS_NEW under 2.6.3 to describe the corrected behavior.
Two new tests pin the regression:
- downloading + album_downloading → downloading
- album_downloading + album_downloading → album_downloading
This commit is contained in:
parent
7f751202d2
commit
4555ff7eb9
3 changed files with 61 additions and 25 deletions
|
|
@ -23,8 +23,11 @@ Design notes:
|
|||
remap is applied to tasks too.
|
||||
- ``task_id`` is a uuid per task — no collision concern across
|
||||
siblings.
|
||||
- Phase aggregation surfaces the LEAST-complete pre-terminal phase
|
||||
so the modal stays "alive" until every sibling is done. Sticky
|
||||
- Phase aggregation surfaces the MOST advanced live phase so the
|
||||
modal renders task rows the moment any sibling reaches the task
|
||||
stage. The earlier "least-complete" rule made the modal appear
|
||||
frozen during parallel album_downloading because the task table
|
||||
is phase-gated to ``downloading``/``complete``/``error``. Sticky
|
||||
``error`` so failures don't get hidden by a running sibling.
|
||||
- ``album_bundle`` is picked from whichever sibling currently has
|
||||
an active bundle download — gives the user a useful progress
|
||||
|
|
@ -36,12 +39,6 @@ from __future__ import annotations
|
|||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
_PHASE_PRIORITY = (
|
||||
'analysis',
|
||||
'album_downloading',
|
||||
'downloading',
|
||||
'complete',
|
||||
)
|
||||
_ACTIVE_BUNDLE_STATES = frozenset({
|
||||
'searching',
|
||||
'downloading',
|
||||
|
|
@ -53,25 +50,40 @@ _ACTIVE_BUNDLE_STATES = frozenset({
|
|||
def _aggregate_phases(phases: List[str]) -> str:
|
||||
"""Pick the merged phase for a multi-sibling wishlist run.
|
||||
|
||||
Surfaces the MOST advanced live phase across the run so the
|
||||
modal renders task progress the moment any sibling reaches
|
||||
the task stage. Earlier ("least complete") aggregation hid
|
||||
downloading tasks behind the bundle progress UI when any
|
||||
sibling was still in ``album_downloading``, so the modal
|
||||
appeared frozen for the entire duration of the slowest
|
||||
sibling's bundle phase.
|
||||
|
||||
Rules:
|
||||
- ``error`` is sticky — if any sibling errored, surface error.
|
||||
- Otherwise return the LEAST-complete pre-terminal phase in
|
||||
priority order (analysis < album_downloading < downloading
|
||||
< complete).
|
||||
- If all siblings are ``complete``, return ``complete``.
|
||||
- Fallback to the first non-empty phase if nothing matches a
|
||||
known priority.
|
||||
|
||||
- ``error`` is sticky — if any sibling errored, surface error
|
||||
so the user notices the failure even mid-run.
|
||||
- ``downloading``/``complete`` (the phases that produce a
|
||||
task table on the modal side) WIN over earlier phases.
|
||||
Any sibling at this stage means "show tasks now". All-
|
||||
complete returns ``complete``; mixed complete + downloading
|
||||
stays ``downloading`` so the modal keeps polling.
|
||||
- ``album_downloading`` wins over ``analysis`` only when no
|
||||
sibling has reached the task stage.
|
||||
- ``analysis`` is the last resort.
|
||||
"""
|
||||
phases = [p for p in phases if p]
|
||||
if not phases:
|
||||
return 'unknown'
|
||||
if 'error' in phases:
|
||||
return 'error'
|
||||
for p in _PHASE_PRIORITY:
|
||||
if p in phases:
|
||||
if p == 'complete':
|
||||
return 'complete' if all(s == 'complete' for s in phases) else 'downloading'
|
||||
return p
|
||||
if all(p == 'complete' for p in phases):
|
||||
return 'complete'
|
||||
if any(p in ('downloading', 'complete') for p in phases):
|
||||
return 'downloading'
|
||||
if 'album_downloading' in phases:
|
||||
return 'album_downloading'
|
||||
if 'analysis' in phases:
|
||||
return 'analysis'
|
||||
return phases[0]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -66,19 +66,43 @@ def test_two_siblings_merge_tasks_with_reindexed_track_index():
|
|||
assert [t['task_id'] for t in merged['tasks']] == ['task-a1', 'task-a2', 'task-b1']
|
||||
|
||||
|
||||
def test_phase_aggregation_least_complete_pre_terminal_wins():
|
||||
"""analysis + downloading + complete → analysis."""
|
||||
def test_phase_aggregation_most_advanced_live_phase_wins():
|
||||
"""analysis + downloading + complete → downloading. Modal's task
|
||||
table is phase-gated to downloading/complete/error so we must
|
||||
surface that phase the moment any sibling has tasks, else the
|
||||
modal stays in bundle/analysis UI and tasks stay invisible."""
|
||||
primary = _status('complete')
|
||||
sibling1 = _status('downloading')
|
||||
sibling2 = _status('analysis')
|
||||
merged = merge_wishlist_run_status(primary, [sibling1, sibling2])
|
||||
assert merged['phase'] == 'analysis'
|
||||
assert merged['phase'] == 'downloading'
|
||||
|
||||
|
||||
def test_phase_aggregation_album_downloading_wins_over_downloading():
|
||||
def test_phase_aggregation_downloading_wins_over_album_downloading():
|
||||
"""Regression test for the parallel-bundle modal-blank bug:
|
||||
one sibling past its bundle into the task stage means the
|
||||
modal MUST be in 'downloading' phase, otherwise the task
|
||||
rows for the advanced sibling don't render and the user
|
||||
sees nothing while both albums actually download on slskd."""
|
||||
primary = _status('downloading')
|
||||
sibling = _status('album_downloading')
|
||||
merged = merge_wishlist_run_status(primary, [sibling])
|
||||
assert merged['phase'] == 'downloading'
|
||||
|
||||
|
||||
def test_phase_aggregation_all_album_downloading_stays_album_downloading():
|
||||
"""No sibling has reached the task stage yet — bundle progress
|
||||
UI is the right thing to show."""
|
||||
primary = _status('album_downloading')
|
||||
sibling = _status('album_downloading')
|
||||
merged = merge_wishlist_run_status(primary, [sibling])
|
||||
assert merged['phase'] == 'album_downloading'
|
||||
|
||||
|
||||
def test_phase_aggregation_album_downloading_wins_over_analysis():
|
||||
primary = _status('analysis')
|
||||
sibling = _status('album_downloading')
|
||||
merged = merge_wishlist_run_status(primary, [sibling])
|
||||
assert merged['phase'] == 'album_downloading'
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3424,7 +3424,7 @@ const WHATS_NEW = {
|
|||
{ title: 'Fix: album-bundle downloads all landing as track 1', desc: 'soulseek album-bundle downloads (and any other untagged release-staging path) were importing every track with track_number=1. the staging-file reader was using the auto-import\'s filename extractor that defaults to 1 when no NN- prefix is present in the filename — so for albums like Ryoto\'s "Cha-La Head-Cha-La" where slskd hands you bare titles, every file got "track 1" stamped on it. now the staging path uses a strict extractor that returns 0 when it can\'t see an explicit prefix, so the downstream resolver correctly falls through to the authoritative Spotify metadata and the right track numbers land in the library.' },
|
||||
{ title: 'Wishlist albums-cycle: one album-bundle search per album', desc: 'auto-wishlist runs in two cycles — albums + singles. previously the albums cycle dumped every missing track from every album into one big batch and ran a separate per-track Soulseek / Prowlarr search for each (~50 searches for a typical scan). now the albums cycle splits the wishlist into per-album sub-batches at submission time, and each one engages the existing slskd / torrent / usenet album-bundle release-first flow with that album\'s context. tracks without resolvable album metadata stay on the classic per-track residual batch so nothing falls off. singles cycle is unchanged.' },
|
||||
{ title: 'Wishlist cycle toggles once per run, not per sub-batch', desc: 'follow-up to the per-album split above. with N sub-batches per wishlist run, the cycle toggle (albums → singles or vice versa) was firing once per sub-batch as each one completed — so the cycle could flip mid-run and the automation completion event got emitted multiple times. each wishlist invocation now stamps every sub-batch with a shared run id; the completion handler waits until the LAST sibling finishes before toggling the cycle + emitting the run-complete event. cleaner state machine, future-proofs frontend grouping of sub-batches under one logical run.' },
|
||||
{ title: 'Wishlist download modal: keeps tracking after first album', desc: 'with the per-album sub-batch split, the modal\'s polling kept hitting the original batch id only and went stale as soon as the first sub-batch finished — subsequent albums downloaded fine but the modal\'s rows stopped updating. backend now merges every sibling sub-batch\'s tasks + analysis results into the response under the original batch id (re-indexes track positions globally so the table doesn\'t collide on row keys, aggregates phase across siblings, picks whichever bundle is currently active). frontend untouched — the modal sees one unified view of the whole run.' },
|
||||
{ title: 'Wishlist download modal: keeps tracking across every album', desc: 'with the per-album sub-batch split, the modal\'s polling kept hitting the original batch id only and went stale as soon as the first sub-batch finished — subsequent albums downloaded fine but the modal\'s rows stopped updating. backend now merges every sibling sub-batch\'s tasks + analysis results into the response under the original batch id (re-indexes track positions globally so the table doesn\'t collide on row keys, picks whichever bundle is currently active, aggregates phase across siblings so the moment ANY sibling reaches the task stage the modal renders task rows — earlier rule waited for the slowest sibling and left the modal looking frozen while both albums were already downloading on slskd). frontend untouched — the modal sees one unified view of the whole run.' },
|
||||
],
|
||||
'2.6.2': [
|
||||
{ date: 'May 24, 2026 — 2.6.2 release' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue