Files
yt-dlp-bot/app/ytdlp.go

206 lines
4.9 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"sync"
"time"
"github.com/lrstanley/go-ytdlp"
)
var ytdlpBinary = os.Getenv("YTDLP_BIN")
func GetFormats(url string) (*FormatOptions, error) {
dl := ytdlp.New().
SetExecutable(ytdlpBinary).
SkipDownload().
DumpJSON()
result, err := dl.Run(context.TODO(), url)
if err != nil {
return nil, err
}
// Parse the JSON output
var info struct {
Formats []struct {
VCodec *string `json:"vcodec"`
ACodec *string `json:"acodec"`
NeedsTesting *bool `json:"__needs_testing"`
Height *int `json:"height"`
Resolution string `json:"resolution"`
FormatID string `json:"format_id"`
Format string `json:"format"`
Ext string `json:"ext"`
TBR *float64 `json:"tbr"`
Language *string `json:"language"`
LanguagePref *int `json:"language_preference"`
URL *string `json:"url"`
Protocol *string `json:"protocol"`
} `json:"formats"`
}
if err := json.Unmarshal([]byte(result.Stdout), &info); err != nil {
return nil, err
}
formatOpts := &FormatOptions{
VideoOptions: []VideoOption{},
AudioOptions: []AudioOption{},
}
for _, fmt := range info.Formats {
// Skip formats that need testing
if fmt.NeedsTesting != nil && *fmt.NeedsTesting {
continue
}
// Skip SABR formats (https://github.com/yt-dlp/yt-dlp/issues/12482)
if fmt.URL == nil || *fmt.URL == "" {
continue
}
// Video-only: has video codec but no audio codec
if fmt.VCodec != nil && *fmt.VCodec != "none" &&
fmt.ACodec != nil && *fmt.ACodec == "none" {
formatOpts.VideoOptions = append(formatOpts.VideoOptions, VideoOption{
Height: fmt.Height,
Resolution: fmt.Resolution,
FormatID: fmt.FormatID,
Ext: fmt.Ext,
TBR: fmt.TBR,
})
}
// Audio-only: has audio codec but no video codec
if fmt.ACodec != nil && *fmt.ACodec != "none" &&
fmt.VCodec != nil && *fmt.VCodec == "none" {
audioOpt := AudioOption{
Format: fmt.Format,
FormatID: fmt.FormatID,
Ext: fmt.Ext,
TBR: fmt.TBR,
}
// Use language if available, otherwise use language_preference
if fmt.Language != nil {
audioOpt.Language = fmt.Language
}
formatOpts.AudioOptions = append(formatOpts.AudioOptions, audioOpt)
}
}
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) {
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
dl := ytdlp.New().
SetExecutable(ytdlpBinary).
Paths(homePath).
Paths(tempPath).
RecodeVideo("mp4").
ProgressFunc(100*time.Millisecond, func(prog ytdlp.ProgressUpdate) {
if prog.Status == ytdlp.ProgressStatusFinished ||
prog.Status == ytdlp.ProgressStatusStarting ||
prog.Status == ytdlp.ProgressStatusError {
return
}
phase := "downloading"
if prog.Status == ytdlp.ProgressStatusPostProcessing {
phase = "post-processing"
}
mu.Lock()
currentPhase = phase
currentFilename = prog.Filename
mu.Unlock()
}).
Output("%(title)s.%(ext)s")
// Set format if both video and audio format IDs are provided
if opts.VideoFormatID != "" && opts.AudioFormatID != "" {
dl = dl.Format(opts.VideoFormatID + "+" + opts.AudioFormatID)
} else if opts.VideoFormatID != "" {
dl = dl.Format(opts.VideoFormatID)
} else if opts.AudioFormatID != "" {
dl = dl.Format(opts.AudioFormatID)
} else {
// Default format selection if none specified
dl = dl.FormatSort("res,ext:mp4:m4a")
}
if opts.EmbedThumbnail {
dl = dl.EmbedThumbnail()
}
if opts.IncludeSubtitles {
dl = dl.CompatOptions("no-keep-subs")
dl = dl.EmbedSubs()
dl = dl.SubLangs("en,en*")
dl = dl.WriteAutoSubs()
dl = dl.WriteSubs()
}
_, err := dl.Run(context.TODO(), url)
if err != nil {
panic(err)
}
}