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:
136
main.go
136
main.go
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user