Files
yellowjacket/frontend/bindings/yellowjacket/backend/download/models.ts
T
yonluandClaude Opus 5 3d375adab1 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
2026-08-17 22:10:51 -04:00

755 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* AutoDownloadPrefs gates and scores what AutoPickable may choose
* 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 {
/**
* 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, ~5001000 is FLAC depending
* on the material, and anything under ~128 is a transcode.
*/
"minKbps": number;
"maxKbps": number;
/**
* 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.
*/
"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
* files are all in one of these formats. Empty means no
* restriction.
*/
"allowedFormats": Format[] | null;
}
/**
* Candidate is one acquirable thing a provider found: a Soulseek user's
* folder, a torrent, a YouTube playlist. Providers fill the descriptive
* fields; the ranker fills Match, Quality and Score.
*/
export interface Candidate {
/**
* ID is unique within the provider that produced it, and is what
* gets handed back to Grab.
*/
"id": string;
"providerId": number;
"kind": Kind;
/**
* Protocol determines which transport can fetch this.
*/
"protocol": Protocol;
/**
* Descriptive.
*/
"title": string;
"artist"?: string;
/**
* peer username, indexer name, channel
*/
"origin"?: string;
"files": CandidateFile[] | null;
"totalSize": number;
/**
* Health is the provider's own availability signal, normalized to
* 0..1: seeder count for torrents, free upload slots and queue
* length for Soulseek. 0.5 when the provider has no signal.
*/
"health": number;
/**
* Scores, filled by the ranker.
*/
"match": MatchScore;
"quality": QualityScore;
"score": number;
}
/**
* CandidateFile is one file inside a candidate. Soulseek and torrent
* results give paths and sizes but no tags, so Format and duration are
* inferred from the path and size where possible.
*/
export interface CandidateFile {
"path": string;
"size": number;
"format": Format;
/**
* kbps, 0 when unknown
*/
"bitrate"?: number;
"isAudio": boolean;
/**
* expected track position
*/
"matchedTo"?: number;
}
/**
* Caps declares which roles a provider fills and which optional
* behaviours it supports. The frontend renders controls from this
* rather than switching on Kind, so a provider that gains resume
* support later needs no frontend change.
*/
export interface Caps {
/**
* Roles.
*/
"canSearch": boolean;
"canTransport": boolean;
"canDelegate": boolean;
/**
* CanList marks a provider that keeps a persistent wanted list of
* its own, which the reconciler mirrors this app's list into.
*/
"canList": boolean;
/**
* Optional behaviours.
*/
"canResume": boolean;
"canCancel": boolean;
"reportsSize": boolean;
/**
* Protocols this provider can transport. Empty for providers that
* only fetch their own search results.
*/
"transports": Protocol[] | null;
}
/**
* Config is a provider's stored settings. Secret values are not held
* here — they live in the secrets store keyed by provider ID, so a
* config blob can be logged or shown in the UI without redaction.
*/
export interface Config {
"id": number;
"kind": Kind;
"name": string;
"enabled": boolean;
"priority": number;
"settings": { [_ in string]?: string } | null;
/**
* SetSecrets names which of the descriptor's secret fields already
* have a stored value, without exposing it. Populated only when a
* Config is built for the frontend (see Service.withSecretFlags);
* empty when read from or written to the store.
*/
"setSecrets"?: { [_ in string]?: boolean } | null;
}
/**
* Descriptor is the static, instance-independent description of a
* provider kind: what it is called, what it can do, and which settings
* it needs. The settings page renders its form from this, so a new
* provider gets a config UI without any frontend work.
*/
export interface Descriptor {
"kind": Kind;
"name": string;
/**
* Summary is one line explaining what connecting this gets you.
*/
"summary": string;
/**
* Caps are the kind's inherent capabilities, before configuration.
*/
"caps": Caps;
/**
* Fields are the settings the user must supply.
*/
"fields": Field[] | null;
/**
* RequiresExternal names the software the user must run themselves
* (a slskd daemon, a Lidarr instance), or is empty for providers
* that need nothing but a binary on PATH.
*/
"requiresExternal"?: string;
}
/**
* DownloadItem is one grab attempt, as stored.
*
* deliberate: distinguishes it from download.Download (the attempt) and
* download.Request (the durable record) at every call site, which a bare
* "Item" would not.
*/
export interface DownloadItem {
"id": string;
"downloadId": string;
"providerId": number;
"transportId"?: number;
"externalId"?: string;
"candidate": Candidate;
"state": State;
"bytesDone": number;
"bytesTotal": number;
"error"?: string;
"createdAt": string;
"updatedAt": string;
}
/**
* DownloadView is one row of the downloads list.
*
* would stop reading as "one row of the Downloads list" the moment this
* package also has a Requests list — see RequestInput/Request nearby.
*/
export interface DownloadView {
"id": string;
/**
* Anchors. Any may be empty; all empty means free-text.
*/
"releaseMbid"?: string;
"releaseGroupMbid"?: string;
/**
* RecordingMBID anchors a single-track download. Its Expected holds
* exactly that one track, which is what lets a track download be
* scored — and therefore auto-picked — on the same footing as an
* album.
*/
"recordingMbid"?: string;
/**
* RequestID links back to the durable Request row this download was
* raised for or attached to, or 0 for a free-text download with
* nothing stable to attach to. The reconciler and manual anchored
* downloads both write the outcome back through it.
*/
"requestId"?: number;
/**
* Source records where the download came from, for the downloads
* list. Empty means "manual".
*/
"source"?: string;
/**
* Display and query text. Artist/Album are what searches are built
* from; Query overrides them when the user typed something raw.
*/
"artist": string;
"album": string;
"query"?: string;
/**
* Expected is the tracklist the anchor resolves to, used for
* completeness scoring and for the autotag match at import. Empty
* for free-text downloads.
*/
"expected"?: ExpectedTrack[] | null;
/**
* LibraryID is the library imported files belong to.
*/
"libraryId": number;
"createdAt": string;
"state": State;
"error"?: string;
"items": DownloadItem[] | null;
}
/**
* Entity says what a request's MBID names, and is the only type
* distinction the durable request list makes.
*/
export enum Entity {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Request entity types.
*
* EntityArtist is a subscription rather than a thing to fetch: it
* is never satisfied, and each reconcile expands the artist's
* discography into child requests.
*/
EntityArtist = "artist",
/**
* EntityReleaseGroup is an album in the abstract — any release of
* it satisfies the request, which is what a user means by "I want
* this album".
*/
EntityReleaseGroup = "release-group",
/**
* EntityRelease is one specific edition, used when the user picked
* a particular pressing.
*/
EntityRelease = "release",
/**
* EntityRecording is a single track.
*/
EntityRecording = "recording",
};
/**
* ExpectedTrack is one track of the release the user asked for.
*/
export interface ExpectedTrack {
"position": number;
"discNumber": number;
"title": string;
"artist": string;
"lengthMillis": number;
}
/**
* Field describes one provider setting for the settings form.
*/
export interface Field {
"key": string;
"label": string;
"placeholder"?: string;
"help"?: string;
/**
* Secret marks a value stored in the secrets store rather than the
* provider config row, and rendered as a password input.
*/
"secret": boolean;
/**
* Path marks a value that names a local filesystem directory, so
* the settings form can offer a native folder picker beside the
* text input rather than making the user type or paste it.
*/
"path": boolean;
"required": boolean;
"default"?: string;
}
/**
* Format is a normalized audio container/codec name.
*/
export enum Format {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Audio formats, ordered by the quality ranking in formatRank.
*/
FormatUnknown = "",
FormatFLAC = "flac",
FormatALAC = "alac",
FormatWAV = "wav",
FormatMP3 = "mp3",
FormatAAC = "aac",
FormatOGG = "ogg",
FormatOpus = "opus",
FormatWMA = "wma",
};
/**
* Kind identifies a provider implementation. It is stored in the
* database and used to look up the constructor in the registry, so
* values are stable strings and never renamed.
*/
export enum Kind {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Provider kinds.
*/
KindSlskd = "slskd",
KindYtDlp = "yt-dlp",
KindLidarr = "lidarr",
KindProwlarr = "prowlarr",
KindQBittorrent = "qbittorrent",
KindSABnzbd = "sabnzbd",
/**
* KindFake is an in-memory provider used by tests. It is never
* offered in the UI.
*/
KindFake = "fake",
};
/**
* MatchScore answers "is this the release the user asked for?" It is
* deliberately separate from QualityScore: a perfect match at 128kbps
* and a mediocre match in FLAC are different failures, and collapsing
* them into one number makes the ranking impossible to explain.
*/
export interface MatchScore {
/**
* Overall is 0..1.
*/
"overall": number;
/**
* filenames vs expected titles
*/
"titleFit": number;
/**
* path/origin vs expected artist
*/
"artistFit": number;
/**
* folder name vs album title
*/
"albumFit": number;
/**
* audio files vs expected count
*/
"completeness": number;
/**
* Anchored records whether an MBID drove this score. Unanchored
* matches are capped, because there is nothing to be right about.
*/
"anchored": boolean;
}
/**
* Protocol is how a candidate's bytes are moved. Search-only providers
* report it so the pipeline can pick a compatible transport; providers
* that transport their own results use ProtocolDirect.
*/
export enum Protocol {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Transport protocols.
*
* ProtocolDirect means the finding provider also does the fetch.
*/
ProtocolDirect = "direct",
ProtocolTorrent = "torrent",
ProtocolUsenet = "usenet",
};
/**
* QualityScore answers "is this a good copy?".
*/
export interface QualityScore {
/**
* Overall is 0..1.
*/
"overall": number;
/**
* FLAC > V0 > 320 > lower
*/
"formatRank": number;
"bitrate": number;
/**
* seeders, free slots
*/
"health": number;
/**
* user's per-provider preference
*/
"priority": number;
/**
* 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.
*/
"bitrateFit": number;
/**
* Mixed marks a candidate whose files are not all the same format,
* which usually means a hand-assembled folder rather than a rip.
*/
"mixed": boolean;
}
/**
* Request is one row of the durable request list.
*/
export interface Request {
"id": number;
"mbid": string;
"entity": Entity;
"libraryId": number;
/**
* Artist and Title are display cache only. Matching always uses
* the MBID.
*/
"artist": string;
"title": string;
"scope": RequestScope;
/**
* Secondary includes compilations, live albums and remixes in an
* artist request's expansion.
*/
"secondary": boolean;
"state": RequestState;
/**
* ParentID is set on requests the reconciler derived from an artist
* subscription. A request the user pinned directly has none, so
* removing the artist leaves it alone.
*/
"parentId"?: number;
"attempts": number;
"lastError"?: string;
"lastTriedAt"?: string;
"nextTryAt"?: string;
/**
* ExternalIDs maps provider row ID (as a string, because JSON
* object keys are strings) to that provider's own identifier for
* this request. Only set for providers that keep a persistent
* list of their own.
*/
"externalIds"?: { [_ in string]?: string } | null;
"createdAt": string;
"updatedAt": string;
}
/**
* RequestInput is what the frontend submits to request something. It
* is one MBID and the type of thing it names, because that is
* genuinely all a durable request is.
*/
export interface RequestInput {
"mbid": string;
"entity": string;
"libraryId": number;
/**
* Artist and Title are display text only, and optional: the
* reconciler fills them in from the catalog when the caller has
* nothing but an MBID.
*/
"artist": string;
"title": string;
/**
* Scope and Secondary apply to artist requests.
*/
"scope": string;
"secondary": boolean;
}
/**
* RequestScope applies to artist requests only.
*/
export enum RequestScope {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Artist request scopes.
*
* ScopeFuture takes only releases first published after the artist
* was added. Default, because subscribing to an artist should not
* silently queue their entire back catalogue.
*/
ScopeFuture = "future",
/**
* ScopeAll backfills the whole discography as well.
*/
ScopeAll = "all",
};
/**
* RequestState is where a request sits. There is deliberately no
* "failed": an attempt can fail, a request cannot. A request that has
* tried and not found anything is still wanted, with attempts and
* last_error recording why it is taking a while.
*/
export enum RequestState {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Request states.
*
* RequestStateWanted is the active state: due for another attempt
* when its backoff elapses.
*/
RequestStateWanted = "wanted",
/**
* RequestStateSatisfied means the library owns it. How it got
* there — downloaded here, ripped, bought elsewhere — does not
* matter.
*/
RequestStateSatisfied = "satisfied",
/**
* RequestStatePaused is the user saying "keep this on the list but
* stop trying".
*/
RequestStatePaused = "paused",
};
/**
* SearchRequest is what the frontend submits to start a download.
*/
export interface SearchRequest {
"libraryId": number;
"releaseMbid": string;
"releaseGroupMbid": string;
"artist": string;
"album": string;
"query": string;
"expected": ExpectedTrack[] | null;
}
/**
* StartResult is what the picker needs after a search.
*/
export interface StartResult {
"downloadId": string;
"candidates": Candidate[] | null;
/**
* AutoPicked reports that the pipeline already chose and is
* downloading, so the picker should show progress rather than a
* list of choices.
*/
"autoPicked": boolean;
}
/**
* State is the lifecycle position of a download item.
*/
export enum State {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Download item states. Searching through Importing are live;
* Complete, Cancelled and Failed are terminal.
*/
StateSearching = "searching",
StateFound = "found",
StateQueued = "queued",
StateGrabbing = "grabbing",
StateVerifying = "verifying",
StateTagging = "tagging",
StateImporting = "importing",
StateComplete = "complete",
StateCancelled = "cancelled",
StateFailed = "failed",
};
/**
* Summary reports what a pass did, for logging and for the UI.
*/
export interface Summary {
/**
* Expanded is how many child requests artist subscriptions produced.
*/
"expanded": number;
/**
* Satisfied is how many requests the library turned out to own.
*/
"satisfied": number;
/**
* Attempted is how many requests were searched for.
*/
"attempted": number;
/**
* Started is how many of those found a clear enough winner to
* download unattended.
*/
"started": number;
/**
* Synced is how many requests were pushed to an external list.
*/
"synced": number;
/**
* Waiting is how many requests are on the list and still being
* looked for. A pass that did nothing is the normal case, and the
* UI can only say so honestly if it knows the list was not empty.
*/
"waiting": number;
/**
* NoProviders reports that nothing could be searched because no
* download client is enabled — the one "nothing happened" the user
* can actually fix.
*/
"noProviders": boolean;
}