diff --git a/app/download.go b/app/download.go index 8c99f33..2af364a 100644 --- a/app/download.go +++ b/app/download.go @@ -48,9 +48,8 @@ func startAsyncDownload(s *discordgo.Session, i *discordgo.InteractionCreate, ur // Handle progress and results asynchronously go func() { - // First update the original ephemeral message with "Processing..." _, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ - Content: ptr(fmt.Sprintf("Downloading video: %s", renderProgressBar(0))), + Content: ptr("⏬ downloading: starting..."), }) if err != nil { log.Printf("Error updating interaction: %v", err) @@ -63,35 +62,28 @@ func startAsyncDownload(s *discordgo.Session, i *discordgo.InteractionCreate, ur progressChan = nil continue } + + var content string if prog.Phase == "post-processing" { - phaseEmoji := "⚙️" - content := fmt.Sprintf("%s %s", - phaseEmoji, - prog.Phase) - _, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ - Content: ptr(content), - }) - if err != nil { - log.Printf("Error updating progress: %v", err) - } + content = "⚙️ post-processing" } else { - phaseEmoji := "⏬" - content := fmt.Sprintf("%s %s: %s [eta: %s]\n📄 %s", - phaseEmoji, - prog.Phase, - renderProgressBar(prog.PercentFloat), - prog.ETA, - prog.Filename) - _, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ - Content: ptr(content), - }) - if err != nil { - log.Printf("Error updating progress: %v", err) + var progressStr string + if prog.DownloadedBytes > 0 { + progressStr = formatBytes(prog.DownloadedBytes) + " downloaded" + } else { + progressStr = "starting..." } + content = fmt.Sprintf("Downloading Video: %s", progressStr) + } + + _, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Content: ptr(content), + }) + if err != nil { + log.Printf("Error updating progress: %v", err) } case result := <-resultChan: - // Handle completion if result.Success { _, err = s.FollowupMessageCreate(i.Interaction, false, &discordgo.WebhookParams{ Content: "📥 Video downloaded: " + result.URL, diff --git a/app/misc.go b/app/misc.go index abdaeec..57a61b6 100644 --- a/app/misc.go +++ b/app/misc.go @@ -1,19 +1,21 @@ package main -import ( - "fmt" - "strings" -) +import "fmt" // Helper function to create string pointer func ptr(s string) *string { return &s } -const progressBarLength = 20 - -func renderProgressBar(percent float64) string { - filled := int(float64(progressBarLength) * percent / 100) - bar := strings.Repeat("█", filled) + strings.Repeat(" ", progressBarLength-filled) - return fmt.Sprintf("[%s] %.0f%%", bar, percent) +func formatBytes(b int) string { + switch { + case b >= 1<<30: + return fmt.Sprintf("%.1f GB", float64(b)/float64(1<<30)) + case b >= 1<<20: + return fmt.Sprintf("%.1f MB", float64(b)/float64(1<<20)) + case b >= 1<<10: + return fmt.Sprintf("%.1f KB", float64(b)/float64(1<<10)) + default: + return fmt.Sprintf("%d B", b) + } } diff --git a/app/types.go b/app/types.go index b889915..7b1ce2b 100644 --- a/app/types.go +++ b/app/types.go @@ -1,11 +1,5 @@ package main -import ( - "time" - - "github.com/lrstanley/go-ytdlp" -) - type DownloadOptions struct { EmbedThumbnail bool IncludeSubtitles bool @@ -35,12 +29,9 @@ type FormatOptions struct { } type ProgressUpdate struct { - Status ytdlp.ProgressStatus - Percent string - PercentFloat float64 - ETA time.Duration - Filename string - Phase string + Phase string + DownloadedBytes int + Filename string } // InteractionState holds the state for a specific interaction diff --git a/app/ytdlp.go b/app/ytdlp.go index 9557b1b..9c21fbc 100644 --- a/app/ytdlp.go +++ b/app/ytdlp.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "os" + "path/filepath" + "sync" "time" "github.com/lrstanley/go-ytdlp" @@ -95,42 +97,80 @@ func GetFormats(url string) (*FormatOptions, error) { return formatOpts, nil } +func dirSize(path string) int { + var total int64 + filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if err == nil && !info.IsDir() { + total += info.Size() + } + return nil + }) + return int(total) +} + func DownloadVideo(out_dir, temp_dir, url string, opts DownloadOptions, progressChan chan<- ProgressUpdate) { - defer close(progressChan) + var mu sync.Mutex + currentPhase := "downloading" + currentFilename := "" + + // Poll the temp directory for actual bytes-on-disk progress. + // The yt-dlp progress callback only tracks phase/filename since + // DownloadedBytes from the callback is unreliable for DASH streams. + var wg sync.WaitGroup + wg.Add(1) + stopPoll := make(chan struct{}) + go func() { + defer wg.Done() + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-stopPoll: + return + case <-ticker.C: + mu.Lock() + phase := currentPhase + filename := currentFilename + mu.Unlock() + progressChan <- ProgressUpdate{ + Phase: phase, + DownloadedBytes: dirSize(temp_dir), + Filename: filename, + } + } + } + }() + + defer func() { + close(stopPoll) + wg.Wait() + close(progressChan) + }() homePath := "home:" + out_dir tempPath := "temp:" + temp_dir - var lastPhase string - dl := ytdlp.New(). SetExecutable(ytdlpBinary). Paths(homePath). Paths(tempPath). RecodeVideo("mp4"). ProgressFunc(100*time.Millisecond, func(prog ytdlp.ProgressUpdate) { - // Detect phase transition -- differentiate "downloading" as the main download - // and "post processing" when the file name changes, preventing it from appearing "reset" + if prog.Status == ytdlp.ProgressStatusFinished || + prog.Status == ytdlp.ProgressStatusStarting || + prog.Status == ytdlp.ProgressStatusError { + return + } + phase := "downloading" - if prog.Status == ytdlp.ProgressStatusDownloading && prog.Percent() == 0.0 { - // If we already had progress, it's likely post-processing - if lastPhase == "downloading" { - phase = "post-processing" - } - } else if prog.Status != ytdlp.ProgressStatusDownloading { + if prog.Status == ytdlp.ProgressStatusPostProcessing { phase = "post-processing" } - lastPhase = phase - - progressChan <- ProgressUpdate{ - Status: prog.Status, - Percent: prog.PercentString(), - PercentFloat: prog.Percent(), - ETA: prog.ETA(), - Filename: prog.Filename, - Phase: phase, - } + mu.Lock() + currentPhase = phase + currentFilename = prog.Filename + mu.Unlock() }). Output("%(title)s.%(ext)s")