Documentation
¶
Overview ¶
Package settings persists user-tunable behaviour (concurrency, speed limit, extraction) as JSON in the data dir and hands out consistent snapshots.
This file holds the shape and the store: the Settings struct, the defaults, and Load/Get/Set. What each group of fields is allowed to contain lives with that group, in settings_queue.go, settings_paths.go, settings_appearance.go, settings_intake.go and settings_network.go, each with its own sanitize hook that sanitize below calls in turn. The struct is one declaration because Go gives it no choice and because embedding sub-structs would flatten differently in JSON and break every composite literal in the tree; the rules about it are what is split, and those were the four hundred lines nobody could edit at once.
Index ¶
Constants ¶
const ( ShapeRound = "round" ShapeSoft = "soft" ShapeSquare = "square" )
The three shapes the interface offers. Anything else falls back to round rather than producing an interface with no radius rule at all.
const ( )
How much of a navigation entry is drawn: in the sidebar, and in the settings rail, which are the app's two sets of tabs (jdp, 2026-08-27: "Man soll per horizontalem Selektor wählen können ob bei den Tabs (Settings und Sidebar) nur glyph, nur text oder text und glyph angezeigt werden soll oder glyph und text nur bei mouseover").
NavLabelsHover is the interesting one and deserves saying out loud, because it is not the collapsing rail it sounds like: NOTHING resizes. The tile and the sidebar row keep the exact size they have in NavLabelsBoth; at rest the glyph sits centred in that space, and on hover it moves aside - up in a settings tile, left in a sidebar row - and the label appears in the room it leaves. jdp's own description, and better than the three alternatives offered: a rail that grows or overlays on hover moves the page under the pointer, and this one cannot.
const ( // ResumeNever leaves everything that was in flight paused. It is the // default, and the reason is worth reading before changing it: a restarted // transfer starts from the beginning. No backend's handle on a running // download survives this process, so what the queue does with a resumed task // is fetch it afresh - and because the partial is still sitting at the // destination, the collision policy applies to it, which under the default // (rename) means the bytes land beside it in a second file. Doing that // unattended, on a box that reboots at four in the morning, spends a metered // line on bytes somebody already had and leaves the half file behind. ResumeNever = "never" // ResumeRunning starts again what was actually running, and only if // something was: a queue that was already idle or halted stays that way. // This is JDownloader's default and the option most people mean. ResumeRunning = "running" // ResumeAll starts everything that had not finished, whether it was running // or still waiting for a slot. ResumeAll = "all" )
What the app does on boot with the downloads that were in flight when the process last stopped. Anything else falls back to ResumeNever, because an unrecognised value must never be read as "start downloading".
const DefaultHistoryMax = 10000
DefaultHistoryMax is how many finished downloads the history keeps. Roughly a year of heavy use, and small enough that the table stays a few megabytes on a database that sits on the same disk the downloads land on.
const DefaultKeepFinishedDays = 30
DefaultKeepFinishedDays is how long a finished download stays in the list.
It is a month rather than forever, and that is a deliberate answer to a list that otherwise only ever grows: the tenth thousand row is not a record, it is the reason the table takes a second to sort. Nothing is lost by it - what was downloaded is kept in the history table, which retention never touches, and the file on disk is never in question here at all.
const RainbowSize = 8
RainbowSize is how many hues the palette has. It is fixed: the colours are handed out by position, so a palette that can change length would silently re-colour every existing row whenever the user added one.
Variables ¶
This section is empty.
Functions ¶
func ParseResumeOnStart ¶
ParseResumeOnStart reads a stored value as one of the three modes, folding anything it does not recognise onto ResumeNever. It exists so the app has one answer to "what does this string mean" rather than a switch of its own that can disagree with the sanitiser.
func ResumeModes ¶
func ResumeModes() []string
ResumeModes lists the three in the order an interface should offer them, cautious first. Built fresh per call so a caller cannot reorder the menu for everybody else.
Types ¶
type Settings ¶
type Settings struct {
MaxConcurrent int `json:"maxConcurrent"` // global simultaneous downloads
MaxPerHost int `json:"maxPerHost"` // simultaneous downloads per host
SpeedLimit int64 `json:"speedLimit"` // bytes/s, 0 = unlimited
Extract bool `json:"extract"` // extract archives after download
// AutoConfirm, AutoConfirmDelay and AutoStart are the three fields a
// single AutoStart boolean used to be, and MUST be read together - see
// migrateAutoStart in settings_confirm.go for the migration this split
// demands from every existing install.
//
// AutoConfirm moves a batch out of the collector on its own, without a
// click - what the old flag actually gated, under the name that
// conflated it with AutoStart below.
AutoConfirm bool `json:"autoConfirm"`
// AutoConfirmDelay is how long AutoConfirm waits before it fires, in
// seconds. Zero fires the instant a batch is staged, which is what
// every install had before this field existed - there was no delay to
// preserve, only the one it would be wrong to invent on their behalf.
AutoConfirmDelay int `json:"autoConfirmDelay"`
// AutoStart is what a confirmed batch does next: start immediately
// (true, the default) or sit in the queue until something explicitly
// releases it (false). Before this split there was no way to ask for
// the second half on its own - confirming a link, however it happened,
// always started it - so AutoStart defaults to true precisely to keep
// that the case for every install that never touches this setting.
// "Confirm without start" (AutoConfirm=true, AutoStart=false) is the
// state this split makes possible for the first time.
AutoStart bool `json:"autoStart"`
// OnDupes and OnOffline are the confirm-time policies for a link that
// duplicates one already in the list, or one a check has already found
// gone - internal/confirm.Policy, stored as its string form the same
// way MirrorPolicy and CollisionPolicy are. These are the INSTANCE's
// own defaults; a batch may carry its own override of either (see
// internal/app.ConfirmTasks), read against these two when it does not.
OnDupes string `json:"onDupes"`
OnOffline string `json:"onOffline"`
// AddAtTop puts a batch leaving the collector at the front of the wait
// order instead of the back, so it plays next rather than after
// whatever was already queued.
AddAtTop bool `json:"addAtTop"`
// DownloadDir is where finished files land. Empty means the built-in
// default inside the data directory.
DownloadDir string `json:"downloadDir"`
// SubfolderByPackage puts each package in its own folder below DownloadDir.
SubfolderByPackage bool `json:"subfolderByPackage"`
// ArchivePasswords are tried in order when extracting an encrypted archive.
ArchivePasswords []string `json:"archivePasswords"`
// ExtractTo collects extractions in one folder instead of leaving each one
// beside its archive. Empty keeps the old behaviour, which is what most
// people expect and what every install had before the setting existed. It
// may be a pathvars template, expanded per task like DownloadDir.
ExtractTo string `json:"extractTo"`
// ExtractSubfolder puts each package in its own folder below ExtractTo. It
// does nothing without ExtractTo - see extract.Options.
ExtractSubfolder bool `json:"extractSubfolder"`
// ExtractCollision is what an extraction does when its destination folder is
// already there: rename, skip or overwrite, decided per folder.
ExtractCollision string `json:"extractCollision"`
// ArchiveDisposal is what happens to an archive that unpacked cleanly:
// keep, trash or delete.
//
// This key replaced the boolean `deleteArchive`, and the two do not live
// side by side: a settings file written by an older build is mapped on the
// way in by migrate() below. A JSON field that changes type is the one
// change that breaks the round-trip for every existing install, so the old
// spelling is read exactly once, at load, and never written again.
ArchiveDisposal string `json:"archiveDisposal"`
// TrashRetentionDays is how long a trashed archive stays before the sweep
// takes it. Zero never sweeps.
TrashRetentionDays int `json:"trashRetentionDays"`
// DeleteInfoFiles sweeps the .nfo/.sfv/.diz/.url that came with the same
// package as the archive, using the same disposal.
DeleteInfoFiles bool `json:"deleteInfoFiles"`
// MaxRetries is how often a failed download is retried automatically.
MaxRetries int `json:"maxRetries"`
// Crawl lets a pasted page URL be opened and the files it links to be
// staged, instead of the page itself becoming one task.
Crawl bool `json:"crawl"`
// WatchDir is a folder whose dropped .txt/.crawljob files are picked up.
// Empty disables the watcher.
WatchDir string `json:"watchDir"`
// VerifyChecksums checks a finished download against a checksum file that
// came with it, when one did.
VerifyChecksums bool `json:"verifyChecksums"`
// PreParserEnabled turns on internal/linkscan for POST /api/links: the
// pasted or dropped blob is scanned for links wherever they sit in it,
// instead of one line being taken as one link verbatim. Off falls back
// to that older, literal behaviour. Named and defaulted after
// JDownloader's own AddLinksPreParserEnabled (verified against
// JDownloader's own source, CFG_LINKGRABBER and LinkgrabberSettings.java:
// same key, same true default, same "works on the pasted text as-is"
// meaning for off), not a spelling picked from the plan's prose.
PreParserEnabled bool `json:"preParserEnabled"`
// Shape is how rounded the whole interface is: "round", "soft" or "square".
// One knob drives every corner, so the app never looks half-converted.
Shape string `json:"shape"`
// Accent is the one colour the interface uses for activity, as #rrggbb.
// Empty means the built-in heraldic gold.
Accent string `json:"accent"`
// Rainbow replaces the single accent with a palette handed out by position,
// so a long list of downloads reads as distinct rows instead of one gold
// wall. It colours activity only, exactly like the accent it stands in for.
Rainbow bool `json:"rainbow"`
// RainbowReactive rests everything neutral and colours only what is hovered
// or active: the restrained reading of the mode.
RainbowReactive bool `json:"rainbowReactive"`
// RainbowRotate offsets the palette by RainbowSeed, so a run does not always
// begin on the same hue.
RainbowRotate bool `json:"rainbowRotate"`
// RainbowSeed is that offset. It is stored with the instance rather than in
// the browser because two clients of one server showing different colours
// for the same download is a bug, not a preference.
RainbowSeed int `json:"rainbowSeed"`
// RainbowPalette overrides the eight built-in hues. Empty means the default.
RainbowPalette []string `json:"rainbowPalette"`
// HideAccountsFromSidebar removes the sidebar's own "Konten" nav item,
// for someone who only ever reaches accounts through the identical
// settings tab and finds the second entry point redundant rather than
// convenient. The zero value (false) keeps the current, pre-existing
// behaviour - both the nav item and the settings tab render the same
// page either way, so hiding one costs nothing but a click.
HideAccountsFromSidebar bool `json:"hideAccountsFromSidebar"`
// HideInstancesFromSidebar does the same for the "Instanzen" nav item
// (jdp, 2026-08-27: "Können wir den Instanzentab wie den konten-tab ein-
// und ausblendbar machen?"), and for the same reason: somebody running a
// single instance has a nav item that lists exactly itself, forever.
//
// A separate field rather than a shared "hidden nav items" list, matching
// HideAccountsFromSidebar above: a set of strings in settings.json is a
// set somebody can put a typo in, and neither of these is the start of a
// family big enough to be worth that.
HideInstancesFromSidebar bool `json:"hideInstancesFromSidebar"`
// "glyph", "text" or "hover". It governs the sidebar AND the settings
// rail together, from one control, because they are one idea wearing
// two shapes and a person who wants glyphs wants glyphs.
//
// Stored with the instance rather than in the browser, alongside Shape
// and Accent above and for the same reason: this is what the interface
// LOOKS like, and the look follows the instance from one machine to the
// next. See settings_appearance.go for the four values and for what
// "hover" actually does, which is not what the word suggests.
NavLabels string `json:"navLabels"`
// AutoUpdateCheck asks the desktop build to call update.Check once at
// startup (and the Allgemein tab to do the same on load) instead of only
// on an explicit click of "Check for updates" - desktop only in
// practice, read nowhere on the container build. Off by default: it is
// an outbound call to GitHub on every launch, and that is an opt-in, not
// something a fresh install does before being asked.
AutoUpdateCheck bool `json:"autoUpdateCheck"`
// AutoUpdateInstall asks the desktop build to install a newer release
// (download, verify, swap the running binary, relaunch) the moment
// AutoUpdateCheck's own check finds one, instead of only offering the
// release page to fetch by hand. Meaningless without AutoUpdateCheck
// also being on - nothing reads this unless a check already found an
// update - and meaningless on the container build, which cannot replace
// itself from the inside (App.RequestUpdateInstall is nil there; the
// route refuses before this field is ever read). Off by default, for
// the same reason AutoUpdateCheck is: silently replacing your own
// running binary is a bigger step than an outbound version check, and
// opting into "check" does not imply opting into "also apply".
AutoUpdateInstall bool `json:"autoUpdateInstall"`
// Packagizer names packages, picks folders and sets download options as
// links are staged. It is stored exactly as the user wrote it: rules.Compile
// is the validator, and a rule with a broken regular expression has to
// round-trip to disk so the user can find and fix it in the form instead of
// watching it disappear on save.
Packagizer rules.Set `json:"packagizer"`
// LinkFilter decides which links are taken into the collector at all.
// StopAfterMatch usually wants to be on here, so a narrow accept placed above
// a broad reject actually protects the link; it is the user's flag and
// nothing here forces it.
LinkFilter rules.Set `json:"linkFilter"`
// MirrorPolicy is when two different URLs count as the same file.
MirrorPolicy string `json:"mirrorPolicy"`
// KeepMirrors keeps the second copy instead of dropping it: the link is
// staged as a sibling of the download it mirrors, parked, and labelled with
// the task it is a copy of.
//
// Off by default, and the reason is that nothing fails over to a sibling on
// its own yet. What it buys today is that the alternative link survives - a
// dropped mirror lives on only in an in-memory trace that the next restart
// clears - and the price of it being on is a parked row per mirror in a list
// people already complain is long. On is a choice; off is what the list looks
// like now.
KeepMirrors bool `json:"keepMirrors"`
// CollisionPolicy is what happens when the destination file already exists.
CollisionPolicy string `json:"collisionPolicy"`
// CollisionMaxAttempts caps how many counted names a rename tries. Zero means
// the package's own cap.
CollisionMaxAttempts int `json:"collisionMaxAttempts,omitempty"`
// Connections is the user-ordered list of outbound connections downloads are
// spread across. Empty means everything goes out over the machine's own
// connection, which is what an install that never opened the page has.
Connections []proxycfg.Entry `json:"connections,omitempty"`
// Chunks is how many connections ONE download opens, when neither the task
// nor a rule has named a number. It is not about the list above: Connections
// is which way out of the machine the bytes go, this is how many sockets one
// file is pulled over.
//
// Zero is "no opinion", exactly as on the task and for the same reason - the
// dispatcher owns the fallback, and a copy of that number here is a second
// one to forget when the first is changed.
Chunks int `json:"chunks"`
// Reconnect gets the box a new public address when a hoster's free-user limit
// is keyed to the one it has. Off by default: it runs a program or talks to
// the router, and neither should ever happen because a default said so.
Reconnect reconnect.Config `json:"reconnect"`
// Schedule is the timetable that pauses or throttles the queue by the clock.
// An empty timetable changes nothing, which is what a fresh install wants.
//
// It stays a field of this struct rather than a file of its own beside
// settings.json - see the doc comment on PUT /api/schedule in
// routes_schedule.go for why that is a considered choice and not an
// oversight, and setFeature's "scheduler" case in routes_features.go for
// the read-current/write-one-field shape every writer of this field, this
// route included, is expected to use.
Schedule []schedule.Entry `json:"schedule,omitempty"`
// IdleAction is what happens once the wait queue has nothing enabled left
// to run, start or finish, after a cancellable countdown - see
// internal/idleaction. Embedded here rather than in a file of its own for
// the same reason Schedule just above is: one small struct, one settings
// page, no secret in it anywhere. The zero value is Action=ActionNone, so
// a fresh install - and an upgrade that has never seen this key - has
// nothing armed.
IdleAction idleaction.Config `json:"idleAction"`
// ResumeOnStart is what happens to the downloads that were in flight when
// the process last stopped: never, only what was running, or everything
// unfinished. See the constants for what each one costs.
ResumeOnStart string `json:"resumeOnStart"`
// KeepFinishedDays is how long a finished download stays in the LIST. Zero
// keeps it forever.
//
// It never touches the file. Removing a row and deleting what was downloaded
// are two different actions in this app and always have been - conflating
// them is the bug that cost somebody their downloads on the ordinary "clear
// finished" path, and this is the same path running on a timer. What was
// fetched is kept in the history table, which retention does not read.
KeepFinishedDays int `json:"keepFinishedDays"`
// HistoryMax caps the download history. Zero keeps every entry, which is a
// table that only grows on an instance that is never restarted.
HistoryMax int `json:"historyMax"`
// CaptchaSolverOrder is which automatic captcha-solving services
// (internal/accounts.Catalogue ids "2captcha"/"anticaptcha") to try, and
// in what order, before a captcha is ever shown to a human. Membership
// AND order live in the one list - an id absent from it is not tried at
// all, exactly the same "presence in an ordered list is the switch" rule
// the accounts page's own resolver-priority order already uses - rather
// than a separate bool per service that could disagree with where the
// service sits in the order. Empty means what a fresh install has:
// nothing configured, straight to the prompt modal, whether or not a key
// happens to be stored - an id here with no matching credential is
// simply skipped when tried (see sanitizeCaptcha for why an id here
// never implies a stored key, and never the reverse). This is the
// NON-secret half; the API key itself is a credential
// (internal/accounts), never a settings field - see
// internal/accounts/catalogue.go's GroupCaptchaSolver.
//
// No omitempty, deliberately, matching RainbowPalette just above rather
// than ArchivePasswords further up: a nil slice with omitempty is
// DROPPED from the JSON entirely, and web/src/lib/api.ts's Settings
// type has no way to type a field that is sometimes simply absent. A
// nil slice with no omitempty encodes as JSON null instead, so the
// field is always present and the frontend types it `string[] | null`,
// the same pairing RainbowPalette already uses.
CaptchaSolverOrder []string `json:"captchaSolverOrder"`
// Ytdlp is the yt-dlp backend's own configuration - format/quality
// selection, subtitles, the output filename template, whether a
// playlist URL fetches one video or the whole list. See
// internal/resolver/ytdlp's own doc comment on Options for why every
// field's zero value reproduces this backend's behaviour from before
// any of them existed - an install that never opens the settings page
// this backs downloads exactly as it always has.
Ytdlp ytdlp.Options `json:"ytdlp"`
// YtdlpPresets is per-host (e.g. "youtube.com") config for the
// "Variante" rows a yt-dlp link now stages (see ytdlp.HosterPreset's
// own doc comment): which of video/audio/thumbnail/subtitle/
// description land in the collector by default for links from that
// host, and the default quality/audio-format for the two variants that
// have one. A host with no entry here gets ytdlp.DefaultHosterPreset()
// - map, not omitempty, matching CaptchaSolverOrder's own reasoning
// just above for why a field a caller has never touched should not
// vanish from the JSON rather than round-trip as an empty object.
YtdlpPresets map[string]ytdlp.HosterPreset `json:"ytdlpPresets"`
// Torrent is the seed/port/DHT/PEX policy for the BitTorrent backend -
// see settings_torrent.go for the full shape and, especially, for what
// of it is and is not actually enforced by the gopeed dependency this
// build embeds today.
Torrent Torrent `json:"torrent"`
// InstanceID, InstanceName and KnownDomains are this instance's own
// identity - see settings_identity.go for the sanitize hook and all
// three fields' own doc comments.
InstanceID string `json:"instanceId"`
InstanceName string `json:"instanceName"`
KnownDomains []string `json:"knownDomains"`
// RelayURL is the self-hosted relay this instance dials out to so that
// it can be reached by siblings on other networks - see
// settings_relay.go for the field's own doc comment, and especially for
// why the relay key that goes with it is a credential in
// internal/accounts rather than a second field here.
RelayURL string `json:"relayUrl"`
// RelayServe makes this instance run the relay itself, on its own address
// under /relay/connect, for instances carrying the same relay key - see
// settings_relay.go for what that does and does not buy.
RelayServe bool `json:"relayServe"`
}
Settings is the user-visible configuration. Zero values mean "unlimited/off" where noted.
func ApplyPatch ¶
ApplyPatch overlays patch's top-level keys onto base's own JSON encoding and decodes the result back into a Settings, without validating, sanitizing or persisting anything.
Exported, and used two ways: SetPartial calls it inside its own lock to build what it is about to write, and the PATCH /api/settings handler calls it OUTSIDE any lock, against a freshly Get() copy, purely to validate the would-be result the same way PUT validates its whole body before ever reaching the store. settings.Validate(preview.DownloadDir) and validateRows(preview) need a real Settings to inspect, and this is the one path that builds one from a patch. That preview can go stale by the microseconds between the read and SetPartial's own later, authoritative merge under lock; sanitize (inside setLocked) is the same safety net PUT already relies on for anything validateRows does not itself cover, so a value that changed out from under a stale preview is clamped, never corrupted.
Marshal, merge as raw JSON, unmarshal, rather than a hand-written field-by-field copy, because Settings already knows how to become and come back from exactly this shape, and a second, hand-maintained copy of "every field this struct has" is one waves 1-11 have already shown drifts (settingsKinds' own doc comment in routes_features.go makes the identical argument for reflecting over the struct instead of listing it by hand, in the opposite direction of the same document). An unknown key in patch is silently dropped by the final Unmarshal, the same as every other decode in this codebase (see decodeJSON's own doc comment), not a new inconsistency introduced here.
func (Settings) Redacted ¶
Redacted returns a copy safe to hand to a browser. Two secrets live in here now — the router password and every proxy password — and the endpoint that serves the settings must use nothing but this: the moment a client is shown them, the merge machinery in Set is protecting a value it already holds.
The two packages disagree about how to hide a password, deliberately. reconnect masks it with a placeholder that WithSecretsFrom reads back, so an empty string can keep meaning "clear it"; proxycfg drops it and lets Merge put it back when the row still describes the same connection. Neither is wrapped or normalised here, because each is one half of a round trip its own package owns.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) SetPartial ¶
SetPartial applies patch on top of whatever is CURRENTLY stored and persists the result, exactly like Set, except the fields patch does not name are read from that current copy under the same lock that then writes the result back, never from a copy the caller fetched earlier. Two partial saves racing each other therefore compose (a speedLimit patch and a concurrent, unrelated maxConcurrent patch both survive) instead of the second one's read predating the first one's write and silently reverting it. That is the same class of bug `PATCH /api/settings` exists to close, guarded one layer further in than the HTTP handler alone could reach: see Set's own comment just above for why taking a snapshot outside this lock is exactly the mistake that already had to be avoided once, for Reconnect's secret merge.
patch's keys are top-level only, exactly as Settings' own JSON encoding has them: a key present replaces that whole field. An object field replaces the whole sub-document, not a deep per-field merge, so a partial Reconnect edit still carries the whole Reconnect object, the same shape the Reconnect settings page already saves today, and a key absent leaves the stored field untouched. There is deliberately no dotted-path syntax for reaching inside a nested field here: 2I's advanced key table (routes_features.go, settings_describe.go) already owns that job and its own validation pass, and duplicating a second one here is how the two quietly disagree about what a clamp allows.
type Torrent ¶
type Torrent struct {
// SeedRatioTarget is uploaded-over-downloaded; 0 means no ratio target
// (only SeedDurationSeconds, if that is also non-zero, applies). See
// SeedDurationSeconds' own doc comment for the combination rule and where
// it actually comes from.
//
// 1.0 - seed until every byte received has been given back once - is
// gopeed's own built-in default
// (internal/protocol/bt.FetcherManager.DefaultConfig), mirrored here
// rather than inventing a KnightLoader-specific number nobody has reason
// to prefer over the one the engine underneath already ships with.
SeedRatioTarget float64 `json:"seedRatioTarget"`
// SeedDurationSeconds is how long to keep seeding after the download
// itself finishes; 0 means no duration target (only SeedRatioTarget, if
// that is also non-zero, applies). Both zero seeds forever - see below.
//
// 7200 (two hours) is gopeed's own default (SeedTime: 120 * 60 in the
// same DefaultConfig SeedRatioTarget's default came from), mirrored for
// the same reason.
//
// WHICHEVER OF THE TWO IS REACHED FIRST STOPS SEEDING. docs/torrent-
// support.md flags this as a rule KnightLoader decided rather than one
// the grilling settled explicitly - it is the only combination that
// makes sense of having both fields at once (a second target that only
// matters once the first has somehow not been enough would need
// different names), so it is implemented on that basis, said plainly
// here rather than presented as a settled decision.
//
// It is also, independently, already what gopeed itself does once these
// two numbers reach it - confirmed by reading
// internal/protocol/bt/fetcher.go's doUpload: it checks
// config.SeedRatio and config.SeedTime in the same loop, each closing
// the fetcher on its own the instant it is met, with no ordering between
// the two. KnightLoader's job is to surface these two numbers to gopeed,
// not to re-implement the stop condition against them - see Port's doc
// comment for how that surfacing happens (ProtocolConfig["bt"]) and for
// how, unlike Port, it reaches every torrent added from here on rather
// than only the first. Engine.SetTorrentConfig
// (internal/engine/engine.go) is what carries it, called from
// internal/app's afterSettingsChange on every save and once at boot
// (internal/app/app.go) - each new torrent's own Fetcher.Setup reads
// ProtocolConfig["bt"] fresh (internal/protocol/bt/fetcher.go), so a
// value saved here is live for the very next torrent added, not only
// after a restart.
//
// Both zero seeds forever without needing gopeed's separate SeedKeep
// switch: its loop only stops on a ">0" target being met, so with
// neither one set it never stops on its own. This settings block does
// not expose SeedKeep for that reason - a zero/zero pair already means
// the same thing, and a THIRD field that agrees with a state two others
// already reach is one more way for a save to disagree with itself.
SeedDurationSeconds int `json:"seedDurationSeconds"`
// UploadLimitKiBs caps upload bandwidth in KiB/s; 0 is unlimited, the
// same convention Settings.SpeedLimit already uses for downloads.
//
// UNWIRED, as of this wave: gopeed's own per-protocol config
// (internal/protocol/bt.config, five fields, read by reading the
// vendored v1.9.3 source directly since the package is internal/ and
// unreachable from here) has no upload-rate field at all, and its
// client is built with UploadRateLimiter left at anacrolix/torrent's own
// "unlimited" default. This number has nowhere to go through gopeed's
// public surface yet. It is stored anyway so the settings page and the
// API shape can exist ahead of an engine that can honour it - the same
// reasoning core.TorrentFile's own doc comment gives for a field
// persisted before everything that will read it exists.
UploadLimitKiBs int `json:"uploadLimitKiBs"`
// Port is the TCP port this instance's torrent client listens on; 0 lets
// gopeed (in turn anacrolix/torrent) pick - which is also gopeed's own
// shipped default. Verified by reading both:
// internal/protocol/bt.FetcherManager.DefaultConfig sets ListenPort: 0,
// which OVERRIDES anacrolix/torrent's own non-zero 42069 default,
// because initClient unconditionally assigns
// cfg.ListenPort = f.config.ListenPort - gopeed's zero always wins.
//
// Unlike UploadLimitKiBs this one IS represented on gopeed's own config
// surface (internal/protocol/bt.config.ListenPort, reached through
// DownloaderStoreConfig.ProtocolConfig["bt"]) - carrying this value
// there is internal/engine's job (Engine.SetTorrentConfig,
// internal/engine/engine.go), called from internal/app's
// afterSettingsChange on every save and once at boot, the same call as
// SeedRatioTarget/SeedDurationSeconds above. UNLIKE THOSE TWO, a value
// saved here only takes if no torrent has started yet this process: it
// is subject to the same once-only construction DHTEnabled describes
// just below - only the very first torrent task this process ever
// starts can make a changed port stick, because gopeed's bt client is a
// lazy singleton (internal/protocol/bt.Fetcher.initClient, "if client !=
// nil { return }"), built once and never rebuilt for the rest of the
// process's life. A save after that point is still stored correctly and
// still reaches gopeed's own config; it simply has no torrent left this
// process will ever start that could read it before the client is
// already built.
Port int `json:"port"`
// DHTEnabled and PEXEnabled are this instance's DEFAULT participation in
// the swarm's own peer discovery - Distributed Hash Table lookups and
// Peer Exchange - for an ORDINARY, non-private torrent. A torrent whose
// own metadata marks it private (BEP 27's info.private) gets neither,
// automatically, with no user toggle able to set either back to true for
// that torrent - and unlike the ordinary-torrent default, that half of
// decision 5 needed no wiring from this package or from internal/engine
// to start being true. See EffectiveDHT/EffectivePEX below for that
// per-torrent decision as this package states it, and the third bullet
// below for why stating it was already enough for the private half.
//
// READ THIS BEFORE CHANGING EITHER FIELD'S DEFAULT, OR BEFORE TELLING A
// USER THIS SETTING DOES SOMETHING FOR AN ORDINARY TORRENT. Verified by
// reading the vendored dependency tree end to end - originally against
// github.com/GopeedLab/gopeed v1.9.3 and its then-pinned
// github.com/anacrolix/torrent v1.60.1-0.20251217073903, re-verified
// after bumping the latter to v1.61.1-0.20260525011549 (go.mod's own
// comment on that require line names the commit and the upstream PR) -
// not assumed, the same discipline internal/resolver/torrent's own
// package doc comment used for the fact this whole feature rests on:
//
// - Neither field has anywhere to go FOR AN ORDINARY TORRENT, still.
// gopeed's own per-protocol config (internal/protocol/bt.config) has
// five fields - ListenPort, Trackers, SeedKeep, SeedRatio, SeedTime -
// and none of them is DHT or PEX. A whole-module grep of the
// vendored gopeed source for "NoDHT" or "DisablePEX" (the two
// anacrolix/torrent ClientConfig fields that gate this at the
// CLIENT level, for every torrent it will ever handle) returns ZERO
// matches, in either gopeed's public packages or its internal ones -
// unchanged by the anacrolix/torrent bump below, since gopeed's own
// bt.Fetcher.initClient still builds the shared torrent.Client from
// torrent.NewDefaultClientConfig() and still overrides exactly six
// fields (Seed, Bep20, ExtendedHandshakeClientVersion, ListenPort,
// HTTPProxy, TrackerDialContext), never NoDHT, never DisablePEX.
// Both therefore still sit at anacrolix's own defaults (both false:
// DHT on, PEX on) for the life of the process, for every torrent,
// and nothing KnightLoader passes through gopeed's public API can
// change that today. A user who sets either field to false wanting
// it to hold for their own ordinary public torrents is saving a
// preference gopeed's client keeps ignoring - this half of decision
// 5 is not what got fixed below.
// - It would still not be per-torrent through THAT door even if it
// were wired. NoDHT and DisablePEX are torrent.ClientConfig fields,
// consumed once by torrent.NewClient at the moment gopeed's bt
// client is first built - a package-level singleton
// (internal/protocol/bt's own `client` var) whose initClient returns
// immediately ("if client != nil { return }") for every torrent
// after the first, for the rest of the process. A client-level
// NoDHT/DisablePEX, even if gopeed exposed one, would only ever be a
// process-wide, decided-once knob.
// - THE PRIVATE-TORRENT HALF OF DECISION 5 IS DELIVERED, through a
// third door neither bullet above accounts for. gopeed's own
// handling is unchanged and still narrower than
// docs/torrent-support.md originally guessed: reading info.Private
// is real (internal/protocol/bt/fetcher.go's addTorrent does it, for
// an uploaded .torrent - a magnet's own branch never attempts it,
// since a magnet carries no info dict to read one out of before the
// swarm answers), but the only thing gopeed itself does with the
// result is skip adding EXTRA trackers to a private torrent's
// announce list - confirmed again by re-reading the whole of
// addTorrent after the bump. gopeed never touches DHT or PEX for
// that torrent, and no longer needs to: anacrolix/torrent
// v1.61.1-0.20260525011549 (upstream PR #1053, "Implement BEP 27
// (private torrents)", merged 2026-05-25) added a Torrent.isPrivate()
// check directly inside dhtAnnouncer's own per-iteration announce
// loop (torrent.go), PEX's connection init (pexconn.go) and both
// directions of Local Peer Discovery (client.go) - five gates, all
// reading the TORRENT's own already-parsed info.Private, none of
// them reading the CLIENT's NoDHT/DisablePEX. That is the
// per-torrent override the first two bullets could never reach on a
// shared, once-built client - solved one layer lower than gopeed,
// inside the library gopeed is itself built on, with nothing left
// for gopeed or KnightLoader to wire. KnightLoader's own go.mod
// already named anacrolix/torrent as a DIRECT requirement, not
// merely transitive through gopeed, so Go's own
// minimum-version-selection let this be a version bump on our side
// alone. End-to-end wiring verified by reading the bumped source
// directly, not assumed from the PR description: spec.go's
// TorrentSpecFromMetaInfoErr carries info.Private through in
// InfoBytes untouched, and client.go's AddTorrentOpt parses those
// bytes into the Torrent's own info, under the client lock, before
// the DHT-announcer goroutines it just spawned can take that same
// lock to read it - no race window for an uploaded .torrent. A
// magnet has no info dict to read this from until the swarm
// answers, so its privacy is enforced from the moment metadata
// arrives, not before - upstream's own comment on the per-iteration
// re-check ("we re-check every loop because info may be loaded
// later, e.g. via magnet") names the same gap this settings block
// would otherwise have had to.
//
// DHTEnabled, PEXEnabled and EffectiveDHT/EffectivePEX below remain a
// declared POLICY for the ORDINARY-torrent half of decision 5 only - the
// private half above needs none of the three to already hold true.
// Wiring the ordinary-torrent half is still real future work, not a
// formality: short of a gopeed change (upstream patch, or a fork) that
// exposes ClientConfig.NoDHT/DisablePEX on its public surface, or
// KnightLoader reaching gopeed's client through something other than
// its current public API, the best available today is accepting DHT/PEX
// can only be decided process-wide, once, before the first torrent of
// the process's life. Whoever wires it must not ship a UI that implies
// the ordinary-torrent preference already holds - that is the identical
// "looked correct on paper" shape Wave 10's own review already had to
// walk back once, over a check that was a tautology by construction
// rather than by anyone's intent.
DHTEnabled bool `json:"dhtEnabled"`
// PEXEnabled - see DHTEnabled immediately above; everything there
// applies here identically, PEX and DHT being gated by sibling
// ClientConfig fields for the ordinary-torrent case that remains
// unwired, and by sibling isPrivate() gates for the private-torrent case
// that no longer needs wiring at all.
PEXEnabled bool `json:"pexEnabled"`
}
Torrent is the seed/port/DHT/PEX policy for every BitTorrent download this instance starts - one block per instance, not one per task. That matches gopeed's own bt fetcher: DHT/PEX participation and the listen port are properties of the ONE embedded torrent client every task shares (github.com/GopeedLab/gopeed@v1.9.3's internal/protocol/bt.Fetcher. initClient builds it once, lazily, on the first torrent this process ever touches, and never rebuilds it) - a per-task override of either through GOPEED'S OWN SURFACE is not a KnightLoader design choice being deferred, it is a shape that surface does not offer at all. See DHTEnabled's own doc comment for what was verified about that, for the one per-torrent override that reaches around gopeed's surface entirely (a private torrent's own DHT/PEX refusal), and for what the rest still means for the ordinary-torrent half of the private-torrent promise below.
func (Torrent) EffectiveDHT ¶
EffectiveDHT and EffectivePEX are the per-torrent decision that decision 5 of the grilling actually asks for: this instance's own default, unless the torrent itself is private, in which case always false - a private torrent gets no vote, from this setting or from anything a user does to it afterwards, because there is no argument here for one to override it with.
private is BEP 27's info.private, read wherever a torrent's metadata is first parsed - internal/resolver/torrent.Metadata.Private for an uploaded .torrent, checked before a byte is written; for a magnet the swarm has to answer first, which happens inside internal/engine's own resolve, not at paste time. Taking a bare bool rather than that Metadata type keeps this package from depending on the resolver for one field of it, and keeps this function usable by a caller that only ever has the bool to begin with.
CALLING THIS ONLY MATTERS FOR THE ORDINARY-TORRENT (private=false) CASE. See DHTEnabled's own doc comment for the full account: nothing in internal/engine reads either return value, for either branch, and for private=false that means gopeed's public API still gives this package nothing to set - that half is a policy, correct and tested on its own terms (see settings_torrent_test.go), waiting on an enforcement point that does not exist yet. The private=true branch is different: it already matches what anacrolix/torrent enforces on its own, inside the library, whether or not anything ever calls this function - see DHTEnabled's doc comment for how. This function stating "false" for a private torrent is therefore correct but not load-bearing; it would already be false in effect even if this function did not exist.
func (Torrent) EffectivePEX ¶
EffectivePEX - see EffectiveDHT immediately above.