package grabui import ( "context" "fmt" "os" "sync" "time" "github.com/cavaliercoder/grab" ) type ConsoleClient struct { mu sync.Mutex client *grab.Client succeeded, failed, inProgress int responses []*grab.Response } func NewConsoleClient(client *grab.Client) *ConsoleClient { return &ConsoleClient{ client: client, } } func (c *ConsoleClient) Do( ctx context.Context, workers int, reqs ...*grab.Request, ) <-chan *grab.Response { // buffer size prevents slow receivers causing back pressure pump := make(chan *grab.Response, len(reqs)) go func() { c.mu.Lock() defer c.mu.Unlock() c.failed = 0 c.inProgress = 0 c.succeeded = 0 c.responses = make([]*grab.Response, 0, len(reqs)) if c.client == nil { c.client = grab.DefaultClient } fmt.Printf("Downloading %d files...\n", len(reqs)) respch := c.client.DoBatch(workers, reqs...) t := time.NewTicker(200 * time.Millisecond) defer t.Stop() Loop: for { select { case <-ctx.Done(): break Loop case resp := <-respch: if resp != nil { // a new response has been received and has started downloading c.responses = append(c.responses, resp) pump <- resp // send to caller } else { // channel is closed - all downloads are complete break Loop } case <-t.C: // update UI on clock tick c.refresh() } } c.refresh() close(pump) fmt.Printf( "Finished %d successful, %d failed, %d incomplete.\n", c.succeeded, c.failed, c.inProgress) }() return pump } // refresh prints the progress of all downloads to the terminal func (c *ConsoleClient) refresh() { // clear lines for incomplete downloads if c.inProgress > 0 { fmt.Printf("\033[%dA\033[K", c.inProgress) } // print newly completed downloads for i, resp := range c.responses { if resp != nil && resp.IsComplete() { if resp.Err() != nil { c.failed++ fmt.Fprintf(os.Stderr, "Error downloading %s: %v\n", resp.Request.URL(), resp.Err()) } else { c.succeeded++ fmt.Printf("Finished %s %s / %s (%d%%)\n", resp.Filename, byteString(resp.BytesComplete()), byteString(resp.Size()), int(100*resp.Progress())) } c.responses[i] = nil } } // print progress for incomplete downloads c.inProgress = 0 for _, resp := range c.responses { if resp != nil { fmt.Printf("Downloading %s %s / %s (%d%%) - %s ETA: %s \033[K\n", resp.Filename, byteString(resp.BytesComplete()), byteString(resp.Size()), int(100*resp.Progress()), bpsString(resp.BytesPerSecond()), etaString(resp.ETA())) c.inProgress++ } } } func bpsString(n float64) string { if n < 1e3 { return fmt.Sprintf("%.02fBps", n) } if n < 1e6 { return fmt.Sprintf("%.02fKB/s", n/1e3) } if n < 1e9 { return fmt.Sprintf("%.02fMB/s", n/1e6) } return fmt.Sprintf("%.02fGB/s", n/1e9) } func byteString(n int64) string { if n < 1<<10 { return fmt.Sprintf("%dB", n) } if n < 1<<20 { return fmt.Sprintf("%dKB", n>>10) } if n < 1<<30 { return fmt.Sprintf("%dMB", n>>20) } if n < 1<<40 { return fmt.Sprintf("%dGB", n>>30) } return fmt.Sprintf("%dTB", n>>40) } func etaString(eta time.Time) string { d := eta.Sub(time.Now()) if d < time.Second { return "<1s" } // truncate to 1s resolution d /= time.Second d *= time.Second return d.String() }