package main import ( "context" "encoding/json" "time" "github.com/lrstanley/go-ytdlp" ) type DownloadOptions struct { EmbedThumbnail bool IncludeSubtitles bool VideoFormatID string AudioFormatID string } type VideoOption struct { Height *int `json:"height,omitempty"` Resolution string `json:"resolution,omitempty"` FormatID string `json:"format_id"` Ext string `json:"ext"` TBR *float64 `json:"tbr,omitempty"` } type AudioOption struct { Format string `json:"format"` FormatID string `json:"format_id"` Ext string `json:"ext"` TBR *float64 `json:"tbr,omitempty"` Language *string `json:"language,omitempty"` } type FormatOptions struct { VideoOptions []VideoOption `json:"video_options"` AudioOptions []AudioOption `json:"audio_options"` } type ProgressUpdate struct { Status ytdlp.ProgressStatus Percent string ETA time.Duration Filename string Phase string } func GetFormats(url string) (*FormatOptions, error) { dl := ytdlp.New(). 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 downloadVideo(out_dir, url string, opts DownloadOptions, progressChan chan<- ProgressUpdate) { defer close(progressChan) var lastPhase string dl := ytdlp.New(). SetWorkDir(out_dir). 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" 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 { phase = "post-processing" } lastPhase = phase progressChan <- ProgressUpdate{ Status: prog.Status, Percent: prog.PercentString(), ETA: prog.ETA(), Filename: prog.Filename, Phase: phase, } }). 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) } }