From 66a8cd11282d4be4473200fd0b9cccbf75f937bb Mon Sep 17 00:00:00 2001 From: db123 Date: Thu, 25 Jun 2026 22:45:25 +0330 Subject: [PATCH] fix: resolve back step, audio labels, progress parsing, file size pre-check, and stale job cleanup - Fix handleBackStep to redirect to download prompt when step state is nil (previously silent no-op after container selection cleared the state) - Improve formatLabel for audio-only entries without bitrate: use ID + extension instead of generic 'audio', fixing duplicate entries in format list - Add yt-dlp --progress-template for reliable JSON progress parsing across all sites (fixes 0.0% progress on sites like xnxx.com where [download] lines are absent); keep regex parser as fallback for older yt-dlp versions - Add file size pre-check in handleContainerSelection before download starts (previously checked only after download completed, wasting bandwidth) - Clear stale active jobs and step state on /start for a clean slate --- internal/dl/progress.go | 31 ++++++++++++++- internal/dl/progress_test.go | 73 ++++++++++++++++++++++++++++++++++++ internal/dl/ytdlp.go | 8 +++- internal/dl/ytdlp_test.go | 3 +- internal/handler/download.go | 12 +++++- internal/handler/handler.go | 4 +- 6 files changed, 125 insertions(+), 6 deletions(-) diff --git a/internal/dl/progress.go b/internal/dl/progress.go index 0d9267b..0dc7d03 100644 --- a/internal/dl/progress.go +++ b/internal/dl/progress.go @@ -12,6 +12,7 @@ import ( ) var progressRE = regexp.MustCompile(`\[\w+\]\s+([\d.]+)%\s+of\s+~?([\d.]+)(\w+)`) +var progressTemplateRE = regexp.MustCompile(`^DLPROG\|([\d.]+)\|(.*?)\|(.*)$`) type progressUpdate struct { pct float64 @@ -19,11 +20,39 @@ type progressUpdate struct { 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 == "" || !strings.HasPrefix(line, "[download]") { + if line == "" { + return nil + } + if u := parseProgressTemplate(line); u != nil { + return u + } + if !strings.HasPrefix(line, "[download]") { return nil } m := progressRE.FindStringSubmatch(line) diff --git a/internal/dl/progress_test.go b/internal/dl/progress_test.go index f1355bd..f3b0b2a 100644 --- a/internal/dl/progress_test.go +++ b/internal/dl/progress_test.go @@ -100,6 +100,79 @@ func TestParseProgressLine(t *testing.T) { } } +func TestParseProgressTemplate(t *testing.T) { + tests := []struct { + line string + pct float64 + speed string + eta string + hasRes bool + }{ + { + line: "DLPROG|45.2|2.34MiB/s|00:45", + pct: 45.2, + speed: "2.34MiB/s", + eta: "00:45", + hasRes: true, + }, + { + line: "DLPROG|100.0|5.00MiB/s|00:00", + pct: 100.0, + speed: "5.00MiB/s", + eta: "00:00", + hasRes: true, + }, + { + line: "DLPROG|0.0||", + pct: 0.0, + speed: "", + eta: "", + hasRes: true, + }, + { + line: "not a progress line", + hasRes: false, + }, + { + line: "", + hasRes: false, + }, + { + line: "[download] 45.2% of 123.45MiB at 2.34MiB/s ETA 00:45", + hasRes: false, + }, + { + line: "DLPROG|50.0|Unknown|Unknown", + pct: 50.0, + speed: "", + eta: "", + hasRes: true, + }, + } + for _, tt := range tests { + got := parseProgressTemplate(tt.line) + if !tt.hasRes { + if got != nil { + t.Errorf("parseProgressTemplate(%q) = %+v, want nil", tt.line, got) + } + continue + } + if got == nil { + t.Errorf("parseProgressTemplate(%q) = nil, want result", tt.line) + continue + } + if math.Abs(got.pct-tt.pct) > 0.01 { + t.Errorf("parseProgressTemplate(%q).pct = %f, want %f", tt.line, got.pct, tt.pct) + } + if got.speed != tt.speed { + t.Errorf("parseProgressTemplate(%q).speed = %q, want %q", tt.line, got.speed, tt.speed) + } + if got.eta != tt.eta { + t.Errorf("parseProgressTemplate(%q).eta = %q, want %q", tt.line, got.eta, tt.eta) + } + } +} + func TestParseProgressLineEdgeCases(t *testing.T) { tests := []struct { line string diff --git a/internal/dl/ytdlp.go b/internal/dl/ytdlp.go index 0e84b18..ee309d1 100644 --- a/internal/dl/ytdlp.go +++ b/internal/dl/ytdlp.go @@ -246,8 +246,9 @@ func (c *Client) StartDownload(job *domain.DownloadJob, downloadDir string) (<-c outputPath := filepath.Join(downloadDir, job.ID+"_%(id)s.%(ext)s") args = append(args, "--output", outputPath) - // Progress reporting (--newline for line-based, no --no-progress so we get actual output) + // Progress reporting using progress-template for machine-readable output args = append(args, "--newline") + args = append(args, "--progress-template", "stderr:DLPROG|%(progress.percent)s|%(progress._speed_str)s|%(progress._eta_str)s") // Language selection if specified if job.Language != "" { @@ -435,7 +436,10 @@ func formatLabel(f domain.Format) string { if f.Bitrate != "" { return f.Bitrate } - return "audio" + if f.Extension != "" { + return f.ID + " (" + f.Extension + ")" + } + return f.ID } if f.Resolution != "" { return f.Resolution diff --git a/internal/dl/ytdlp_test.go b/internal/dl/ytdlp_test.go index 92f721d..ae3ad6d 100644 --- a/internal/dl/ytdlp_test.go +++ b/internal/dl/ytdlp_test.go @@ -89,7 +89,8 @@ func TestFormatLabel(t *testing.T) { want string }{ {domain.Format{IsAudioOnly: true, Bitrate: "128k"}, "128k"}, - {domain.Format{IsAudioOnly: true}, "audio"}, + {domain.Format{ID: "140", Extension: "m4a", IsAudioOnly: true}, "140 (m4a)"}, + {domain.Format{ID: "251", IsAudioOnly: true}, "251"}, {domain.Format{Resolution: "1920x1080", Height: 1080}, "1920x1080"}, {domain.Format{Height: 720}, "720p"}, {domain.Format{ID: "137"}, "137"}, diff --git a/internal/handler/download.go b/internal/handler/download.go index cbb6ce1..af68c2a 100644 --- a/internal/handler/download.go +++ b/internal/handler/download.go @@ -346,6 +346,14 @@ func (h *Handler) handleContainerSelection(ctx context.Context, chatID int64, ms format = ss.MainFormat } + fileSize := format.Filesize + + if fileSize > 0 && fileSize > h.maxFileSize { + h.editText(ctx, chatID, msgID, + fmt.Sprintf("File too large (%s). Max: %s", formatBytes(fileSize), formatBytes(h.maxFileSize)), nil) + return + } + job := &domain.DownloadJob{ ID: h.generateJobID(), UserID: userID, @@ -359,9 +367,10 @@ func (h *Handler) handleContainerSelection(ctx context.Context, chatID int64, ms Language: ss.Language, Status: domain.StatusDownloading, Title: ss.MediaInfo.Title, - FileSize: format.Filesize, + FileSize: fileSize, } + h.editText(ctx, chatID, msgID, "Processing...", nil) h.setActiveJob(userID, job) h.clearStepState(chatID) @@ -526,6 +535,7 @@ func (h *Handler) handleBackStep(ctx context.Context, chatID int64, msgID int, c defer h.answerCb(ctx, cbID) ss := h.getUserStepState(chatID) if ss == nil { + h.handleDownloadPrompt(ctx, chatID, msgID, cbID, userID) return } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 4e8b0df..a279411 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -442,9 +442,11 @@ func (h *Handler) generateJobID() string { return uuid.NewString()[:8] } -// handleStart shows the main menu. +// handleStart shows the main menu and cleans up any stale state. func (h *Handler) handleStart(ctx context.Context, msg *models.Message, userID int64) { slog.Info("start", "user_id", userID, "chat_id", msg.Chat.ID) + h.clearActiveJob(userID) + h.clearStepState(msg.Chat.ID) h.sendWithKB(ctx, msg.Chat.ID, "Main Menu:", mainKeyboard()) }