From af2d4f9859755d38546737c2a4168aeb142403fa Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 11 Oct 2026 16:15:21 -0400 Subject: [PATCH] refactor: a running download is worded the same on the admin Overview as on the wanted list, from one liveDownloadLine (M505 #5641) Co-Authored-By: Claude Opus 5.5 --- web/src/lib/wanted/downloadState.test.ts | 23 ++++++++++++++++++++++ web/src/lib/wanted/downloadState.ts | 20 +++++++++++++++++++ web/src/lib/wanted/wantStatus.ts | 16 ++++----------- web/src/routes/admin/+page.svelte | 19 ++++-------------- web/src/routes/admin/admin.test.ts | 25 +++++++++++++++++++++++- 5 files changed, 75 insertions(+), 28 deletions(-) create mode 100644 web/src/lib/wanted/downloadState.test.ts create mode 100644 web/src/lib/wanted/downloadState.ts diff --git a/web/src/lib/wanted/downloadState.test.ts b/web/src/lib/wanted/downloadState.test.ts new file mode 100644 index 00000000..30f409bd --- /dev/null +++ b/web/src/lib/wanted/downloadState.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'vitest'; +import { liveDownloadLine } from './downloadState.js'; + +describe('liveDownloadLine', () => { + const g = (import_state: Parameters[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(); + }); +}); diff --git a/web/src/lib/wanted/downloadState.ts b/web/src/lib/wanted/downloadState.ts new file mode 100644 index 00000000..aa4d38a7 --- /dev/null +++ b/web/src/lib/wanted/downloadState.ts @@ -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; +} diff --git a/web/src/lib/wanted/wantStatus.ts b/web/src/lib/wanted/wantStatus.ts index dcc6d0c9..9ad0de08 100644 --- a/web/src/lib/wanted/wantStatus.ts +++ b/web/src/lib/wanted/wantStatus.ts @@ -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)}`; } diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 99f7f11f..599ce1d2 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -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'; {pageTitle('Admin · Overview')} diff --git a/web/src/routes/admin/admin.test.ts b/web/src/routes/admin/admin.test.ts index 0c020bb2..3d37d1dd 100644 --- a/web/src/routes/admin/admin.test.ts +++ b/web/src/routes/admin/admin.test.ts @@ -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', () => {