package dl import ( "bufio" "io" "log/slog" "regexp" "strconv" "strings" "uptodownBot/internal/domain" ) var progressRE = regexp.MustCompile(`\[\w+\]\s+([\d.]+)%\s+of\s+~?([\d.]+)(\w+)`) var progressTemplateRE = regexp.MustCompile(`^DLPROG\|\s*([\d.]+)%\|\s*(.*?)\s*\|(.*)$`) type progressUpdate struct { pct float64 speed string eta string } // parseProgressTemplate parses a DLPROG-prefixed line from yt-dlp's --progress-template. func parseProgressTemplate(line string) *progressUpdate { m := progressTemplateRE.FindStringSubmatch(line) if m == nil { return nil } pct, err := strconv.ParseFloat(m[1], 64) if err != nil { return nil } u := &progressUpdate{pct: pct} speed := strings.TrimSpace(m[2]) eta := strings.TrimSpace(m[3]) if speed != "" && !strings.EqualFold(speed, "unknown") && speed != "N/A" { u.speed = speed } if eta != "" && !strings.EqualFold(eta, "unknown") && eta != "N/A" { u.eta = eta } return u } // 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 == "" { return nil } if u := parseProgressTemplate(line); u != nil { return u } if !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) lineCount := 0 matchCount := 0 for scanner.Scan() { line := scanner.Text() lineCount++ if stderrBuf.Len() < 4096 { stderrBuf.WriteString(line) stderrBuf.WriteByte('\n') } parsed := parseProgressLine(line) if parsed == nil { continue } matchCount++ job.Progress = parsed.pct job.Speed = parsed.speed job.ETA = parsed.eta updates <- job } job.StderrLog = stderrBuf.String() slog.Info("readProgress finished", "url", job.URL, "lines", lineCount, "matches", matchCount, "stderr_len", len(job.StderrLog)) }