Skip to content

Commit 4a5fe40

Browse files
authored
Merge pull request #1260 from Zibbp/ffmpeg-hls
update yt-dlp to prefer ffmpeg for vod downloads
2 parents 90a05a1 + 33a4ef9 commit 4a5fe40

2 files changed

Lines changed: 105 additions & 17 deletions

File tree

internal/exec/exec.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,48 @@ func appendFFmpegLiveOutputStreamArgs(args []string, audioOnly bool) []string {
5858
)
5959
}
6060

61+
func appendYtDlpVideoConfigArgs(args []string, configArgs string) []string {
62+
skipValue := false
63+
for _, arg := range strings.Split(configArgs, ",") {
64+
trimmedArg := strings.TrimSpace(arg)
65+
if skipValue {
66+
skipValue = false
67+
continue
68+
}
69+
if trimmedArg == "-o" || trimmedArg == "--output" || trimmedArg == "-P" || trimmedArg == "--paths" {
70+
skipValue = true
71+
continue
72+
}
73+
if strings.HasPrefix(trimmedArg, "--output=") || strings.HasPrefix(trimmedArg, "--paths=") ||
74+
(len(trimmedArg) > 2 && (strings.HasPrefix(trimmedArg, "-o") || strings.HasPrefix(trimmedArg, "-P"))) {
75+
continue
76+
}
77+
if trimmedArg != "" {
78+
args = append(args, trimmedArg)
79+
}
80+
}
81+
return args
82+
}
83+
84+
func twitchVideoDownloadArgs(quality, url, outputPath, configArgs string) []string {
85+
args := []string{
86+
"-f", quality,
87+
url,
88+
"-o", outputPath,
89+
"--merge-output-format", "mp4", "--no-part",
90+
"--no-warnings", "--progress", "--newline", "--no-check-certificate",
91+
// Twitch VOD playlists can change their fMP4 initialization segment after a
92+
// discontinuity. The native HLS downloader rejects a second EXT-X-MAP, while
93+
// ffmpeg handles the new initialization section correctly.
94+
"--hls-prefer-ffmpeg",
95+
}
96+
97+
// User arguments are intentionally last so an explicit downloader preference
98+
// in the configuration can override the default. Output options are excluded
99+
// so the application-owned temporary path remains authoritative.
100+
return appendYtDlpVideoConfigArgs(args, configArgs)
101+
}
102+
61103
// DownloadTwitchVideo downloads a Twitch video.
62104
func DownloadTwitchVideo(ctx context.Context, video ent.Vod) error {
63105
// Get video channel
@@ -115,26 +157,13 @@ func DownloadTwitchVideo(ctx context.Context, video ent.Vod) error {
115157
tmpVideoDownloadExt := filepath.Ext(video.TmpVideoDownloadPath)
116158
tmpVideoDownloadPathNoExt := strings.TrimSuffix(video.TmpVideoDownloadPath, tmpVideoDownloadExt)
117159

118-
// Get user arguments from config
119-
configYtDlpArgs := config.Get().Parameters.YtDlpVideo
120-
configYtDlpArgsArr := strings.Split(configYtDlpArgs, ",")
121-
122-
var cmdArgs []string
123-
cmdArgs = append(cmdArgs,
124-
"-f", qualityString,
160+
cmdArgs := twitchVideoDownloadArgs(
161+
qualityString,
125162
url,
126-
"-o", fmt.Sprintf("%s.%%(ext)s", tmpVideoDownloadPathNoExt),
127-
"--merge-output-format", "mp4", "--no-part",
128-
"--no-warnings", "--progress", "--newline", "--no-check-certificate",
163+
fmt.Sprintf("%s.%%(ext)s", tmpVideoDownloadPathNoExt),
164+
config.Get().Parameters.YtDlpVideo,
129165
)
130166

131-
// Sanitize config args before appending
132-
for _, arg := range configYtDlpArgsArr {
133-
if strings.TrimSpace(arg) != "" {
134-
cmdArgs = append(cmdArgs, arg)
135-
}
136-
}
137-
138167
// Create yt-dlp command
139168
cmd, cookieFile, err := ytdlpSvc.CreateCommand(ctx, cmdArgs, true)
140169
defer func() {

internal/exec/exec_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,65 @@ func TestVodArchiveProcessAttributes(t *testing.T) {
6262
}
6363
}
6464

65+
func TestTwitchVideoDownloadArgsPreferFFmpegForHLS(t *testing.T) {
66+
t.Parallel()
67+
68+
args := twitchVideoDownloadArgs(
69+
"best[height=1080]/best",
70+
"https://twitch.tv/videos/2838897713",
71+
"/tmp/video.%(ext)s",
72+
"--fragment-retries,20",
73+
)
74+
75+
ffmpegPreference := -1
76+
customArgs := -1
77+
for i, arg := range args {
78+
switch arg {
79+
case "--hls-prefer-ffmpeg":
80+
ffmpegPreference = i
81+
case "--fragment-retries":
82+
customArgs = i
83+
}
84+
}
85+
86+
if ffmpegPreference == -1 {
87+
t.Fatalf("Twitch VOD arguments do not prefer the ffmpeg HLS downloader: %v", args)
88+
}
89+
if customArgs == -1 || customArgs < ffmpegPreference {
90+
t.Fatalf("configured yt-dlp arguments must follow defaults: %v", args)
91+
}
92+
}
93+
94+
func TestAppendYtDlpVideoConfigArgsExcludesOutputOptions(t *testing.T) {
95+
t.Parallel()
96+
97+
initial := []string{"-o", "/data/temp/video.%(ext)s"}
98+
configArgs := strings.Join([]string{
99+
" --retries ", " 10 ", " ",
100+
"-o", "/tmp/short-separated",
101+
"--output", "/tmp/long-separated",
102+
"-o=/tmp/short-equals",
103+
"-o/tmp/short-attached",
104+
"--output=/tmp/long-equals",
105+
"-P", "/tmp/path-short-separated",
106+
"--paths", "/tmp/path-long-separated",
107+
"-P=/tmp/path-short-equals",
108+
"-P/tmp/path-short-attached",
109+
"--paths=/tmp/path-long-equals",
110+
" --fragment-retries", "20 ",
111+
}, ",")
112+
113+
got := appendYtDlpVideoConfigArgs(initial, configArgs)
114+
want := []string{
115+
"-o", "/data/temp/video.%(ext)s",
116+
"--retries", "10",
117+
"--fragment-retries", "20",
118+
}
119+
if !reflect.DeepEqual(got, want) {
120+
t.Fatalf("appendYtDlpVideoConfigArgs() = %v, want %v", got, want)
121+
}
122+
}
123+
65124
func TestPostProcessVideoFFmpegArgsIncludesTitleMetadata(t *testing.T) {
66125
t.Parallel()
67126

0 commit comments

Comments
 (0)