refactor(download): rename Want/Request to Request/Download, unify downloads flow, add auto-download guardrails
Build & publish Arch package / arch-package (push) Successful in 2m2s
Search index maintenance / maintain-index (push) Successful in 7s

The durable "I asked for this" record was called Want, and the one-shot
search-and-grab attempt was called Request — names that didn't match
what either actually did. Want is now Request, and the old Request/Item
is now Download/DownloadItem, with a table-rename migration
(download_wants -> download_requests, old download_requests ->
download_downloads) safe against both fresh installs and existing data.

Every anchored manual download now upserts/reuses a durable Request
before running, so a "download now" that finds nothing is picked up by
the background reconciler automatically instead of just failing with
no trace — the gap that caused this session's repeated "no candidates
found" failures on the same album.

Also adds auto-download guardrails (file-size min/max with a preferred
target, allowed file types) that gate what the pipeline may grab
unattended, live-editable from a new settings section. The frontend's
wanted-view becomes downloads-view, with a new Downloads tab showing
attempt/transfer history that previously had no UI at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
This commit is contained in:
2026-08-10 14:35:57 -04:00
co-authored by Claude Sonnet 5
parent cbd82a5a74
commit 65333857e2
62 changed files with 4067 additions and 2524 deletions
@@ -15,8 +15,28 @@ import type {
} from '@store/download-store';
import { downloadStore } from '@store/download-store';
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/Config';
import { SetPreferences } from '@go/download/Service';
import type { download } from '@go/models';
import './config-section';
/**
* Allowed audio formats for auto-download, mirrored from
* backend/download/types.go's `Format` constants. `FormatUnknown` is
* deliberately excluded — it names "no format detected", not a format a
* user could opt into.
*/
const AUTO_DOWNLOAD_FORMATS: { value: string; label: string }[] = [
{ value: 'flac', label: 'FLAC' },
{ value: 'alac', label: 'ALAC' },
{ value: 'wav', label: 'WAV' },
{ value: 'mp3', label: 'MP3' },
{ value: 'aac', label: 'AAC' },
{ value: 'ogg', label: 'OGG' },
{ value: 'opus', label: 'Opus' },
{ value: 'wma', label: 'WMA' },
];
/**
* Download client configuration.
*
@@ -59,6 +79,24 @@ export class DownloadClients extends LitElement {
@state()
private errorMessage = '';
/** Working copy of the auto-download guardrails. */
@state()
private prefs: download.AutoDownloadPrefs = {
minSizeMb: 0,
maxSizeMb: 0,
preferredSizeMb: 0,
allowedFormats: [],
} as download.AutoDownloadPrefs;
@state()
private prefsSaving = false;
@state()
private prefsError = '';
@state()
private prefsSaved = false;
private unsubscribe: (() => void) | null = null;
override connectedCallback(): void {
@@ -67,6 +105,7 @@ export class DownloadClients extends LitElement {
this.unsubscribe = downloadStore.subscribe(() => this.syncFromStore());
void downloadStore.init().then(() => this.syncFromStore());
void this.loadPreferences();
}
override disconnectedCallback(): void {
@@ -81,6 +120,14 @@ export class DownloadClients extends LitElement {
this.descriptors = downloadStore.descriptors;
}
private async loadPreferences(): Promise<void> {
try {
this.prefs = await GetDownloadPreferences();
} catch (err) {
console.error('Failed to load auto-download preferences:', err);
}
}
static override styles = [
designTokens,
css`
@@ -178,6 +225,21 @@ export class DownloadClients extends LitElement {
.field-row .browse-button {
flex-shrink: 0;
}
.format-options {
display: flex;
flex-wrap: wrap;
gap: 0.4em 1em;
margin-top: 0.4em;
}
.format-option {
display: flex;
align-items: center;
gap: 0.4em;
font-size: 0.9em;
cursor: pointer;
}
`,
];
@@ -208,6 +270,104 @@ export class DownloadClients extends LitElement {
</div>
`}
</config-section>
<config-section
heading="Auto-download preferences"
description="Guardrails on what the pipeline may grab without asking — a manual pick is never restricted by these, only automatic ones."
>
${this.prefsError
? html`<wa-callout variant="danger">${this.prefsError}</wa-callout>`
: nothing}
<div class="form">
<div class="field-row">
<wa-input
label="Minimum size (MB)"
type="number"
min="0"
placeholder="No minimum"
.value=${this.prefs.minSizeMb ? String(this.prefs.minSizeMb) : ''}
@input=${(e: Event) => {
this.prefs = {
...this.prefs,
minSizeMb: Number((e.target as HTMLInputElement).value) || 0,
};
}}
></wa-input>
<wa-input
label="Maximum size (MB)"
type="number"
min="0"
placeholder="No maximum"
.value=${this.prefs.maxSizeMb ? String(this.prefs.maxSizeMb) : ''}
@input=${(e: Event) => {
this.prefs = {
...this.prefs,
maxSizeMb: Number((e.target as HTMLInputElement).value) || 0,
};
}}
></wa-input>
<wa-input
label="Preferred size (MB)"
type="number"
min="0"
placeholder="No preference"
.value=${this.prefs.preferredSizeMb
? String(this.prefs.preferredSizeMb)
: ''}
@input=${(e: Event) => {
this.prefs = {
...this.prefs,
preferredSizeMb:
Number((e.target as HTMLInputElement).value) || 0,
};
}}
></wa-input>
</div>
<div>
<div class="requires">
Allowed formats — leave all unchecked to allow any format.
</div>
<div class="format-options">
${AUTO_DOWNLOAD_FORMATS.map(
(format) => html`
<label class="format-option">
<input
type="checkbox"
.checked=${(this.prefs.allowedFormats ?? []).includes(
format.value,
)}
@change=${(e: Event) =>
this.toggleFormat(
format.value,
(e.target as HTMLInputElement).checked,
)}
/>
${format.label}
</label>
`,
)}
</div>
</div>
<div class="form-actions">
${this.prefsSaved
? html`<span class="test-result ok">Saved.</span>`
: nothing}
<wa-button
size="small"
variant="brand"
?disabled=${this.prefsSaving}
@click=${this.savePreferences}
>
${this.prefsSaving
? html`<wa-spinner></wa-spinner>`
: 'Save preferences'}
</wa-button>
</div>
</div>
</config-section>
`;
}
@@ -554,6 +714,37 @@ export class DownloadClients extends LitElement {
this.testing = null;
}
}
private toggleFormat(format: string, checked: boolean): void {
const current = this.prefs.allowedFormats ?? [];
const allowedFormats = checked
? [...current, format]
: current.filter((f) => f !== format);
this.prefs = { ...this.prefs, allowedFormats };
}
/**
* Saves the guardrails both to disk and to the running download
* manager in one action — persistence alone would leave the setting
* inert until restart, which is exactly the bug this mirrors away
* from (see `config.Library`'s prior persist-without-apply gap).
*/
private savePreferences = async () => {
this.prefsSaving = true;
this.prefsError = '';
this.prefsSaved = false;
try {
await SetDownloadPreferences(this.prefs);
await SetPreferences(this.prefs);
this.prefsSaved = true;
} catch (err) {
this.prefsError = String(err);
} finally {
this.prefsSaving = false;
}
};
}
declare global {