all: cut bloated HTTP-client and tuning options

This is a personal torrent client, not a web client or daemon, so the
rarely-used HTTP-client knobs and redundant tuning flags are gone — flag,
config field, implementation, and tests removed for each. The option
surface drops from 54 to 28.

Web-client cluster removed: --all-proxy (HTTP_PROXY env still works),
load/save-cookies, --referer, -U/--user-agent, --http-user/--http-passwd,
--conditional-get, --remote-time, --check-certificate, --ca-certificate,
--lowest-speed-limit. Pass --header for a one-off auth/cookie/UA header.

Redundant knobs removed: -x and -k (folded into -s), --connect-timeout,
--retry-wait, per-item --max-download-limit/-u, --bt-max-peers,
--bt-stop-timeout, --disable-ipv6, --auto-save-interval, --human-readable,
--stop, -T (a positional .torrent works). follow-torrent and human-readable
are now unconditional defaults.
This commit is contained in:
2026-06-22 08:30:12 +09:00
parent 2afcb861e4
commit cf9082ff72
15 changed files with 157 additions and 1053 deletions

136
main.go
View File

@@ -88,12 +88,6 @@ func run(args []string) int {
sessionFile := opts.Str("save-session")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// --stop: cancel everything after N seconds, exactly like a Ctrl-C, so an
// unattended run can't hang forever (0 = off).
if stop := opts.Int("stop"); stop > 0 {
t := time.AfterFunc(time.Duration(stop)*time.Second, cancel)
defer t.Stop()
}
var jobsRef atomic.Pointer[[]job]
// publish snapshots the slice header (j is a by-value copy) and stores a
// pointer to it, so the signal handler can read the current jobs lock-free
@@ -106,7 +100,7 @@ func run(args []string) int {
uiCtx, uiCancel := context.WithCancel(context.Background())
uiDone := make(chan struct{})
if !opts.Bool("quiet") && !opts.Bool("dry-run") {
rep := progress.New(eng.Snapshot, opts.Bool("human-readable"))
rep := progress.New(eng.Snapshot, true)
go func() {
rep.Run(uiCtx)
close(uiDone)
@@ -118,10 +112,9 @@ func run(args []string) int {
publish(jobs)
results := runJobs(ctx, eng, jobs)
// --follow-torrent: a .torrent fetched over HTTP becomes a BitTorrent
// download of its content (on by default). Done as a second pass so the
// engine and scheduler stay simple.
if opts.Bool("follow-torrent") && ctx.Err() == nil && !opts.Bool("dry-run") {
// A .torrent fetched over HTTP becomes a BitTorrent download of its content,
// done as a second pass so the engine and scheduler stay simple.
if ctx.Err() == nil && !opts.Bool("dry-run") {
if follow := followUps(jobs, btClient, btOptions(opts)); len(follow) > 0 {
// The engine numbers each Result by its position in the slice it was
// passed, so the follow pass restarts at Index 0. Offset those by the
@@ -141,8 +134,6 @@ func run(args []string) int {
uiCancel()
<-uiDone // wait for the renderer's final frame before printing the OK/FAIL lines
saveCookies(jobs)
if sessionFile != "" {
if err := saveSession(sessionFile, jobs); err != nil {
fmt.Fprintln(os.Stderr, "got:", err)
@@ -208,11 +199,11 @@ func seenInfoHash(seen map[metainfo.Hash]string, source string, isFile bool) (fi
// dedupeTorrents finds torrent/magnet sources that repeat an earlier source's
// infohash, returning each duplicate URI mapped to the source it repeats.
func dedupeTorrents(uris []string, follow bool) map[string]string {
func dedupeTorrents(uris []string) map[string]string {
seen := map[metainfo.Hash]string{}
dup := map[string]string{}
for _, u := range uris {
k := uriKind(u, follow)
k := uriKind(u)
if k != kindMagnet && k != kindTorrent {
continue
}
@@ -230,7 +221,7 @@ func build(opts *cli.Options, targets []target) ([]job, *torrent.Client, error)
// finish would Drop it out from under the other. The duplicate is reported as
// a failed download (duplicate infohash, exit 12); the loop below does this,
// so the duplicate never obtains or Drops a shared torrent.
dups := dedupeTorrents(flat, opts.Bool("follow-torrent"))
dups := dedupeTorrents(flat)
overallDL := makeLimiter(opts.Size("max-overall-download-limit"))
overallUL := makeLimiter(opts.Size("max-overall-upload-limit"))
@@ -248,7 +239,7 @@ func build(opts *cli.Options, targets []target) ([]job, *torrent.Client, error)
// it (e.g. the listen port is taken) disables BitTorrent but still lets HTTP
// downloads run; those torrents then report the error.
var client *torrent.Client
if needsBT(opts, flat) {
if needsBT(flat) {
c, err := bt.NewClient(btClientConfig(opts, overallDL, overallUL))
if err != nil {
fmt.Fprintln(os.Stderr, "got: bittorrent disabled:", err)
@@ -269,7 +260,7 @@ func build(opts *cli.Options, targets []target) ([]job, *torrent.Client, error)
continue
}
var dl download.Download
switch uriKind(u, opts.Bool("follow-torrent")) {
switch uriKind(u) {
case kindMagnet:
dl = bt.New(client, u, false, bo)
case kindHTTP, kindFollow:
@@ -288,16 +279,15 @@ func build(opts *cli.Options, targets []target) ([]job, *torrent.Client, error)
}
// uriKind classifies a URI by scheme into the taxonomy build() and needsBT()
// both switch on. Scheme wins over suffix: an http URL ending in .torrent is a
// file to fetch over HTTP, and with --follow-torrent it becomes a torrent only
// after the fetch (kindFollow), so it still downloads over HTTP first.
func uriKind(u string, follow bool) kind {
// both switch on. Scheme wins over suffix: an http URL ending in .torrent is
// fetched over HTTP first, then followed as a torrent (kindFollow).
func uriKind(u string) kind {
lu := strings.ToLower(u)
switch {
case strings.HasPrefix(u, "magnet:"):
return kindMagnet
case strings.HasPrefix(u, "http://"), strings.HasPrefix(u, "https://"):
if follow && strings.HasSuffix(lu, ".torrent") {
if strings.HasSuffix(lu, ".torrent") {
return kindFollow
}
return kindHTTP
@@ -319,10 +309,9 @@ const (
// needsBT reports whether the run will involve any torrent, so we only start a
// client when one is actually needed.
func needsBT(opts *cli.Options, uris []string) bool {
follow := opts.Bool("follow-torrent")
func needsBT(uris []string) bool {
for _, u := range uris {
switch uriKind(u, follow) {
switch uriKind(u) {
case kindMagnet, kindTorrent, kindFollow:
return true
}
@@ -387,19 +376,6 @@ func followUps(jobs []job, client *torrent.Client, bo bt.Options) []job {
return out
}
// saveCookies asks each HTTP download to write its cookie jar back to the
// --save-cookies file, persisting cookies at session end. A download with no
// --save-cookies configured is a no-op.
func saveCookies(jobs []job) {
for _, j := range jobs {
if h, ok := j.dl.(*httpdl.Download); ok {
if err := h.SaveCookies(); err != nil {
fmt.Fprintln(os.Stderr, "got:", err)
}
}
}
}
// target is one download: a single torrent/magnet/URL, or several HTTP mirror
// URLs that serve identical bytes (uris[0] is the primary).
type target struct{ uris []string }
@@ -414,16 +390,14 @@ func allURIs(ts []target) []string {
return u
}
// gatherURIs collects download targets from the command line, --torrent-file,
// and --input-file. Plain command-line http/https URIs are grouped into ONE
// mirrored download by default (every command-line URI names the same file);
// -Z/--force-sequential makes each its own download. Torrents, magnets, and
// .torrent URLs (fetched over HTTP then followed) are always one target each,
// since distinct torrents are not mirrors of one another; --input-file groups
// TAB-separated URIs per line.
// gatherURIs collects download targets from the command line and --input-file.
// Plain command-line http/https URIs are grouped into ONE mirrored download by
// default (every command-line URI names the same file); -Z/--force-sequential
// makes each its own download. Torrents, magnets, and .torrent URLs (fetched
// over HTTP then followed) are always one target each, since distinct torrents
// are not mirrors of one another; --input-file groups TAB-separated URIs per line.
func gatherURIs(opts *cli.Options, cmdURIs []string) []target {
var ts []target
follow := opts.Bool("follow-torrent")
seq := opts.Bool("force-sequential")
var httpGroup []string
for _, u := range cmdURIs {
@@ -431,7 +405,7 @@ func gatherURIs(opts *cli.Options, cmdURIs []string) []target {
// for the same file); -Z opts out. A .torrent URL (kindFollow), like a
// torrent or magnet, is its own download — distinct torrents are never
// mirrors of one another.
if uriKind(u, follow) == kindHTTP && !seq {
if uriKind(u) == kindHTTP && !seq {
httpGroup = append(httpGroup, u)
} else {
ts = append(ts, target{uris: []string{u}})
@@ -440,9 +414,6 @@ func gatherURIs(opts *cli.Options, cmdURIs []string) []target {
if len(httpGroup) > 0 {
ts = append(ts, target{uris: httpGroup})
}
if tf := opts.Str("torrent-file"); tf != "" {
ts = append(ts, target{uris: []string{tf}})
}
if in := opts.Str("input-file"); in != "" {
ts = append(ts, readInputFile(in)...)
}
@@ -497,49 +468,20 @@ func httpConfig(opts *cli.Options, single bool, overallDL *rate.Limiter) httpdl.
if single {
out = opts.Str("out")
}
// max-connection-per-server caps connections per host. Its default is 1,
// which on its own would clamp --split to a single connection, so a bare
// `got URL` would download single-threaded. Honor the literal default of 1
// only when the user actually set -x; otherwise let --split drive per-host
// parallelism, capped at the -x ceiling of 16. -x explicitly set keeps the
// exact min(split, mirrors*x) behavior.
perHost := opts.Int("max-connection-per-server")
if !opts.IsSet("max-connection-per-server") {
perHost = min(opts.Int("split"), 16)
}
return httpdl.Config{
Dir: opts.Str("dir"),
Out: out,
Split: opts.Int("split"),
MaxConnPerServer: perHost,
MinSplit: opts.Size("min-split-size"),
Tries: opts.Int("max-tries"),
Timeout: time.Duration(opts.Int("timeout")) * time.Second,
FileAlloc: opts.Str("file-allocation"),
Continue: opts.Bool("continue"),
AllowOverwrite: opts.Bool("allow-overwrite"),
AutoRename: opts.Bool("auto-file-renaming"),
Headers: opts.List("header"),
UserAgent: opts.Str("user-agent"),
Referer: opts.Str("referer"),
Proxy: opts.Str("all-proxy"),
CheckCert: opts.Bool("check-certificate"),
Limit: opts.Size("max-download-limit"),
OverallLimiter: overallDL,
RetryWait: time.Duration(opts.Int("retry-wait")) * time.Second,
AutoSaveInterval: time.Duration(opts.Int("auto-save-interval")) * time.Second,
ConnectTimeout: time.Duration(opts.Int("connect-timeout")) * time.Second,
LoadCookies: opts.Str("load-cookies"),
SaveCookies: opts.Str("save-cookies"),
ConditionalGet: opts.Bool("conditional-get"),
RemoteTime: opts.Bool("remote-time"),
HTTPUser: opts.Str("http-user"),
HTTPPasswd: opts.Str("http-passwd"),
LowestSpeedLimit: opts.Size("lowest-speed-limit"),
Checksum: opts.Str("checksum"),
DryRun: opts.Bool("dry-run"),
CACert: opts.Str("ca-certificate"),
DisableIPv6: opts.Bool("disable-ipv6"),
Dir: opts.Str("dir"),
Out: out,
Split: opts.Int("split"),
Tries: opts.Int("max-tries"),
Timeout: time.Duration(opts.Int("timeout")) * time.Second,
FileAlloc: opts.Str("file-allocation"),
Continue: opts.Bool("continue"),
AllowOverwrite: opts.Bool("allow-overwrite"),
AutoRename: opts.Bool("auto-file-renaming"),
Headers: opts.List("header"),
OverallLimiter: overallDL,
Checksum: opts.Str("checksum"),
DryRun: opts.Bool("dry-run"),
}
}
@@ -549,13 +491,8 @@ func btClientConfig(opts *cli.Options, overallDL, overallUL *rate.Limiter) bt.Cl
Dir: opts.Str("dir"),
ListenPortSpec: opts.Str("listen-port"),
DHT: opts.Bool("enable-dht"),
MaxPeers: opts.Int("bt-max-peers"),
OverallDown: overallDL,
OverallUp: overallUL,
DownLimit: opts.Size("max-download-limit"),
UpLimit: opts.Size("max-upload-limit"),
UserAgent: opts.Str("user-agent"),
DisableIPv6: opts.Bool("disable-ipv6"),
}
}
@@ -565,7 +502,6 @@ func btOptions(opts *cli.Options) bt.Options {
SeedTimeSet: opts.IsSet("seed-time"),
SeedTime: time.Duration(opts.Float("seed-time") * float64(time.Minute)),
SeedRatio: opts.Float("seed-ratio"),
StopTimeout: time.Duration(opts.Int("bt-stop-timeout")) * time.Second,
SelectFiles: parseSelect(opts.Str("select-file")),
CheckIntegrity: opts.Bool("check-integrity"),
DryRun: opts.Bool("dry-run"),
@@ -686,8 +622,6 @@ func exitCode(err error) int {
return 6
case errors.Is(err, httpdl.ErrNotFound):
return 3
case errors.Is(err, httpdl.ErrTooSlow):
return 5
case errors.Is(err, httpdl.ErrChecksum):
return 32
case errors.Is(err, errDuplicate):