all: cut bloat found in the pike audit
Dead, vestigial, and over-built code with no behavior change (except the
bool-vocabulary trim, a deliberate behavior tightening):
- httpdl: drop the vestigial sort+copy in validSegs (work-stealing was
removed in 270812d, so segments always tile [0,total) ascending now);
validate order in place, which also rejects a scrambled sidecar. Inline
the endOff() accessor to s.end.
- cli: shrink the boolean vocabulary to true/false, which is all that's
accepted. The invented yes/no/1/0/on/off spellings are gone, so e.g.
--enable-dht=yes (or yes in got.conf) is now a usage error.
- download: remove the dead, never-called Stat.Done() method.
- progress: drop speed(), a pure alias of humanSize; call humanSize directly.
- main: replace the jobsHolder mutex box with atomic.Pointer[[]job] for the
forced-exit session save; the capability stays, the lock and type go.
- bt: collapse isAddrInUse's X||X (missinggo.IsAddrInUse is that exact
string match); drop the now-unused missinggo import (tidy -> indirect).
- option: note that prealloc and falloc are identical here.
This commit is contained in:
8
bt/bt.go
8
bt/bt.go
@@ -19,7 +19,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
alog "github.com/anacrolix/log"
|
alog "github.com/anacrolix/log"
|
||||||
missinggo "github.com/anacrolix/missinggo/v2"
|
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
"github.com/hanbok/got/download"
|
"github.com/hanbok/got/download"
|
||||||
@@ -172,10 +171,11 @@ func NewClient(cfg ClientConfig) (*torrent.Client, error) {
|
|||||||
return cl, nil
|
return cl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAddrInUse reports whether err is an "address already in use" bind failure,
|
// isAddrInUse reports whether err is an "address already in use" bind failure.
|
||||||
// matching either missinggo's helper or the raw string the library wraps.
|
// (anacrolix/missinggo's IsAddrInUse is this exact string match, so we just do
|
||||||
|
// it directly rather than carry the import for it.)
|
||||||
func isAddrInUse(err error) bool {
|
func isAddrInUse(err error) bool {
|
||||||
return err != nil && (missinggo.IsAddrInUse(err) || strings.Contains(err.Error(), "address already in use"))
|
return err != nil && strings.Contains(err.Error(), "address already in use")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options are the per-download settings.
|
// Options are the per-download settings.
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ var options = []Opt{
|
|||||||
{Long: "allow-overwrite", Kind: Bool, Default: "false", Help: "overwrite an existing file", Tag: Advanced},
|
{Long: "allow-overwrite", Kind: Bool, Default: "false", Help: "overwrite an existing file", Tag: Advanced},
|
||||||
{Long: "auto-file-renaming", Kind: Bool, Default: "true", Help: "rename file (.1, .2, ...) if it already exists", Tag: Advanced},
|
{Long: "auto-file-renaming", Kind: Bool, Default: "true", Help: "rename file (.1, .2, ...) if it already exists", Tag: Advanced},
|
||||||
{Long: "human-readable", Kind: Bool, Default: "true", Help: "show sizes as Ki/Mi/Gi", Tag: Advanced},
|
{Long: "human-readable", Kind: Bool, Default: "true", Help: "show sizes as Ki/Mi/Gi", Tag: Advanced},
|
||||||
{Long: "file-allocation", Short: 'a', Kind: Enum, Default: "prealloc", Enum: []string{"none", "prealloc", "trunc", "falloc"}, Help: "how to allocate disk space", Tag: Advanced},
|
{Long: "file-allocation", Short: 'a', Kind: Enum, Default: "prealloc", Enum: []string{"none", "prealloc", "trunc", "falloc"}, Help: "how to reserve disk space (prealloc and falloc are identical here)", Tag: Advanced},
|
||||||
{Long: "dry-run", Kind: Bool, Default: "false", Help: "check that the file is available but do not download it", Tag: Advanced},
|
{Long: "dry-run", Kind: Bool, Default: "false", Help: "check that the file is available but do not download it", Tag: Advanced},
|
||||||
{Long: "stop", Kind: Int, Default: "0", Min: 0, Help: "stop the program after N seconds (0 = off)", Tag: Advanced},
|
{Long: "stop", Kind: Int, Default: "0", Min: 0, Help: "stop the program after N seconds (0 = off)", Tag: Advanced},
|
||||||
{Long: "disable-ipv6", Kind: Bool, Default: "false", Help: "disable IPv6 (force IPv4-only connections)", Tag: Advanced},
|
{Long: "disable-ipv6", Kind: Bool, Default: "false", Help: "disable IPv6 (force IPv4-only connections)", Tag: Advanced},
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ func (o *Options) IsSet(name string) bool { return o.set[name] }
|
|||||||
// Str returns the raw string value (empty if unset and no default).
|
// Str returns the raw string value (empty if unset and no default).
|
||||||
func (o *Options) Str(name string) string { return o.vals[name] }
|
func (o *Options) Str(name string) string { return o.vals[name] }
|
||||||
|
|
||||||
// boolWords is the single accepted vocabulary for boolean options, mapping each
|
// boolWords is the accepted vocabulary for boolean options. aria2 takes only
|
||||||
// recognised spelling to its truth value. The Bool reader, truthy, and
|
// true/false (and rejects everything else), so we match it rather than inventing
|
||||||
// validate() all consult it, so exactly the words that validate are honoured
|
// extra spellings. boolWord is the single consult point — the Bool reader,
|
||||||
// (no "accepted by the reader but rejected by validate" surprises like --x=on).
|
// validate(), and the no-conf bootstrap all go through it — so exactly the words
|
||||||
|
// that validate are honoured.
|
||||||
var boolWords = map[string]bool{
|
var boolWords = map[string]bool{
|
||||||
"true": true, "yes": true, "1": true, "on": true,
|
"true": true,
|
||||||
"false": false, "no": false, "0": false, "off": false,
|
"false": false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// boolWord reports a value's truth and whether it is a recognised boolean word.
|
// boolWord reports a value's truth and whether it is a recognised boolean word.
|
||||||
|
|||||||
@@ -139,13 +139,17 @@ func TestValidateFloatNonNegative(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateBool(t *testing.T) {
|
func TestValidateBool(t *testing.T) {
|
||||||
b := byLong["enable-dht"]
|
b := byLong["enable-dht"]
|
||||||
for _, ok := range []string{"true", "false", "yes", "no", "1", "0", "on", "off"} {
|
// aria2 accepts only true/false; the invented yes/no/1/0/on/off spellings
|
||||||
|
// were removed, so they must now fail validation.
|
||||||
|
for _, ok := range []string{"true", "false"} {
|
||||||
if err := validate(b, ok); err != nil {
|
if err := validate(b, ok); err != nil {
|
||||||
t.Errorf("validate bool %q: %v", ok, err)
|
t.Errorf("validate bool %q: %v", ok, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := validate(b, "flase"); err == nil {
|
for _, bad := range []string{"yes", "no", "1", "0", "on", "off", "flase"} {
|
||||||
t.Errorf("validate bool flase: want error")
|
if err := validate(b, bad); err == nil {
|
||||||
|
t.Errorf("validate bool %q: want error (only true/false accepted)", bad)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,6 @@ type Stat struct {
|
|||||||
Seeders int // connected seeders (BitTorrent only)
|
Seeders int // connected seeders (BitTorrent only)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done reports whether the download has reached a terminal state.
|
|
||||||
func (s Stat) Done() bool { return s.Status == Complete || s.Status == Errored }
|
|
||||||
|
|
||||||
// A Download is one logical job: a URL, a torrent, a magnet. Run blocks in its
|
// A Download is one logical job: a URL, a torrent, a magnet. Run blocks in its
|
||||||
// own goroutine until the work finishes, fails, or ctx is cancelled. Stat may
|
// own goroutine until the work finishes, fails, or ctx is cancelled. Stat may
|
||||||
// be called concurrently at any time and must not block; implementations back
|
// be called concurrently at any time and must not block; implementations back
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -4,7 +4,6 @@ go 1.25
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb
|
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb
|
||||||
github.com/anacrolix/missinggo/v2 v2.10.0
|
|
||||||
github.com/anacrolix/torrent v1.61.0
|
github.com/anacrolix/torrent v1.61.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/term v0.37.0
|
golang.org/x/term v0.37.0
|
||||||
@@ -22,6 +21,7 @@ require (
|
|||||||
github.com/anacrolix/go-libutp v1.3.2 // indirect
|
github.com/anacrolix/go-libutp v1.3.2 // indirect
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.10.0 // indirect
|
||||||
github.com/anacrolix/mmsg v1.0.1 // indirect
|
github.com/anacrolix/mmsg v1.0.1 // indirect
|
||||||
github.com/anacrolix/multiless v0.4.0 // indirect
|
github.com/anacrolix/multiless v0.4.0 // indirect
|
||||||
github.com/anacrolix/stm v0.5.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ func controlPath(out string) string { return out + ".got" }
|
|||||||
func snapshot(url string, total int64, etag, lastmod string, segs []seg) control {
|
func snapshot(url string, total int64, etag, lastmod string, segs []seg) control {
|
||||||
c := control{URL: url, Total: total, ETag: etag, LastModified: lastmod, Segs: make([]segState, len(segs))}
|
c := control{URL: url, Total: total, ETag: etag, LastModified: lastmod, Segs: make([]segState, len(segs))}
|
||||||
for i := range segs {
|
for i := range segs {
|
||||||
c.Segs[i] = segState{segs[i].start, segs[i].endOff(), atomic.LoadInt64(&segs[i].written)}
|
c.Segs[i] = segState{segs[i].start, segs[i].end, atomic.LoadInt64(&segs[i].written)}
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -88,16 +87,15 @@ func loadControl(out, url string, total int64, etag, lastmod string) *control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validSegs reports whether segs cover [0,total) with no gap or overlap and a
|
// validSegs reports whether segs cover [0,total) with no gap or overlap and a
|
||||||
// sane written count for each. The recorded order is not sorted (a steal appends
|
// sane written count for each. got always writes segments in ascending start
|
||||||
// a tail), so we check coverage on a sorted copy.
|
// order, so coverage is checked in place; an out-of-order sidecar (only possible
|
||||||
|
// from a hand-edited or corrupted file) is rejected, which restarts cleanly.
|
||||||
func validSegs(segs []segState, total int64) bool {
|
func validSegs(segs []segState, total int64) bool {
|
||||||
if total <= 0 || len(segs) == 0 {
|
if total <= 0 || len(segs) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
sorted := append([]segState(nil), segs...)
|
|
||||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Start < sorted[j].Start })
|
|
||||||
var next int64
|
var next int64
|
||||||
for _, s := range sorted {
|
for _, s := range segs {
|
||||||
length := s.End - s.Start + 1
|
length := s.End - s.Start + 1
|
||||||
if s.Start != next || s.End < s.Start || s.Written < 0 || s.Written > length {
|
if s.Start != next || s.End < s.Start || s.Written < 0 || s.Written > length {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -758,7 +758,7 @@ func (d *Download) fetchSeg(ctx context.Context, f *os.File, s *seg, total int64
|
|||||||
func (d *Download) fetchOnce(ctx context.Context, f *os.File, s *seg, uri string, total int64) error {
|
func (d *Download) fetchOnce(ctx context.Context, f *os.File, s *seg, uri string, total int64) error {
|
||||||
reqCtx, cancel := context.WithCancel(ctx)
|
reqCtx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req, err := d.request(reqCtx, uri, fmt.Sprintf("bytes=%d-%d", s.offset(), s.endOff()))
|
req, err := d.request(reqCtx, uri, fmt.Sprintf("bytes=%d-%d", s.offset(), s.end))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type seg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *seg) length() int64 { return s.end - s.start + 1 }
|
func (s *seg) length() int64 { return s.end - s.start + 1 }
|
||||||
func (s *seg) endOff() int64 { return s.end }
|
|
||||||
func (s *seg) done() bool { return atomic.LoadInt64(&s.written) >= s.length() }
|
func (s *seg) done() bool { return atomic.LoadInt64(&s.written) >= s.length() }
|
||||||
func (s *seg) addWritten(n int64) { atomic.AddInt64(&s.written, n) }
|
func (s *seg) addWritten(n int64) { atomic.AddInt64(&s.written, n) }
|
||||||
func (s *seg) progress() int64 { return atomic.LoadInt64(&s.written) }
|
func (s *seg) progress() int64 { return atomic.LoadInt64(&s.written) }
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func tilesCover(t *testing.T, segs []seg, total int64) {
|
|||||||
if s.start != next {
|
if s.start != next {
|
||||||
t.Fatalf("segment gap/overlap: next byte %d, got start %d", next, s.start)
|
t.Fatalf("segment gap/overlap: next byte %d, got start %d", next, s.start)
|
||||||
}
|
}
|
||||||
next = s.endOff() + 1
|
next = s.end + 1
|
||||||
}
|
}
|
||||||
if next != total {
|
if next != total {
|
||||||
t.Fatalf("segments cover %d bytes, want %d", next, total)
|
t.Fatalf("segments cover %d bytes, want %d", next, total)
|
||||||
|
|||||||
41
main.go
41
main.go
@@ -17,7 +17,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -94,8 +94,12 @@ func run(args []string) int {
|
|||||||
t := time.AfterFunc(time.Duration(stop)*time.Second, cancel)
|
t := time.AfterFunc(time.Duration(stop)*time.Second, cancel)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
}
|
}
|
||||||
jobsRef := &jobsHolder{}
|
var jobsRef atomic.Pointer[[]job]
|
||||||
go handleSignals(cancel, sessionFile, jobsRef)
|
// 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
|
||||||
|
// without racing the `jobs = append(...)` reassignment in the follow pass.
|
||||||
|
publish := func(j []job) { jobsRef.Store(&j) }
|
||||||
|
go handleSignals(cancel, sessionFile, &jobsRef)
|
||||||
|
|
||||||
eng := download.NewEngine(opts.Int("max-concurrent-downloads"))
|
eng := download.NewEngine(opts.Int("max-concurrent-downloads"))
|
||||||
|
|
||||||
@@ -111,7 +115,7 @@ func run(args []string) int {
|
|||||||
close(uiDone)
|
close(uiDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
jobsRef.set(jobs)
|
publish(jobs)
|
||||||
results := runJobs(ctx, eng, jobs)
|
results := runJobs(ctx, eng, jobs)
|
||||||
|
|
||||||
// --follow-torrent: a .torrent fetched over HTTP becomes a BitTorrent
|
// --follow-torrent: a .torrent fetched over HTTP becomes a BitTorrent
|
||||||
@@ -125,7 +129,7 @@ func run(args []string) int {
|
|||||||
// Index space, so the sort below restores input order deterministically.
|
// Index space, so the sort below restores input order deterministically.
|
||||||
nInitial := len(jobs)
|
nInitial := len(jobs)
|
||||||
jobs = append(jobs, follow...)
|
jobs = append(jobs, follow...)
|
||||||
jobsRef.set(jobs)
|
publish(jobs)
|
||||||
followResults := runJobs(ctx, eng, follow)
|
followResults := runJobs(ctx, eng, follow)
|
||||||
for i := range followResults {
|
for i := range followResults {
|
||||||
followResults[i].Index += nInitial
|
followResults[i].Index += nInitial
|
||||||
@@ -153,25 +157,6 @@ func run(args []string) int {
|
|||||||
return report(results)
|
return report(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// jobsHolder lets the signal handler reach the latest job list for a
|
|
||||||
// best-effort session save on a forced (second-Ctrl-C) exit.
|
|
||||||
type jobsHolder struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
jobs []job
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *jobsHolder) set(jobs []job) {
|
|
||||||
h.mu.Lock()
|
|
||||||
h.jobs = jobs
|
|
||||||
h.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *jobsHolder) get() []job {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
return h.jobs
|
|
||||||
}
|
|
||||||
|
|
||||||
// job pairs a download with the source string that produced it, so we can write
|
// job pairs a download with the source string that produced it, so we can write
|
||||||
// the still-unfinished ones back to a session file.
|
// the still-unfinished ones back to a session file.
|
||||||
type job struct {
|
type job struct {
|
||||||
@@ -810,14 +795,18 @@ func progressNote(s download.Stat) string {
|
|||||||
// TERM) cancels the graceful context so run() can wind down and save normally;
|
// TERM) cancels the graceful context so run() can wind down and save normally;
|
||||||
// a second forces exit, but first writes the session best-effort so the resume
|
// a second forces exit, but first writes the session best-effort so the resume
|
||||||
// list survives a force-quit.
|
// list survives a force-quit.
|
||||||
func handleSignals(cancel context.CancelFunc, sessionFile string, jobs *jobsHolder) {
|
func handleSignals(cancel context.CancelFunc, sessionFile string, jobs *atomic.Pointer[[]job]) {
|
||||||
sig := make(chan os.Signal, 2)
|
sig := make(chan os.Signal, 2)
|
||||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||||
<-sig // first: cancel the graceful context
|
<-sig // first: cancel the graceful context
|
||||||
cancel()
|
cancel()
|
||||||
<-sig // second: force exit
|
<-sig // second: force exit
|
||||||
if sessionFile != "" {
|
if sessionFile != "" {
|
||||||
if err := saveSession(sessionFile, jobs.get()); err != nil {
|
var js []job // nil until the first publish; saveSession handles an empty list
|
||||||
|
if p := jobs.Load(); p != nil {
|
||||||
|
js = *p
|
||||||
|
}
|
||||||
|
if err := saveSession(sessionFile, js); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "got:", err)
|
fmt.Fprintln(os.Stderr, "got:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,6 @@ func humanSize(n int64, human bool) string {
|
|||||||
return fmt.Sprintf("%.0f%ciB", val, suffix)
|
return fmt.Sprintf("%.0f%ciB", val, suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// speed formats a bytes-per-second rate. The DL:/UL: fields show the bare
|
|
||||||
// abbreviated size with no "/s" suffix.
|
|
||||||
func speed(bytesPerSec int64, human bool) string {
|
|
||||||
return humanSize(bytesPerSec, human)
|
|
||||||
}
|
|
||||||
|
|
||||||
// secfmt renders a duration as "1h2m3s", appending each unit only when it is
|
// secfmt renders a duration as "1h2m3s", appending each unit only when it is
|
||||||
// nonzero (but still showing seconds when the whole input is 0):
|
// nonzero (but still showing seconds when the whole input is 0):
|
||||||
// 3600s->"1h", 120s->"2m", 3720s->"1h2m". A non-positive or absurd
|
// 3600s->"1h", 120s->"2m", 3720s->"1h2m". A non-positive or absurd
|
||||||
|
|||||||
@@ -53,16 +53,6 @@ func TestSecfmt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSpeedNoSuffix(t *testing.T) {
|
|
||||||
// The DL:/UL: fields show the bare abbreviated size with no "/s".
|
|
||||||
if got := speed(1536, true); got != "1.5KiB" {
|
|
||||||
t.Errorf("speed(1536,true) = %q, want %q", got, "1.5KiB")
|
|
||||||
}
|
|
||||||
if got := speed(2048, false); got != "2048B" {
|
|
||||||
t.Errorf("speed(2048,false) = %q, want %q", got, "2048B")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPercent(t *testing.T) {
|
func TestPercent(t *testing.T) {
|
||||||
if got := percent(50, 100); got != 50 {
|
if got := percent(50, 100); got != 50 {
|
||||||
t.Errorf("percent(50,100) = %d, want 50", got)
|
t.Errorf("percent(50,100) = %d, want 50", got)
|
||||||
|
|||||||
@@ -147,10 +147,10 @@ func (r *Reporter) lineOne(s download.Stat) string {
|
|||||||
fmt.Fprintf(&b, " SD:%d", s.Seeders)
|
fmt.Fprintf(&b, " SD:%d", s.Seeders)
|
||||||
}
|
}
|
||||||
if !seeding {
|
if !seeding {
|
||||||
fmt.Fprintf(&b, " DL:%s", speed(dl, r.human))
|
fmt.Fprintf(&b, " DL:%s", humanSize(dl, r.human))
|
||||||
}
|
}
|
||||||
if seeding || ul > 0 {
|
if seeding || ul > 0 {
|
||||||
fmt.Fprintf(&b, " UL:%s", speed(ul, r.human))
|
fmt.Fprintf(&b, " UL:%s", humanSize(ul, r.human))
|
||||||
}
|
}
|
||||||
if !seeding && s.Total > 0 && dl > 0 {
|
if !seeding && s.Total > 0 && dl > 0 {
|
||||||
fmt.Fprintf(&b, " ETA:%s", secfmt(etaDuration(s.Total-s.Completed, dl)))
|
fmt.Fprintf(&b, " ETA:%s", secfmt(etaDuration(s.Total-s.Completed, dl)))
|
||||||
@@ -227,7 +227,7 @@ func (r *Reporter) summary(stats []download.Stat) string {
|
|||||||
stalled++
|
stalled++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
line := fmt.Sprintf("%d active DL:%s UL:%s", len(stats)-waiting, speed(totDL, r.human), speed(totUL, r.human))
|
line := fmt.Sprintf("%d active DL:%s UL:%s", len(stats)-waiting, humanSize(totDL, r.human), humanSize(totUL, r.human))
|
||||||
if waiting > 0 {
|
if waiting > 0 {
|
||||||
line += fmt.Sprintf(" (%d waiting)", waiting)
|
line += fmt.Sprintf(" (%d waiting)", waiting)
|
||||||
}
|
}
|
||||||
@@ -254,13 +254,13 @@ func (r *Reporter) rowMetric(s download.Stat, dl, ul int64) (metric, tail string
|
|||||||
case download.Waiting:
|
case download.Waiting:
|
||||||
return "queued", ""
|
return "queued", ""
|
||||||
case download.Seeding:
|
case download.Seeding:
|
||||||
return "seeding", "UL:" + speed(ul, r.human)
|
return "seeding", "UL:" + humanSize(ul, r.human)
|
||||||
case download.Complete:
|
case download.Complete:
|
||||||
return "done", ""
|
return "done", ""
|
||||||
case download.Errored:
|
case download.Errored:
|
||||||
return "failed", ""
|
return "failed", ""
|
||||||
default:
|
default:
|
||||||
metric = "DL:" + speed(dl, r.human)
|
metric = "DL:" + humanSize(dl, r.human)
|
||||||
switch {
|
switch {
|
||||||
case dl > 0 && s.Total > 0:
|
case dl > 0 && s.Total > 0:
|
||||||
tail = secfmt(etaDuration(s.Total-s.Completed, dl))
|
tail = secfmt(etaDuration(s.Total-s.Completed, dl))
|
||||||
|
|||||||
Reference in New Issue
Block a user