refactor: a running download is worded the same on the admin Overview as on the wanted list, from one liveDownloadLine (M505 #5641)
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m29s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m8s
release / android (push) Successful in 5m21s
release / Build signed APK (releases and dev) (push) Successful in 5m35s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 16:15:21 -04:00
co-authored by Claude Opus 5.5
parent 622f248ea2
commit af2d4f9859
5 changed files with 75 additions and 28 deletions
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, test } from 'vitest';
import { liveDownloadLine } from './downloadState.js';
describe('liveDownloadLine', () => {
const g = (import_state: Parameters<typeof liveDownloadLine>[0]['import_state'], import_reason = '') => ({
import_state,
progress: 0.428,
import_reason
});
test('says each state of a running download', () => {
expect(liveDownloadLine(g('downloading'))).toBe('Downloading 42%');
expect(liveDownloadLine(g('importing'))).toBe('Downloaded; checking it is the album and filing it');
expect(liveDownloadLine(g('waiting'))).toBe('Downloaded; waiting to be imported');
expect(liveDownloadLine(g('waiting', 'the library is busy'))).toBe('Downloaded; the library is busy');
expect(liveDownloadLine(g('review', '9 files against 12 tracks'))).toBe('Needs you: 9 files against 12 tracks');
expect(liveDownloadLine(g('failed', 'disk full'))).toBe('Import failed: disk full');
});
test('has nothing to say before the client reports', () => {
expect(liveDownloadLine(g(''))).toBeNull();
});
});
+20
View File
@@ -0,0 +1,20 @@
import type { ImportState } from '#lib/api/types.js';
// A running download's state in a line: how far it has got, or what its
// import is doing or waiting on. The wanted list and the admin Overview say
// it the same way (M505). Null before the download client has reported on it.
export function liveDownloadLine(g: { import_state: ImportState; progress: number; import_reason: string }): string | null {
switch (g.import_state) {
case 'downloading':
return `Downloading ${Math.floor(g.progress * 100)}%`;
case 'importing':
return 'Downloaded; checking it is the album and filing it';
case 'waiting':
return `Downloaded; ${g.import_reason || 'waiting to be imported'}`;
case 'review':
return `Needs you: ${g.import_reason}`;
case 'failed':
return `Import failed: ${g.import_reason}`;
}
return null;
}
+4 -12
View File
@@ -1,5 +1,6 @@
import { relativeTime } from '#lib/utils/relativeTime.js';
import type { SearchResult, Want } from '#lib/api/types.js';
import { liveDownloadLine } from './downloadState.js';
// How a want reads in a line, wherever it is shown: the Collection's lists
// and the Missing page's folders (M504).
@@ -29,18 +30,9 @@ export function searched(w: Want, r: SearchResult): string {
export function downloading(w: Want): string | null {
const g = w.grab;
if (!g?.live) return null;
switch (g.import_state) {
case 'downloading':
return `Downloading ${Math.floor(g.progress * 100)}% · ${g.name}`;
case 'importing':
return `Downloaded; checking it is the album and filing it · ${g.name}`;
case 'waiting':
return `Downloaded; ${g.import_reason || 'waiting to be imported'}`;
case 'review':
return `Needs you: ${g.import_reason}`;
case 'failed':
return `Import failed: ${g.import_reason}`;
}
const line = liveDownloadLine(g);
// While it downloads or files, the line names the release it took.
if (line) return g.import_state === 'downloading' || g.import_state === 'importing' ? `${line} · ${g.name}` : line;
const who = g.origin === 'operator' ? 'taken by you' : 'taken by the schedule';
return `Downloading ${g.name}, ${who} ${relativeTime(g.grabbed_at)}`;
}
+4 -15
View File
@@ -4,6 +4,7 @@
import { relativeTime } from '#lib/utils/relativeTime.js';
import { formatBytes } from '#lib/utils/bytes.js';
import { QUEUES, countOf, queueLabel } from '#lib/admin/queues.js';
import { liveDownloadLine } from '#lib/wanted/downloadState.js';
import type { Download } from '#lib/api/types.js';
// The admin Overview (#5605) answers "does anything need me?" first: a
@@ -67,21 +68,9 @@
const plural = (n: number, one: string, many: string) => `${n.toLocaleString('en-US')} ${n === 1 ? one : many}`;
function downloadLine(d: Download): string {
switch (d.import_state) {
case 'downloading':
return `${Math.floor(d.progress * 100)}%`;
case 'importing':
return 'Checking and filing it';
case 'waiting':
return d.import_reason || 'Waiting to be imported';
case 'review':
case 'failed':
return `Needs you: ${d.import_reason}`;
default:
return 'Starting';
}
}
// Worded as the wanted list words it; before the client reports, it is
// simply downloading, as the Downloads list says.
const downloadLine = (d: Download) => liveDownloadLine(d) ?? 'Downloading';
</script>
<svelte:head><title>{pageTitle('Admin · Overview')}</title></svelte:head>
+24 -1
View File
@@ -67,7 +67,7 @@ describe('admin/+layout.ts load gate', () => {
// Importing at module load means the mocks attach to the same handles the
// page binds against.
import { createAdminStatusQuery, createAdminSummaryQuery, createScanStatusQuery } from '#lib/api/admin.js';
import type { AdminStatus, AdminSummary } from '#lib/api/types.js';
import type { AdminStatus, AdminSummary, Download } from '#lib/api/types.js';
import OverviewPage from './+page.svelte';
import AdminLayout from './+layout.svelte';
import { createRawSnippet } from 'svelte';
@@ -247,6 +247,29 @@ describe('admin Overview status', () => {
]);
expect(textOf('downloads-line')).toBe('1 downloading. In the last day, 4 imported. Activity');
});
// Worded as the wanted list words a download (M505).
test('each live download says what it is doing, as the wanted list says it', () => {
const d = (id: string, import_state: Download['import_state'], import_reason = ''): Download => ({
id, want_id: null, want_title: `Album ${id}`, want_artist: 'Blur', name: id, indexer: '', client: 'qbittorrent',
quality: 'FLAC', origin: 'schedule', grabbed_at: minutesAgo(5), ended_at: null, end_reason: '', progress: 0.42,
import_state, import_reason, imported_at: null, imported_files: 0, destination: '', files: [], skipped: 0,
end_detail: '', seeding: false, ratio: 0, seed_released_at: null, condition: null
});
setupOverview(summary(), vi.fn(), status({
downloads: {
running: 3, needs_you: 1, needs_you_since: minutesAgo(5), imported_day: 0, given_up_day: 0, clients_down: [],
live: [d('a', 'review', '9 files against 12 tracks'), d('b', 'downloading'), d('c', '')]
}
}));
render(OverviewPage);
const rows = screen.getAllByTestId('download-row').map((li) => li.textContent?.replace(/\s+/g, ' ').trim());
expect(rows).toEqual([
'Album a · Blur Needs you: 9 files against 12 tracks · 5m ago',
'Album b · Blur Downloading 42% · 5m ago',
'Album c · Blur Downloading · 5m ago'
]);
});
});
describe('admin layout', () => {