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

160 lines
4.1 KiB
Go

package main
import (
"context"
"encoding/json"
"time"
"github.com/lrstanley/go-ytdlp"
)
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, temp_dir, url string, opts DownloadOptions, progressChan chan<- ProgressUpdate) {
defer close(progressChan)
homePath := "home:" + out_dir
tempPath := "temp:" + temp_dir
var lastPhase string
dl := ytdlp.New().
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"
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)
}
}