-
Notifications
You must be signed in to change notification settings - Fork 28
/
YoutubeDL.cs
353 lines (335 loc) · 17.7 KB
/
YoutubeDL.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using YoutubeDLSharp.Helpers;
using YoutubeDLSharp.Metadata;
using YoutubeDLSharp.Options;
namespace YoutubeDLSharp
{
/// <summary>
/// A class providing methods for downloading videos using youtube-dl.
/// </summary>
public class YoutubeDL
{
private static Regex rgxFile = new Regex("echo\\s\\\"?(.*)\\\"?", RegexOptions.Compiled);
protected ProcessRunner runner;
/// <summary>
/// Path to the youtube-dl executable.
/// </summary>
public string YoutubeDLPath { get; set; } = "youtube-dl.exe";
/// <summary>
/// Path to the FFmpeg executable.
/// </summary>
public string FFmpegPath { get; set; } = "ffmpeg.exe";
/// <summary>
/// Path of the folder where items will be downloaded to.
/// </summary>
public string OutputFolder { get; set; } = Environment.CurrentDirectory;
/// <summary>
/// Template of the name of the downloaded file on youtube-dl style.
/// See https://github.com/ytdl-org/youtube-dl#output-template.
/// </summary>
public string OutputFileTemplate { get; set; } = "%(title)s.%(ext)s";
/// <summary>
/// If set to true, file names a re restricted to ASCII characters.
/// </summary>
public bool RestrictFilenames { get; set; } = false;
/* TODO IMPORTANT This flag does not work fully as expected:
* Does not guarantee overwrites (see https://github.com/ytdl-org/youtube-dl/pull/20405)
* Always overwrites post-processed files
* (see https://github.com/ytdl-org/youtube-dl/issues/5173, https://github.com/ytdl-org/youtube-dl/issues/333)
*/
public bool OverwriteFiles { get; set; } = true;
/// <summary>
/// If set to true, download errors are ignored and downloading is continued.
/// </summary>
public bool IgnoreDownloadErrors { get; set; } = true;
/// <summary>
/// Gets the product version of the youtube-dl executable file.
/// </summary>
public string Version
=> FileVersionInfo.GetVersionInfo(Utils.GetFullPath(YoutubeDLPath)).FileVersion;
/// <summary>
/// Creates a new instance of the YoutubeDL class.
/// </summary>
/// <param name="maxNumberOfProcesses">The maximum number of concurrent youtube-dl processes.</param>
public YoutubeDL(byte maxNumberOfProcesses = 4)
{
runner = new ProcessRunner(maxNumberOfProcesses);
}
/// <summary>
/// Sets the maximal number of parallel download processes.
/// </summary>
/// <param name="count"></param>
/// <returns></returns>
public async Task SetMaxNumberOfProcesses(byte count) => await runner.SetTotalCount(count);
#region Process methods
/// <summary>
/// Runs youtube-dl with the given option set.
/// </summary>
/// <param name="urls">The video URLs passed to youtube-dl.</param>
/// <param name="options">The OptionSet of youtube-dl options.</param>
/// <param name="ct">A CancellationToken used to cancel the process.</param>
/// <returns>A RunResult object containing the output of youtube-dl as an array of string.</returns>
public async Task<RunResult<string[]>> RunWithOptions(string[] urls, OptionSet options, CancellationToken ct)
{
var output = new List<string>();
var process = new YoutubeDLProcess(YoutubeDLPath);
process.OutputReceived += (o, e) => output.Add(e.Data);
(int code, string[] errors) = await runner.RunThrottled(process, urls, options, ct);
return new RunResult<string[]>(code == 0, errors, output.ToArray());
}
/// <summary>
/// Runs an update of youtube-dl.
/// </summary>
/// <returns>The output of youtube-dl as string.</returns>
public async Task<string> RunUpdate()
{
string output = String.Empty;
var process = new YoutubeDLProcess(YoutubeDLPath);
process.OutputReceived += (o, e) => output = e.Data;
await process.RunAsync(null, new OptionSet() { Update = true });
return output;
}
/// <summary>
/// Runs a fetch of information for the given video without downloading the video.
/// </summary>
/// <param name="url">The URL of the video to fetch information for.</param>
/// <param name="ct">A CancellationToken used to cancel the process.</param>
/// <param name="flat">If set to true, does not extract information for each video in a playlist.</param>
/// <param name="overrideOptions">Override options of the default option set for this run.</param>
/// <returns>A RunResult object containing a VideoData object with the requested video information.</returns>
public async Task<RunResult<VideoData>> RunVideoDataFetch(string url,
CancellationToken ct = default, bool flat = true, OptionSet overrideOptions = null)
{
var opts = GetDownloadOptions();
if (overrideOptions != null)
{
opts = opts.OverrideOptions(overrideOptions);
}
opts.DumpSingleJson = true;
opts.FlatPlaylist = flat;
VideoData videoData = null;
var process = new YoutubeDLProcess(YoutubeDLPath);
process.OutputReceived += (o, e) => videoData = JsonConvert.DeserializeObject<VideoData>(e.Data);
(int code, string[] errors) = await runner.RunThrottled(process, new[] { url }, opts, ct);
return new RunResult<VideoData>(code == 0, errors, videoData);
}
/// <summary>
/// Runs a download of the specified video with an optional conversion afterwards.
/// </summary>
/// <param name="url">The URL of the video to be downloaded.</param>
/// <param name="format">A format selection string in youtube-dl style.</param>
/// <param name="mergeFormat">If a merge is required, the container format of the merged downloads.</param>
/// <param name="recodeFormat">The video format the output will be recoded to after download.</param>
/// <param name="ct">A CancellationToken used to cancel the download.</param>
/// <param name="progress">A progress provider used to get download progress information.</param>
/// <param name="output">A progress provider used to capture the standard output.</param>
/// <param name="overrideOptions">Override options of the default option set for this run.</param>
/// <returns>A RunResult object containing the path to the downloaded and converted video.</returns>
public async Task<RunResult<string>> RunVideoDownload(string url,
string format = "bestvideo+bestaudio/best",
DownloadMergeFormat mergeFormat = DownloadMergeFormat.Unspecified,
VideoRecodeFormat recodeFormat = VideoRecodeFormat.None,
CancellationToken ct = default, IProgress<DownloadProgress> progress = null,
IProgress<string> output = null, OptionSet overrideOptions = null)
{
var opts = GetDownloadOptions();
if (overrideOptions != null)
{
opts = opts.OverrideOptions(overrideOptions);
}
opts.Format = format;
opts.MergeOutputFormat = mergeFormat;
opts.RecodeVideo = recodeFormat;
string outputFile = String.Empty;
var process = new YoutubeDLProcess(YoutubeDLPath);
// Report the used ytdl args
output?.Report($"Arguments: {process.ConvertToArgs(new[] { url }, opts)}\n");
process.OutputReceived += (o, e) =>
{
var match = rgxFile.Match(e.Data);
if (match.Success)
{
outputFile = match.Groups[1].ToString().Trim('"');
progress?.Report(new DownloadProgress(DownloadState.Success, data: outputFile));
}
output?.Report(e.Data);
};
(int code, string[] errors) = await runner.RunThrottled(process, new[] { url }, opts, ct, progress);
return new RunResult<string>(code == 0, errors, outputFile);
}
/// <summary>
/// Runs a download of the specified video playlist with an optional conversion afterwards.
/// </summary>
/// <param name="url">The URL of the playlist to be downloaded.</param>
/// <param name="start">The index of the first playlist video to download (starting at 1).</param>
/// <param name="end">The index of the last playlist video to dowload (if null, download to end).</param>
/// <param name="items">An array of indices of playlist video to download.</param>
/// <param name="format">A format selection string in youtube-dl style.</param>
/// <param name="recodeFormat">The video format the output will be recoded to after download.</param>
/// <param name="ct">A CancellationToken used to cancel the download.</param>
/// <param name="progress">A progress provider used to get download progress information.</param>
/// <param name="output">A progress provider used to capture the standard output.</param>
/// <param name="overrideOptions">Override options of the default option set for this run.</param>
/// <returns>A RunResult object containing the paths to the downloaded and converted videos.</returns>
public async Task<RunResult<string[]>> RunVideoPlaylistDownload(string url,
int? start = 1, int? end = null,
int[] items = null,
string format = "bestvideo+bestaudio/best",
VideoRecodeFormat recodeFormat = VideoRecodeFormat.None,
CancellationToken ct = default, IProgress<DownloadProgress> progress = null,
IProgress<string> output = null, OptionSet overrideOptions = null)
{
var opts = GetDownloadOptions();
if (overrideOptions != null)
{
opts = opts.OverrideOptions(overrideOptions);
}
opts.NoPlaylist = false;
opts.PlaylistStart = start;
opts.PlaylistEnd = end;
if (items != null)
opts.PlaylistItems = String.Join(",", items);
opts.Format = format;
opts.RecodeVideo = recodeFormat;
var outputFiles = new List<string>();
var process = new YoutubeDLProcess(YoutubeDLPath);
// Report the used ytdl args
output?.Report($"Arguments: {process.ConvertToArgs(new[] { url }, opts)}\n");
process.OutputReceived += (o, e) =>
{
var match = rgxFile.Match(e.Data);
if (match.Success)
{
var file = match.Groups[1].ToString().Trim('"');
outputFiles.Add(file);
progress?.Report(new DownloadProgress(DownloadState.Success, data: file));
}
output?.Report(e.Data);
};
(int code, string[] errors) = await runner.RunThrottled(process, new[] { url }, opts, ct, progress);
return new RunResult<string[]>(code == 0, errors, outputFiles.ToArray());
}
/// <summary>
/// Runs a download of the specified video with and converts it to an audio format afterwards.
/// </summary>
/// <param name="url">The URL of the video to be downloaded.</param>
/// <param name="format">The audio format the video will be converted to after downloaded.</param>
/// <param name="ct">A CancellationToken used to cancel the download.</param>
/// <param name="progress">A progress provider used to get download progress information.</param>
/// <param name="output">A progress provider used to capture the standard output.</param>
/// <param name="overrideOptions">Override options of the default option set for this run.</param>
/// <returns>A RunResult object containing the path to the downloaded and converted video.</returns>
public async Task<RunResult<string>> RunAudioDownload(string url, AudioConversionFormat format,
CancellationToken ct = default, IProgress<DownloadProgress> progress = null,
IProgress<string> output = null, OptionSet overrideOptions = null)
{
var opts = GetDownloadOptions();
if (overrideOptions != null)
{
opts = opts.OverrideOptions(overrideOptions);
}
opts.Format = "bestaudio/best";
opts.ExtractAudio = true;
opts.AudioFormat = format;
string outputFile = String.Empty;
var error = new List<string>();
var process = new YoutubeDLProcess(YoutubeDLPath);
// Report the used ytdl args
output?.Report($"Arguments: {process.ConvertToArgs(new[] { url }, opts)}\n");
process.OutputReceived += (o, e) =>
{
var match = rgxFile.Match(e.Data);
if (match.Success)
{
outputFile = match.Groups[1].ToString().Trim('"');
progress?.Report(new DownloadProgress(DownloadState.Success, data: outputFile));
}
output?.Report(e.Data);
};
(int code, string[] errors) = await runner.RunThrottled(process, new[] { url }, opts, ct, progress);
return new RunResult<string>(code == 0, errors, outputFile);
}
/// <summary>
/// Runs a download of the specified video playlist and converts all videos to an audio format afterwards.
/// </summary>
/// <param name="url">The URL of the playlist to be downloaded.</param>
/// <param name="start">The index of the first playlist video to download (starting at 1).</param>
/// <param name="end">The index of the last playlist video to dowload (if null, download to end).</param>
/// <param name="items">An array of indices of playlist video to download.</param>
/// <param name="format">The audio format the videos will be converted to after downloaded.</param>
/// <param name="ct">A CancellationToken used to cancel the download.</param>
/// <param name="progress">A progress provider used to get download progress information.</param>
/// <param name="output">A progress provider used to capture the standard output.</param>
/// <param name="overrideOptions">Override options of the default option set for this run.</param>
/// <returns>A RunResult object containing the paths to the downloaded and converted videos.</returns>
public async Task<RunResult<string[]>> RunAudioPlaylistDownload(string url,
int? start = 1, int? end = null,
int[] items = null, AudioConversionFormat format = AudioConversionFormat.Best,
CancellationToken ct = default, IProgress<DownloadProgress> progress = null,
IProgress<string> output = null, OptionSet overrideOptions = null)
{
var outputFiles = new List<string>();
var opts = GetDownloadOptions();
if (overrideOptions != null)
{
opts = opts.OverrideOptions(overrideOptions);
}
opts.NoPlaylist = false;
opts.PlaylistStart = start;
opts.PlaylistEnd = end;
if (items != null)
opts.PlaylistItems = String.Join(",", items);
opts.Format = "bestaudio/best";
opts.ExtractAudio = true;
opts.AudioFormat = format;
var process = new YoutubeDLProcess(YoutubeDLPath);
// Report the used ytdl args
output?.Report($"Arguments: {process.ConvertToArgs(new[] { url }, opts)}\n");
process.OutputReceived += (o, e) =>
{
var match = rgxFile.Match(e.Data);
if (match.Success)
{
var file = match.Groups[1].ToString().Trim('"');
outputFiles.Add(file);
progress?.Report(new DownloadProgress(DownloadState.Success, data: file));
}
output?.Report(e.Data);
};
(int code, string[] errors) = await runner.RunThrottled(process, new[] { url }, opts, ct, progress);
return new RunResult<string[]>(code == 0, errors, outputFiles.ToArray());
}
/// <summary>
/// Returns an option set with default options used for most downloading operations.
/// </summary>
protected virtual OptionSet GetDownloadOptions()
{
return new OptionSet()
{
IgnoreErrors = this.IgnoreDownloadErrors,
IgnoreConfig = true,
NoPlaylist = true,
HlsPreferNative = true,
ExternalDownloaderArgs = "-nostats -loglevel 0",
Output = Path.Combine(OutputFolder, OutputFileTemplate),
RestrictFilenames = this.RestrictFilenames,
NoContinue = this.OverwriteFiles,
NoOverwrites = !this.OverwriteFiles,
NoPart = true,
FfmpegLocation = Utils.GetFullPath(this.FFmpegPath),
/* TODO This is used to retrieve the final file path.
* Could be replaced by https://github.com/ytdl-org/youtube-dl/pull/22769.
*/
Exec = "echo {}"
};
}
#endregion
}
}