fix: stop offering a dead Start button on queued downloads (closes #1081)

A download waiting for a MAX_CONCURRENT_DOWNLOADS slot sat at status
'pending' — the same status as an item added with auto-start off, which
is waiting for the user to press Start. The Downloading table draws its
Start button for exactly that status, so every queued row offered one.

Pressing it did nothing. start_pending() looks the id up in self.pending
first, and a queued download is not there; the fallback branch only acts
on 'scheduled' items, so the call fell through and still returned
{'status': 'ok'} — the UI reported success for a no-op.

One status name was covering two different states. A download in
self.queue waiting on the semaphore is now 'queued', leaving 'pending'
to mean only "waiting for you to press Start". The template condition is
unchanged and now excludes these rows by construction, and a 'Queued'
badge says why the row is idle instead of leaving a bare empty bar.

start_pending() notifies on promotion as well: with the slots saturated
the wait before Download.start() reports 'preparing' is unbounded, and
until something lands the client keeps showing the button it outgrew.

Persisted state needs no migration — __import_queue re-adds saved items
through __add_download, which sets the new status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Alex Shnitman
2026-09-20 09:29:49 +03:00
parent 6708a88229
commit a6d81d4513
5 changed files with 116 additions and 5 deletions
+3
View File
@@ -728,6 +728,9 @@
@if (download.value.live_status === 'is_live' && download.value.status !== 'scheduled') {
<span class="badge bg-danger">LIVE</span>
}
@if (download.value.status === 'queued') {
<span class="badge bg-secondary">Queued</span>
}
</div>
@if (download.value.status === 'scheduled') {
<span class="badge bg-warning text-dark">
+53
View File
@@ -544,4 +544,57 @@ describe('App', () => {
});
});
// Issue #1081: a download waiting for a concurrency slot ('queued') starts on
// its own, so it must not offer the Start button that a 'pending' row — one
// added with auto-start off — legitimately has.
describe('queued rows do not offer a dead Start button (#1081)', () => {
const queueEntry = (status: string): Download => ({
id: 'vid1',
title: 'Test',
url: 'https://example.com/v',
download_type: 'video',
quality: 'best',
format: 'any',
folder: '',
custom_name_prefix: '',
playlist_item_limit: 0,
status,
msg: '',
percent: 0,
speed: 0,
eta: 0,
filename: '',
checked: false,
} as Download);
const render = (status: string) => {
const fixture = TestBed.createComponent(App);
downloads.queue.set('https://example.com/v', queueEntry(status));
downloads.queueChanged.next();
fixture.detectChanges();
return fixture;
};
it('hides Start for a queued row but keeps it for a pending one', () => {
const queued = render('queued');
expect(
(queued.nativeElement as HTMLElement).querySelector('[aria-label="Start download for Test"]')
).toBeNull();
downloads.queue.clear();
const pending = render('pending');
expect(
(pending.nativeElement as HTMLElement).querySelector('[aria-label="Start download for Test"]')
).not.toBeNull();
});
it('says why the row is idle and counts it as queued', () => {
const fixture = render('queued');
expect((fixture.nativeElement as HTMLElement).textContent).toContain('Queued');
expect(fixture.componentInstance.queuedDownloads).toBe(1);
expect(fixture.componentInstance.activeDownloads).toBe(0);
});
});
});
+1 -1
View File
@@ -1728,7 +1728,7 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
speed += download.speed || 0;
} else if (download.status === 'preparing' || download.status === 'postprocessing') {
active++;
} else if (download.status === 'pending' || download.status === 'scheduled') {
} else if (download.status === 'queued' || download.status === 'pending' || download.status === 'scheduled') {
queued++;
}
});