From 64beefb3bfe8b4e0f0ec977d884c09df94c7f58f Mon Sep 17 00:00:00 2001 From: dreamjz <25699818+dreamjz@users.noreply.github.com> Date: Wed, 13 Mar 2024 01:31:02 +0800 Subject: [PATCH] Feat: display download speed and remaining time (#32) * chore(util): format size * feat(dl): add download speed and time * feat(dl): fix zero division * chore: fix linting issue --- go.mod | 2 +- internal/downloader/downloader.go | 9 ++-- internal/tui/progressbar/model.go | 19 ++++++++- internal/tui/progressbar/update.go | 4 ++ internal/tui/progressbar/view.go | 9 ++-- pkg/util/math.go | 25 +++++++++++ pkg/util/math_test.go | 68 ++++++++++++++++++++++++++++++ 7 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 pkg/util/math.go create mode 100644 pkg/util/math_test.go diff --git a/go.mod b/go.mod index 1be1a67..6a148f8 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/log v0.3.1 github.com/grafov/m3u8 v0.12.0 github.com/lucasb-eyer/go-colorful v1.2.0 + github.com/magiconair/properties v1.8.7 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.0 golang.org/x/net v0.19.0 @@ -23,7 +24,6 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect diff --git a/internal/downloader/downloader.go b/internal/downloader/downloader.go index c9b950c..820ff08 100644 --- a/internal/downloader/downloader.go +++ b/internal/downloader/downloader.go @@ -402,12 +402,15 @@ func countProgressBar(p *tea.Program, total int64, fileName string) *progressbar func progressBar(p *tea.Program, total int64, fileName string) *progressbar.ProgressBar { pw := &progressbar.ProgressWriter{ - FileName: fileName, - Total: total, - OnProgress: func(fileName string, ratio float64) { + FileName: fileName, + Total: total, + StartTime: time.Now(), + OnProgress: func(fileName string, ratio, dltime float64, speed int64) { p.Send(progressbar.ProgressMsg{ FileName: fileName, Ratio: ratio, + DLTime: dltime, + Speed: speed, }) }, } diff --git a/internal/tui/progressbar/model.go b/internal/tui/progressbar/model.go index b15ba91..201ad43 100644 --- a/internal/tui/progressbar/model.go +++ b/internal/tui/progressbar/model.go @@ -5,6 +5,7 @@ import ( "os" "sync" "sync/atomic" + "time" "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" @@ -40,7 +41,10 @@ type ProgressWriter struct { File *os.File Reader io.Reader FileName string - OnProgress func(string, float64) + StartTime time.Time + Speed int64 + DLTime float64 + OnProgress func(string, float64, float64, int64) } func (pw *ProgressWriter) Start(p *tea.Program) (int64, error) { @@ -60,7 +64,18 @@ func (pw *ProgressWriter) Start(p *tea.Program) (int64, error) { func (pw *ProgressWriter) Write(p []byte) (int, error) { pw.Downloaded += int64(len(p)) if pw.Total > 0 && pw.OnProgress != nil { - pw.OnProgress(pw.FileName, float64(pw.Downloaded)/float64(pw.Total)) + t := int64(time.Since(pw.StartTime).Seconds()) + var speed int64 + if t > 0 { + speed = pw.Downloaded / t + } + + var dltime float64 + if speed > 0 { + dltime = float64((pw.Total - pw.Downloaded) / speed) + } + + pw.OnProgress(pw.FileName, float64(pw.Downloaded)/float64(pw.Total), dltime, speed) } return len(p), nil } diff --git a/internal/tui/progressbar/update.go b/internal/tui/progressbar/update.go index 00ed3ac..0e9e935 100644 --- a/internal/tui/progressbar/update.go +++ b/internal/tui/progressbar/update.go @@ -13,6 +13,8 @@ const ( type ProgressMsg struct { FileName string Ratio float64 + Speed int64 + DLTime float64 } type ProgressCompleteMsg struct{} @@ -53,6 +55,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:cyclop fileName, ratio := msg.FileName, msg.Ratio if pb, ok := m.Pbs[fileName]; ok { cmds = append(cmds, pb.Progress.SetPercent(ratio)) + pb.Pw.Speed = msg.Speed + pb.Pw.DLTime = msg.DLTime } return m, tea.Batch(cmds...) diff --git a/internal/tui/progressbar/view.go b/internal/tui/progressbar/view.go index e2c95bd..d355415 100644 --- a/internal/tui/progressbar/view.go +++ b/internal/tui/progressbar/view.go @@ -4,7 +4,9 @@ import ( "fmt" "sort" "strings" + "time" + "github.com/acgtools/hanime-hunter/pkg/util" "github.com/charmbracelet/lipgloss" ) @@ -59,7 +61,7 @@ func (m *Model) View() string { if pb.Pc != nil { stats = fmt.Sprintf("%d/%d", pb.Pc.Downloaded.Load(), pb.Pc.Total) } else { - stats = getDLStatus(pb.Pw.Downloaded, pb.Pw.Total) + stats = getDLStatus(pb.Pw) } status := renderPbStatus(pb.Status) @@ -110,6 +112,7 @@ func pbMapToSortedSlice(m map[string]*ProgressBar, w int) []*ProgressBar { return res } -func getDLStatus(downloaded, total int64) string { - return fmt.Sprintf("%.2f MiB/%.2f MiB", float64(downloaded)/mib, float64(total)/mib) +func getDLStatus(pw *ProgressWriter) string { + d := time.Duration(pw.DLTime) * time.Second + return fmt.Sprintf("%.2f MiB/%.2f MiB %s %s", float64(pw.Downloaded)/mib, float64(pw.Total)/mib, util.FormatSize(pw.Speed), d.String()) } diff --git a/pkg/util/math.go b/pkg/util/math.go new file mode 100644 index 0000000..a4f4c50 --- /dev/null +++ b/pkg/util/math.go @@ -0,0 +1,25 @@ +package util + +import "fmt" + +// FormatSize formats the file size and ensure the number of size always > 1. +// the unit of parameter `size` is Byte. +func FormatSize(size int64) string { + units := [4]string{"B", "KiB", "MiB", "GiB"} + + i := 0 + for v := size; ; i++ { + if v>>10 < 1 || i > 3 { + break + } + v >>= 10 + } + if i > 3 { //nolint:gomnd + i = 3 + } + + tmp := 1 << (i * 10) //nolint:gomnd + num := float64(size) / float64(tmp) + + return fmt.Sprintf("%.2f %s/s", num, units[i]) +} diff --git a/pkg/util/math_test.go b/pkg/util/math_test.go new file mode 100644 index 0000000..2df88f2 --- /dev/null +++ b/pkg/util/math_test.go @@ -0,0 +1,68 @@ +package util_test + +import ( + "testing" + + "github.com/acgtools/hanime-hunter/pkg/util" + "github.com/magiconair/properties/assert" +) + +func TestFormatSize(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + size int64 + res string + }{ + { + name: "0", + size: 0, + res: "0.00 B/s", + }, + { + name: "512", + size: 512, + res: "512.00 B/s", + }, + { + name: "1024", + size: 1024, + res: "1.00 KiB/s", + }, + { + name: "1048576", + size: 1048576, + res: "1.00 MiB/s", + }, + { + name: "1073741824", + size: 1073741824, + res: "1.00 GiB/s", + }, + { + name: "1023", + size: 1023, + res: "1023.00 B/s", + }, + { + name: "1025", + size: 1025, + res: "1.00 KiB/s", + }, + { + name: "1,099,511,627,776", // 1TiB + size: 1099511627776, + res: "1024.00 GiB/s", + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + s := util.FormatSize(tc.size) + + assert.Equal(t, s, tc.res) + }) + } +}