Files
ggjuku/internal/app/download_progress.go
T
2026-09-30 10:19:54 +08:00

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)
}
}