156 lines
4.0 KiB
Go
156 lines
4.0 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, 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)
|
|
}
|
|
}
|