Skip to content

Commit e47b745

Browse files
committed
feat: Enhance yt-dlp download functionality with progress tracking and error handling
1 parent 45273f8 commit e47b745

3 files changed

Lines changed: 119 additions & 21 deletions

File tree

internal/player/download.go

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package player
22

33
import (
4+
"bufio"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -153,13 +154,81 @@ func DownloadVideo(url, destPath string, numThreads int, m *model) error {
153154
return nil
154155
}
155156

156-
// downloadWithYtDlp downloads a video using yt-dlp.
157-
func downloadWithYtDlp(url, path string) error {
158-
cmd := exec.Command("yt-dlp", "--no-progress", "-f", "best", "-o", path, url)
159-
if output, err := cmd.CombinedOutput(); err != nil {
160-
return fmt.Errorf("yt-dlp error: %v\n%s", err, string(output))
157+
// downloadWithYtDlp downloads a video using yt-dlp and updates the progress model if provided.
158+
func downloadWithYtDlp(url, path string, m *model) error {
159+
// Build yt-dlp command with newline progress and no colors
160+
args := []string{"--newline", "--no-color", "-f", "best", "-o", path, url}
161+
cmd := exec.Command("yt-dlp", args...)
162+
163+
stderr, err := cmd.StderrPipe()
164+
if err != nil {
165+
return fmt.Errorf("yt-dlp stderr pipe: %w", err)
161166
}
162-
return nil
167+
stdout, err := cmd.StdoutPipe()
168+
if err != nil {
169+
return fmt.Errorf("yt-dlp stdout pipe: %w", err)
170+
}
171+
172+
if err := cmd.Start(); err != nil {
173+
// Fallback to original behavior to not break flow
174+
out, e := exec.Command("yt-dlp", "--no-progress", "-f", "best", "-o", path, url).CombinedOutput()
175+
if e != nil {
176+
return fmt.Errorf("yt-dlp error: %v\n%s", e, string(out))
177+
}
178+
return nil
179+
}
180+
181+
// Estimate per-episode total for progress accounting
182+
var epTotal int64
183+
var lastBytes int64
184+
if m != nil {
185+
client := &http.Client{Transport: api.SafeTransport(10 * time.Second)}
186+
if sz, e := getContentLength(url, client); e == nil && sz > 0 {
187+
epTotal = sz
188+
} else {
189+
// Fallback estimate for HLS
190+
epTotal = 500 * 1024 * 1024
191+
}
192+
}
193+
194+
reader := bufio.NewScanner(io.MultiReader(stdout, stderr))
195+
// Increase buffer in case yt-dlp outputs long lines
196+
buf := make([]byte, 0, 1024*64)
197+
reader.Buffer(buf, 1024*1024)
198+
percentRe := regexp.MustCompile(`(?i)(\d{1,3}(?:\.\d+)?)\s*%`)
199+
for reader.Scan() {
200+
line := reader.Text()
201+
// Parse percent and update shared progress
202+
if m != nil && epTotal > 0 {
203+
if pm := percentRe.FindStringSubmatch(line); len(pm) > 1 {
204+
pStr := pm[1]
205+
pVal, _ := strconv.ParseFloat(pStr, 64)
206+
if pVal < 0 {
207+
pVal = 0
208+
}
209+
if pVal > 100 {
210+
pVal = 100
211+
}
212+
current := int64(float64(epTotal) * (pVal / 100.0))
213+
delta := current - lastBytes
214+
if delta > 0 {
215+
m.mu.Lock()
216+
m.received += delta
217+
m.mu.Unlock()
218+
lastBytes = current
219+
}
220+
}
221+
}
222+
}
223+
// Wait for command completion
224+
err = cmd.Wait()
225+
if m != nil && epTotal > 0 && lastBytes < epTotal {
226+
// Ensure completion accounts for full episode size
227+
m.mu.Lock()
228+
m.received += (epTotal - lastBytes)
229+
m.mu.Unlock()
230+
}
231+
return err
163232
}
164233

165234
// ExtractVideoSources returns the available video sources for an episode.
@@ -437,9 +506,9 @@ func HandleBatchDownload(episodes []models.Episode, animeURL string) error {
437506
}
438507
// Use yt-dlp for HLS/DASH playlists and hosters that require it
439508
if strings.Contains(videoURL, ".m3u8") || strings.Contains(videoURL, ".mpd") || strings.Contains(videoURL, "repackager.wixmp.com") {
440-
err = downloadWithYtDlp(videoURL, episodePath)
509+
err = downloadWithYtDlp(videoURL, episodePath, m)
441510
} else if strings.Contains(videoURL, "blogger.com") {
442-
err = downloadWithYtDlp(videoURL, episodePath)
511+
err = downloadWithYtDlp(videoURL, episodePath, m)
443512
} else {
444513
err = DownloadVideo(videoURL, episodePath, 4, m)
445514
}

internal/player/helper.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5252

5353
case statusMsg:
5454
m.status = string(msg)
55-
return m, nil
55+
// Force a small progress refresh when status changes
56+
cmd := m.progress.SetPercent(float64(m.received) / max(1, float64(m.totalBytes)))
57+
return m, tea.Batch(cmd)
5658

5759
case progress.FrameMsg:
5860
var cmd tea.Cmd
@@ -114,3 +116,11 @@ func tickCmd() tea.Cmd {
114116
return tickMsg(t)
115117
})
116118
}
119+
120+
// Provide a small helper for avoiding div by zero
121+
func max(a, b float64) float64 {
122+
if a > b {
123+
return a
124+
}
125+
return b
126+
}

internal/player/player.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package player
22

33
import (
44
"bytes"
5-
"context"
65
"encoding/json"
76
"fmt"
87
"net"
@@ -24,7 +23,6 @@ import (
2423
"github.com/charmbracelet/bubbles/progress"
2524
tea "github.com/charmbracelet/bubbletea"
2625
"github.com/charmbracelet/huh"
27-
"github.com/lrstanley/go-ytdlp"
2826
"github.com/pkg/errors"
2927
)
3028

@@ -363,19 +361,40 @@ func downloadAndPlayEpisode(
363361
strings.Contains(videoURL, ".m3u8") ||
364362
strings.Contains(videoURL, "wixmp.com") ||
365363
strings.Contains(videoURL, "sharepoint.com") {
366-
// Use yt-dlp to download from these sources
367-
fmt.Printf("Downloading episode %s with yt-dlp (detected streaming URL)...\n", episodeNumberStr)
364+
// Use yt-dlp with progress bar
365+
m := &model{
366+
progress: progress.New(progress.WithDefaultGradient()),
367+
keys: keyMap{
368+
quit: key.NewBinding(
369+
key.WithKeys("ctrl+c"),
370+
key.WithHelp("ctrl+c", "quit"),
371+
),
372+
},
373+
}
374+
p := tea.NewProgram(m)
368375

369-
// Ensure yt-dlp is installed
370-
ytdlp.MustInstall(context.Background(), nil)
376+
// Estimate/obtain total size for progress percentage
377+
httpClient := &http.Client{Transport: api.SafeTransport(10 * time.Second)}
378+
if sz, err := getContentLength(videoURL, httpClient); err == nil && sz > 0 {
379+
m.totalBytes = sz
380+
} else {
381+
// Fallback for HLS
382+
m.totalBytes = 500 * 1024 * 1024
383+
}
371384

372-
// Configure downloader
373-
dl := ytdlp.New().
374-
Output(episodePath) // -o <episodePath>
385+
go func() {
386+
p.Send(statusMsg(fmt.Sprintf("Downloading episode %s...", episodeNumberStr)))
387+
if err := downloadWithYtDlp(videoURL, episodePath, m); err != nil {
388+
util.Fatal("Failed to download video:", err)
389+
}
390+
m.mu.Lock()
391+
m.done = true
392+
m.mu.Unlock()
393+
p.Send(statusMsg("Download completed!"))
394+
}()
375395

376-
// Execute download
377-
if _, err := dl.Run(context.Background(), videoURL); err != nil {
378-
return fmt.Errorf("failed to download video using yt-dlp: %w", err)
396+
if _, err := p.Run(); err != nil {
397+
util.Fatal("Error running progress bar:", err)
379398
}
380399

381400
// Verify the file was actually downloaded

0 commit comments

Comments
 (0)