178 lines
4.4 KiB
Go
178 lines
4.4 KiB
Go
package app
|
|
|
|
import (
|
|
"math"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type cappedStringWriter struct {
|
|
b strings.Builder
|
|
limit int
|
|
}
|
|
|
|
func (w *cappedStringWriter) Write(p []byte) (int, error) {
|
|
if w.limit <= 0 {
|
|
return len(p), nil
|
|
}
|
|
remaining := w.limit - w.b.Len()
|
|
if remaining > 0 {
|
|
if len(p) > remaining {
|
|
_, _ = w.b.Write(p[:remaining])
|
|
} else {
|
|
_, _ = w.b.Write(p)
|
|
}
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (w *cappedStringWriter) String() string { return w.b.String() }
|
|
|
|
type downloadProgressState struct {
|
|
mu sync.Mutex
|
|
callbackMu sync.Mutex
|
|
callback func(DownloadProgress)
|
|
partPath string
|
|
outPath string
|
|
totalBytes int64
|
|
mediaTotal time.Duration
|
|
mediaDone time.Duration
|
|
started time.Time
|
|
lastSample time.Time
|
|
lastBytes int64
|
|
byteProgress int64
|
|
hasByteProgress bool
|
|
speed float64
|
|
lastReport time.Time
|
|
lastPercent int
|
|
lastPhase string
|
|
}
|
|
|
|
func newDownloadProgressState(partPath, outPath string, totalBytes int64, callback func(DownloadProgress)) *downloadProgressState {
|
|
return &downloadProgressState{partPath: partPath, outPath: outPath, totalBytes: totalBytes, callback: callback, started: time.Now()}
|
|
}
|
|
|
|
func (p *downloadProgressState) setMediaTotal(total time.Duration) {
|
|
p.mu.Lock()
|
|
p.mediaTotal = total
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *downloadProgressState) setTotalBytes(total int64) {
|
|
if total <= 0 {
|
|
return
|
|
}
|
|
p.mu.Lock()
|
|
p.totalBytes = total
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *downloadProgressState) setByteProgress(downloaded, total int64) {
|
|
if downloaded < 0 {
|
|
downloaded = 0
|
|
}
|
|
p.mu.Lock()
|
|
p.byteProgress = downloaded
|
|
p.hasByteProgress = true
|
|
if total > 0 {
|
|
p.totalBytes = total
|
|
}
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *downloadProgressState) setMediaElapsed(elapsed time.Duration) {
|
|
if elapsed < 0 {
|
|
elapsed = 0
|
|
}
|
|
p.mu.Lock()
|
|
if elapsed > p.mediaDone {
|
|
p.mediaDone = elapsed
|
|
}
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *downloadProgressState) snapshot(now time.Time, phase string) DownloadProgress {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
bytesDownloaded := int64(0)
|
|
if p.hasByteProgress {
|
|
bytesDownloaded = p.byteProgress
|
|
} else if st, err := os.Stat(p.partPath); err == nil && !st.IsDir() {
|
|
bytesDownloaded = st.Size()
|
|
} else if phase == "completed" && p.outPath != "" {
|
|
if st, err := os.Stat(p.outPath); err == nil && !st.IsDir() {
|
|
bytesDownloaded = st.Size()
|
|
}
|
|
}
|
|
if !p.lastSample.IsZero() {
|
|
seconds := now.Sub(p.lastSample).Seconds()
|
|
if seconds > 0 && bytesDownloaded >= p.lastBytes {
|
|
p.speed = float64(bytesDownloaded-p.lastBytes) / seconds
|
|
}
|
|
}
|
|
p.lastSample = now
|
|
p.lastBytes = bytesDownloaded
|
|
if p.speed < 0 || math.IsNaN(p.speed) || math.IsInf(p.speed, 0) {
|
|
p.speed = 0
|
|
}
|
|
elapsed := now.Sub(p.started)
|
|
if elapsed < 0 {
|
|
elapsed = 0
|
|
}
|
|
mediaElapsed := p.mediaDone
|
|
mediaTotal := p.mediaTotal
|
|
percent := 0
|
|
if mediaTotal > 0 {
|
|
percent = int(float64(mediaElapsed) / float64(mediaTotal) * 100)
|
|
} else if p.totalBytes > 0 {
|
|
percent = int(float64(bytesDownloaded) / float64(p.totalBytes) * 100)
|
|
}
|
|
if phase == "completed" {
|
|
percent = 100
|
|
if mediaTotal > 0 {
|
|
mediaElapsed = mediaTotal
|
|
}
|
|
}
|
|
if percent < 0 {
|
|
percent = 0
|
|
}
|
|
if phase != "completed" && percent >= 100 {
|
|
percent = 99
|
|
} else if percent > 100 {
|
|
percent = 100
|
|
}
|
|
remaining := time.Duration(0)
|
|
if mediaTotal > 0 && mediaElapsed > 0 {
|
|
remaining = time.Duration(float64(mediaTotal-mediaElapsed) * float64(elapsed) / float64(mediaElapsed))
|
|
} else if p.totalBytes > 0 && p.speed > 0 && bytesDownloaded < p.totalBytes {
|
|
remaining = time.Duration(float64(p.totalBytes-bytesDownloaded) / p.speed * float64(time.Second))
|
|
}
|
|
if remaining < 0 || math.IsNaN(float64(remaining)) || math.IsInf(float64(remaining), 0) {
|
|
remaining = 0
|
|
}
|
|
return DownloadProgress{Percent: percent, DownloadedBytes: bytesDownloaded, TotalBytes: p.totalBytes, SpeedBytesPerSecond: p.speed, Elapsed: elapsed, Remaining: remaining, MediaElapsed: mediaElapsed, MediaTotal: mediaTotal, Phase: phase}
|
|
}
|
|
|
|
func (p *downloadProgressState) report(phase string, force bool) {
|
|
if p.callback == nil {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
p.callbackMu.Lock()
|
|
defer p.callbackMu.Unlock()
|
|
progress := p.snapshot(now, phase)
|
|
p.mu.Lock()
|
|
changed := force || progress.Percent != p.lastPercent || progress.Phase != p.lastPhase || now.Sub(p.lastReport) >= time.Second
|
|
if changed {
|
|
p.lastPercent = progress.Percent
|
|
p.lastPhase = progress.Phase
|
|
p.lastReport = now
|
|
}
|
|
p.mu.Unlock()
|
|
if changed {
|
|
p.callback(progress)
|
|
}
|
|
}
|