feat(downloads): bound auto-pick by bitrate, and take a good copy
Three faults, one subsystem, and the middle one is why a request that looked obviously satisfiable came back refused. **The guardrails were in megabytes, which cannot mean anything.** 300 MB is a generous FLAC single and a suspiciously small boxset, and whoever fills the field in has no idea which release the pipeline will apply it to. `MinKbps`/`MaxKbps`/`PreferredKbps` are the same statement divided by how long the music is, so one number holds across a nine-minute EP and a three-hour opera. The runtime comes from `Download.Expected`, which every anchored request already carries, so this costs no lookup; the rate is audio bytes over that, falling back to the mean stated per-file bitrate when the runtime is unknown. Artwork is excluded from the numerator, or a folder with 30 MB of scans reads as a better rip. An unknown runtime *passes* the window rather than failing it: the window is a statement about quality, and refusing everything the moment MusicBrainz is missing a track length would be a silent embargo. `MaxFileSizeMB` survives as a separate ceiling, still in megabytes on purpose -- it is a question about disk space, and it has to apply to a candidate whose bitrate cannot be worked out at all. **Auto-pick required daylight over the runner-up**, 0.08 on the combined score, and so fired hardest in the case it was never written for: a popular album turns up five *correct* copies, all matching the tracklist at 95%+ and differing only in format and seeders, their scores land within a point of each other, and it refused forever on the grounds that the choice was the user's. It was not. There was no question about what to fetch, only about which copy -- and abundance is the condition under which that matters least. A candidate no longer has to beat the field, only clear the bars on its own terms; where several do, ranking puts the one closest to the preferred bitrate first. That tie-break needed the preference to carry weight or it would have been decorative in a new unit: `BitrateFit` was 0.05 against format's 0.42, so asking for 320 and being handed a FLAC every time was the designed behaviour. When a preference is set the weights shift to fit 0.40 / format 0.20 / bitrate 0.10, taking it off the two heuristics that exist as stand-ins for the preference the user has now given. Health and priority are untouched. And the fit spans 0.5 to 1.0 rather than 0 to 1, so a preference can promote the copy that matches it and can never push the others under `minQuality` -- turning "I like 320" into "never take anything else" silently is what `MinKbps`/`MaxKbps` are for, out loud. **And a refusal quoted numbers that passed.** The request list built its message from `ranked[0]` -- the best candidate *before* the guardrails and before the lead check -- so a request killed by the size window, or by having too many good copies, reported "best of 12 found is not a confident enough match (match 96%, quality 88%)". `AutoPickVeto` names the gate that actually refused, and `AutoPickable` is that returning empty. Existing configs: the old `MinFileSizeMB`/`PreferredFileSizeMB` are not migrated. A number meaning "300 MB" cannot be reinterpreted as a rate without knowing the album it was aimed at, so carrying it over would be inventing an intent nobody expressed. Those two fall back to no window, which is the permissive default and what a fresh install gets; `MaxFileSizeMB` carries over unchanged, because a ceiling on bytes still means exactly what it did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MeQt5hgXg5YGoNZQ9ozG7L
This commit is contained in:
@@ -3,28 +3,52 @@
|
||||
|
||||
/**
|
||||
* AutoDownloadPrefs gates and scores what AutoPickable may choose
|
||||
* without asking. Zero values are permissive: no size window and no
|
||||
* format restriction.
|
||||
* without asking. Zero values are permissive: no bitrate window, no
|
||||
* size ceiling and no format restriction.
|
||||
*
|
||||
* **The window is a rate, not a size.** It used to be three numbers in
|
||||
* megabytes, which cannot mean anything on their own: 300 MB is a
|
||||
* generous FLAC single and a suspiciously small boxset, and the user
|
||||
* setting the number has no idea which release the pipeline will
|
||||
* eventually apply it to. A bitrate is the same statement normalised
|
||||
* by how long the music is, so one number holds across a 9-minute EP
|
||||
* and a 3-hour opera — and it is the unit the thing being described is
|
||||
* actually measured in. The runtime is known for every request
|
||||
* auto-pick can act on (`Download.Expected` carries per-track lengths,
|
||||
* and an anchored request is the only kind that reaches here), so this
|
||||
* costs no extra lookup.
|
||||
*/
|
||||
export interface AutoDownloadPrefs {
|
||||
/**
|
||||
* MinSizeMB and MaxSizeMB bound what auto-pick will grab. Zero
|
||||
* means no bound on that side. A candidate outside the window is
|
||||
* filtered out of auto-pick entirely, not merely scored down — a
|
||||
* tiny "sampler" torrent or a boxset ten times the expected size is
|
||||
* usually the wrong thing entirely, not a worse copy of the right
|
||||
* thing.
|
||||
* MinKbps and MaxKbps bound the average bitrate auto-pick will
|
||||
* grab. Zero means no bound on that side. A candidate outside the
|
||||
* window is filtered out of auto-pick entirely, not merely scored
|
||||
* down — a 96 kbps rip of the right album is not a worse copy the
|
||||
* user might accept, it is one they said not to take unattended.
|
||||
*
|
||||
* For reference: 320 is the top of MP3, ~500–1000 is FLAC depending
|
||||
* on the material, and anything under ~128 is a transcode.
|
||||
*/
|
||||
"minSizeMb": number;
|
||||
"maxSizeMb": number;
|
||||
"minKbps": number;
|
||||
"maxKbps": number;
|
||||
|
||||
/**
|
||||
* PreferredSizeMB nudges the score toward a target size within the
|
||||
* min/max window (a lossless rip and a heavily-padded lossless rip
|
||||
* can both pass the window). Zero disables the nudge; sizeFit then
|
||||
* returns a neutral value that does not affect ranking.
|
||||
* PreferredKbps nudges the score toward a target rate within the
|
||||
* window, and breaks the tie when several candidates are equally
|
||||
* good matches. Zero disables the nudge; bitrateFit then returns a
|
||||
* neutral value that does not affect ranking.
|
||||
*/
|
||||
"preferredSizeMb": number;
|
||||
"preferredKbps": number;
|
||||
|
||||
/**
|
||||
* MaxSizeMB is a hard ceiling on the whole candidate, and it is
|
||||
* deliberately still a size. It answers a different question from
|
||||
* the window above — not "is this the quality I want" but "is this
|
||||
* going to fill the disk" — and it has to hold even for a candidate
|
||||
* whose bitrate cannot be worked out, which is exactly the shape a
|
||||
* mislabelled boxset arrives in. Zero means no ceiling.
|
||||
*/
|
||||
"maxSizeMb": number;
|
||||
|
||||
/**
|
||||
* AllowedFormats restricts auto-pick to candidates whose audio
|
||||
@@ -487,9 +511,13 @@ export interface QualityScore {
|
||||
"priority": number;
|
||||
|
||||
/**
|
||||
* closeness to the preferred download size
|
||||
* BitrateFit is closeness to the preferred *rate*, which is what
|
||||
* the auto-download window is expressed in. It replaced a
|
||||
* `SizeFit` measured in megabytes: a size means nothing without
|
||||
* knowing how long the music is, so the same number described a
|
||||
* generous single and a suspiciously small boxset.
|
||||
*/
|
||||
"sizeFit": number;
|
||||
"bitrateFit": number;
|
||||
|
||||
/**
|
||||
* Mixed marks a candidate whose files are not all the same format,
|
||||
|
||||
@@ -86,9 +86,10 @@ export class DownloadClients extends LitElement {
|
||||
/** Working copy of the auto-download guardrails. */
|
||||
@state()
|
||||
private prefs: download.AutoDownloadPrefs = {
|
||||
minSizeMb: 0,
|
||||
minKbps: 0,
|
||||
maxKbps: 0,
|
||||
preferredKbps: 0,
|
||||
maxSizeMb: 0,
|
||||
preferredSizeMb: 0,
|
||||
allowedFormats: [],
|
||||
} as download.AutoDownloadPrefs;
|
||||
|
||||
@@ -284,25 +285,72 @@ export class DownloadClients extends LitElement {
|
||||
: nothing}
|
||||
|
||||
<div class="form">
|
||||
<!-- Bitrate, not megabytes. A size means nothing
|
||||
on its own: 300 MB is a generous single and a
|
||||
suspiciously small boxset, and whoever fills
|
||||
this in has no idea which release it will be
|
||||
applied to. A rate is the same statement
|
||||
divided by how long the music is, so one number
|
||||
holds across an EP and an opera. -->
|
||||
<div class="field-row">
|
||||
<wa-input
|
||||
label="Minimum size (MB)"
|
||||
label="Minimum bitrate (kbps)"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="No minimum"
|
||||
.value=${this.prefs.minSizeMb ? String(this.prefs.minSizeMb) : ''}
|
||||
.value=${this.prefs.minKbps ? String(this.prefs.minKbps) : ''}
|
||||
@input=${(e: Event) => {
|
||||
this.prefs = {
|
||||
...this.prefs,
|
||||
minSizeMb: Number((e.target as HTMLInputElement).value) || 0,
|
||||
minKbps: Number((e.target as HTMLInputElement).value) || 0,
|
||||
};
|
||||
}}
|
||||
></wa-input>
|
||||
<wa-input
|
||||
label="Maximum size (MB)"
|
||||
label="Maximum bitrate (kbps)"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="No maximum"
|
||||
.value=${this.prefs.maxKbps ? String(this.prefs.maxKbps) : ''}
|
||||
@input=${(e: Event) => {
|
||||
this.prefs = {
|
||||
...this.prefs,
|
||||
maxKbps: Number((e.target as HTMLInputElement).value) || 0,
|
||||
};
|
||||
}}
|
||||
></wa-input>
|
||||
<wa-input
|
||||
label="Preferred bitrate (kbps)"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="No preference"
|
||||
.value=${this.prefs.preferredKbps
|
||||
? String(this.prefs.preferredKbps)
|
||||
: ''}
|
||||
@input=${(e: Event) => {
|
||||
this.prefs = {
|
||||
...this.prefs,
|
||||
preferredKbps:
|
||||
Number((e.target as HTMLInputElement).value) || 0,
|
||||
};
|
||||
}}
|
||||
></wa-input>
|
||||
</div>
|
||||
|
||||
<div class="requires">
|
||||
320 is the top of MP3; a FLAC rip is usually
|
||||
500–1000 depending on the music. Preferred
|
||||
decides between copies that are otherwise equally
|
||||
good — it never rules one out, which is what the
|
||||
minimum and maximum are for.
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<wa-input
|
||||
label="Never grab more than (MB)"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="No limit"
|
||||
.value=${this.prefs.maxSizeMb ? String(this.prefs.maxSizeMb) : ''}
|
||||
@input=${(e: Event) => {
|
||||
this.prefs = {
|
||||
@@ -311,22 +359,14 @@ export class DownloadClients extends LitElement {
|
||||
};
|
||||
}}
|
||||
></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 class="requires">
|
||||
A ceiling on the download itself, in case a
|
||||
mislabelled boxset gets through. Still a size
|
||||
because it is a question about disk space, and
|
||||
because it has to apply to a candidate whose
|
||||
bitrate cannot be worked out at all.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user