package dl import ( "bufio" "io" "regexp" "strconv" "strings" "uptodownBot/internal/domain" ) var progressRE = regexp.MustCompile(`\[\w+\]\s+([\d.]+)%\s+of\s+~?([\d.]+)(\w+)`) type progressUpdate struct { pct float64 speed string eta string } // parseProgressLine parses a single yt-dlp stderr progress line. // Returns nil if the line doesn't contain progress info. func parseProgressLine(line string) *progressUpdate { line = strings.TrimSpace(line) if line == "" || !strings.HasPrefix(line, "[download]") { return nil } m := progressRE.FindStringSubmatch(line) if m == nil { return nil } pct, err := strconv.ParseFloat(m[1], 64) if err != nil { return nil } u := &progressUpdate{pct: pct} rest := line if idx := strings.Index(rest, "%"); idx >= 0 { rest = rest[idx+1:] } if atIdx := strings.Index(rest, " at "); atIdx >= 0 { afterAt := rest[atIdx+4:] if spaceIdx := strings.Index(afterAt, " "); spaceIdx >= 0 { u.speed = strings.TrimSpace(afterAt[:spaceIdx]) rest = afterAt[spaceIdx:] } } if etaIdx := strings.Index(rest, "ETA "); etaIdx >= 0 { etaStr := strings.TrimSpace(rest[etaIdx+4:]) u.eta = etaStr } return u } // readProgress reads yt-dlp stderr and sends progress updates to the channel. // The caller is responsible for closing the updates channel. func readProgress(stderr io.ReadCloser, updates chan<- *domain.DownloadJob, job *domain.DownloadJob) { var stderrBuf strings.Builder scanner := bufio.NewScanner(stderr) for scanner.Scan() { line := scanner.Text() if stderrBuf.Len() < 4096 { stderrBuf.WriteString(line) stderrBuf.WriteByte('\n') } parsed := parseProgressLine(line) if parsed == nil { continue } job.Progress = parsed.pct job.Speed = parsed.speed job.ETA = parsed.eta updates <- job } job.StderrLog = stderrBuf.String() }