]> jfr.im git - yt-dlp.git/blame - yt_dlp/downloader/external.py
[cleanup] Add more ruff rules (#10149)
[yt-dlp.git] / yt_dlp / downloader / external.py
CommitLineData
c487cf00 1import enum
8c53322c 2import json
1ceb657b 3import os
f0298f65 4import re
222516d9 5import subprocess
12b84ac8 6import sys
1ceb657b 7import tempfile
f0298f65 8import time
8c53322c 9import uuid
5219cb3e 10
1009f67c 11from .fragment import FragmentFD
14f25df2 12from ..compat import functools
3d2623a8 13from ..networking import Request
f8271158 14from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor
222516d9 15from ..utils import (
f8271158 16 Popen,
be5c1ae8 17 RetryManager,
f8271158 18 _configuration_args,
19 check_executable,
28787f16 20 classproperty,
f8271158 21 cli_bool_option,
1195a38f
S
22 cli_option,
23 cli_valueless_option,
af6793f8 24 determine_ext,
74f8654a 25 encodeArgument,
f8271158 26 encodeFilename,
8c53322c 27 find_available_port,
af6793f8 28 remove_end,
0a5a191a 29 traverse_obj,
222516d9
PH
30)
31
32
c487cf00 33class Features(enum.Enum):
34 TO_STDOUT = enum.auto()
35 MULTIPLE_FORMATS = enum.auto()
36
37
1009f67c 38class ExternalFD(FragmentFD):
5219cb3e 39 SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps')
c487cf00 40 SUPPORTED_FEATURES = ()
f0c9fb96 41 _CAPTURE_STDERR = True
5219cb3e 42
222516d9
PH
43 def real_download(self, filename, info_dict):
44 self.report_destination(filename)
45 tmpfilename = self.temp_name(filename)
1ceb657b 46 self._cookies_tempfile = None
222516d9 47
e7db6759 48 try:
f0298f65 49 started = time.time()
e7db6759
S
50 retval = self._call_downloader(tmpfilename, info_dict)
51 except KeyboardInterrupt:
52 if not info_dict.get('is_live'):
53 raise
54 # Live stream downloading cancellation should be considered as
55 # correct and expected termination thus all postprocessing
56 # should take place
57 retval = 0
add96eb9 58 self.to_screen(f'[{self.get_basename()}] Interrupted by user')
1ceb657b 59 finally:
60 if self._cookies_tempfile:
61 self.try_remove(self._cookies_tempfile)
e7db6759 62
222516d9 63 if retval == 0:
f0298f65
S
64 status = {
65 'filename': filename,
66 'status': 'finished',
67 'elapsed': time.time() - started,
68 }
69 if filename != '-':
80aa2460 70 fsize = os.path.getsize(encodeFilename(tmpfilename))
80aa2460 71 self.try_rename(tmpfilename, filename)
f0298f65 72 status.update({
80aa2460
JH
73 'downloaded_bytes': fsize,
74 'total_bytes': fsize,
80aa2460 75 })
3ba7740d 76 self._hook_progress(status, info_dict)
222516d9
PH
77 return True
78 else:
79 self.to_stderr('\n')
80 self.report_error('%s exited with code %d' % (
81 self.get_basename(), retval))
82 return False
83
84 @classmethod
85 def get_basename(cls):
86 return cls.__name__[:-2].lower()
87
28787f16 88 @classproperty
89 def EXE_NAME(cls):
90 return cls.get_basename()
91
2762dbb1 92 @functools.cached_property
222516d9 93 def exe(self):
28787f16 94 return self.EXE_NAME
222516d9 95
99cbe98c 96 @classmethod
7f7de7f9 97 def available(cls, path=None):
28787f16 98 path = check_executable(
99 cls.EXE_NAME if path in (None, cls.get_basename()) else path,
100 [cls.AVAILABLE_OPT])
101 if not path:
102 return False
103 cls.exe = path
104 return path
99cbe98c 105
222516d9
PH
106 @classmethod
107 def supports(cls, info_dict):
c487cf00 108 return all((
109 not info_dict.get('to_stdout') or Features.TO_STDOUT in cls.SUPPORTED_FEATURES,
110 '+' not in info_dict['protocol'] or Features.MULTIPLE_FORMATS in cls.SUPPORTED_FEATURES,
7e68567e 111 not traverse_obj(info_dict, ('hls_aes', ...), 'extra_param_to_segment_url'),
c487cf00 112 all(proto in cls.SUPPORTED_PROTOCOLS for proto in info_dict['protocol'].split('+')),
113 ))
222516d9 114
2cb99ebb 115 @classmethod
7f7de7f9 116 def can_download(cls, info_dict, path=None):
117 return cls.available(path) and cls.supports(info_dict)
2cb99ebb 118
bf812ef7 119 def _option(self, command_option, param):
1195a38f 120 return cli_option(self.params, command_option, param)
bf812ef7 121
266b0ad6 122 def _bool_option(self, command_option, param, true_value='true', false_value='false', separator=None):
1195a38f 123 return cli_bool_option(self.params, command_option, param, true_value, false_value, separator)
266b0ad6 124
dc534b67 125 def _valueless_option(self, command_option, param, expected_value=True):
1195a38f 126 return cli_valueless_option(self.params, command_option, param, expected_value)
f30c2e8e 127
330690a2 128 def _configuration_args(self, keys=None, *args, **kwargs):
129 return _configuration_args(
28787f16 130 self.get_basename(), self.params.get('external_downloader_args'), self.EXE_NAME,
330690a2 131 keys, *args, **kwargs)
c75f0b36 132
1ceb657b 133 def _write_cookies(self):
134 if not self.ydl.cookiejar.filename:
135 tmp_cookies = tempfile.NamedTemporaryFile(suffix='.cookies', delete=False)
136 tmp_cookies.close()
137 self._cookies_tempfile = tmp_cookies.name
138 self.to_screen(f'[download] Writing temporary cookies file to "{self._cookies_tempfile}"')
139 # real_download resets _cookies_tempfile; if it's None then save() will write to cookiejar.filename
62b5c94c 140 self.ydl.cookiejar.save(self._cookies_tempfile)
1ceb657b 141 return self.ydl.cookiejar.filename or self._cookies_tempfile
142
222516d9
PH
143 def _call_downloader(self, tmpfilename, info_dict):
144 """ Either overwrite this or implement _make_cmd """
74f8654a 145 cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
222516d9 146
74f8654a 147 self._debug_cmd(cmd)
222516d9 148
fc5c8b64 149 if 'fragments' not in info_dict:
8c53322c 150 _, stderr, returncode = self._call_process(cmd, info_dict)
f0c9fb96 151 if returncode and stderr:
152 self.to_stderr(stderr)
153 return returncode
fc5c8b64 154
fc5c8b64 155 skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
156
be5c1ae8 157 retry_manager = RetryManager(self.params.get('fragment_retries'), self.report_retry,
158 frag_index=None, fatal=not skip_unavailable_fragments)
159 for retry in retry_manager:
8c53322c 160 _, stderr, returncode = self._call_process(cmd, info_dict)
f0c9fb96 161 if not returncode:
fc5c8b64 162 break
163 # TODO: Decide whether to retry based on error code
164 # https://aria2.github.io/manual/en/html/aria2c.html#exit-status
f0c9fb96 165 if stderr:
166 self.to_stderr(stderr)
be5c1ae8 167 retry.error = Exception()
168 continue
169 if not skip_unavailable_fragments and retry_manager.error:
170 return -1
fc5c8b64 171
172 decrypt_fragment = self.decrypter(info_dict)
205a0654 173 dest, _ = self.sanitize_open(tmpfilename, 'wb')
fc5c8b64 174 for frag_index, fragment in enumerate(info_dict['fragments']):
add96eb9 175 fragment_filename = f'{tmpfilename}-Frag{frag_index}'
fc5c8b64 176 try:
205a0654 177 src, _ = self.sanitize_open(fragment_filename, 'rb')
86e5f3ed 178 except OSError as err:
fc5c8b64 179 if skip_unavailable_fragments and frag_index > 1:
b4b855eb 180 self.report_skip_fragment(frag_index, err)
fc5c8b64 181 continue
b4b855eb 182 self.report_error(f'Unable to open fragment {frag_index}; {err}')
fc5c8b64 183 return -1
184 dest.write(decrypt_fragment(fragment, src.read()))
185 src.close()
186 if not self.params.get('keep_fragments', False):
45806d44 187 self.try_remove(encodeFilename(fragment_filename))
fc5c8b64 188 dest.close()
add96eb9 189 self.try_remove(encodeFilename(f'{tmpfilename}.frag.urls'))
fc5c8b64 190 return 0
222516d9 191
8c53322c 192 def _call_process(self, cmd, info_dict):
66aeaac9 193 return Popen.run(cmd, text=True, stderr=subprocess.PIPE if self._CAPTURE_STDERR else None)
8c53322c 194
222516d9 195
384b6202 196class CurlFD(ExternalFD):
91ee320b 197 AVAILABLE_OPT = '-V'
f0c9fb96 198 _CAPTURE_STDERR = False # curl writes the progress to stderr
99cbe98c 199
384b6202 200 def _make_cmd(self, tmpfilename, info_dict):
af14914b 201 cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed']
42ded0a4 202 cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
203 if cookie_header:
204 cmd += ['--cookie', cookie_header]
002ea8fe 205 if info_dict.get('http_headers') is not None:
206 for key, val in info_dict['http_headers'].items():
86e5f3ed 207 cmd += ['--header', f'{key}: {val}']
002ea8fe 208
98e698f1
RA
209 cmd += self._bool_option('--continue-at', 'continuedl', '-', '0')
210 cmd += self._valueless_option('--silent', 'noprogress')
211 cmd += self._valueless_option('--verbose', 'verbose')
212 cmd += self._option('--limit-rate', 'ratelimit')
37b239b3
S
213 retry = self._option('--retry', 'retries')
214 if len(retry) == 2:
215 if retry[1] in ('inf', 'infinite'):
216 retry[1] = '2147483647'
217 cmd += retry
98e698f1 218 cmd += self._option('--max-filesize', 'max_filesize')
9f3da138 219 cmd += self._option('--interface', 'source_address')
e7a8c303 220 cmd += self._option('--proxy', 'proxy')
dc534b67 221 cmd += self._valueless_option('--insecure', 'nocheckcertificate')
c75f0b36 222 cmd += self._configuration_args()
384b6202
PH
223 cmd += ['--', info_dict['url']]
224 return cmd
225
226
e0ac5214 227class AxelFD(ExternalFD):
91ee320b 228 AVAILABLE_OPT = '-V'
99cbe98c 229
e0ac5214 230 def _make_cmd(self, tmpfilename, info_dict):
231 cmd = [self.exe, '-o', tmpfilename]
002ea8fe 232 if info_dict.get('http_headers') is not None:
233 for key, val in info_dict['http_headers'].items():
86e5f3ed 234 cmd += ['-H', f'{key}: {val}']
1ceb657b 235 cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
236 if cookie_header:
42ded0a4 237 cmd += ['-H', f'Cookie: {cookie_header}', '--max-redirect=0']
e0ac5214 238 cmd += self._configuration_args()
239 cmd += ['--', info_dict['url']]
240 return cmd
241
242
222516d9 243class WgetFD(ExternalFD):
91ee320b 244 AVAILABLE_OPT = '--version'
99cbe98c 245
222516d9 246 def _make_cmd(self, tmpfilename, info_dict):
1ceb657b 247 cmd = [self.exe, '-O', tmpfilename, '-nv', '--compression=auto']
248 if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
249 cmd += ['--load-cookies', self._write_cookies()]
002ea8fe 250 if info_dict.get('http_headers') is not None:
251 for key, val in info_dict['http_headers'].items():
86e5f3ed 252 cmd += ['--header', f'{key}: {val}']
8c80603f
S
253 cmd += self._option('--limit-rate', 'ratelimit')
254 retry = self._option('--tries', 'retries')
255 if len(retry) == 2:
256 if retry[1] in ('inf', 'infinite'):
257 retry[1] = '0'
258 cmd += retry
9f3da138 259 cmd += self._option('--bind-address', 'source_address')
8a23db95 260 proxy = self.params.get('proxy')
261 if proxy:
262 for var in ('http_proxy', 'https_proxy'):
86e5f3ed 263 cmd += ['--execute', f'{var}={proxy}']
dc534b67 264 cmd += self._valueless_option('--no-check-certificate', 'nocheckcertificate')
c75f0b36 265 cmd += self._configuration_args()
222516d9
PH
266 cmd += ['--', info_dict['url']]
267 return cmd
268
269
384b6202 270class Aria2cFD(ExternalFD):
91ee320b 271 AVAILABLE_OPT = '-v'
52a8a1e1 272 SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'dash_frag_urls', 'm3u8_frag_urls')
99cbe98c 273
0a473f2f 274 @staticmethod
275 def supports_manifest(manifest):
276 UNSUPPORTED_FEATURES = [
277 r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [1]
278 # 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
279 ]
280 check_results = (not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES)
281 return all(check_results)
282
af7a5eef 283 @staticmethod
284 def _aria2c_filename(fn):
285 return fn if os.path.isabs(fn) else f'.{os.path.sep}{fn}'
286
8c53322c 287 def _call_downloader(self, tmpfilename, info_dict):
ad68b16a 288 # FIXME: Disabled due to https://github.com/yt-dlp/yt-dlp/issues/5931
289 if False and 'no-external-downloader-progress' not in self.params.get('compat_opts', []):
8c53322c
L
290 info_dict['__rpc'] = {
291 'port': find_available_port() or 19190,
292 'secret': str(uuid.uuid4()),
293 }
294 return super()._call_downloader(tmpfilename, info_dict)
295
384b6202 296 def _make_cmd(self, tmpfilename, info_dict):
8a8af356 297 cmd = [self.exe, '-c', '--no-conf',
2b3bf01c 298 '--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
dcd55f76 299 '--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16']
2b3bf01c 300 if 'fragments' in info_dict:
301 cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true']
ff0f78e1 302 else:
303 cmd += ['--min-split-size', '1M']
2b3bf01c 304
1ceb657b 305 if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
306 cmd += [f'--load-cookies={self._write_cookies()}']
002ea8fe 307 if info_dict.get('http_headers') is not None:
308 for key, val in info_dict['http_headers'].items():
86e5f3ed 309 cmd += ['--header', f'{key}: {val}']
691d5823 310 cmd += self._option('--max-overall-download-limit', 'ratelimit')
9f3da138 311 cmd += self._option('--interface', 'source_address')
bf812ef7 312 cmd += self._option('--all-proxy', 'proxy')
266b0ad6 313 cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
71f47617 314 cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
f44afb54 315 cmd += self._bool_option('--show-console-readout', 'noprogress', 'false', 'true', '=')
2b3bf01c 316 cmd += self._configuration_args()
317
8c53322c
L
318 if '__rpc' in info_dict:
319 cmd += [
320 '--enable-rpc',
321 f'--rpc-listen-port={info_dict["__rpc"]["port"]}',
322 f'--rpc-secret={info_dict["__rpc"]["secret"]}']
323
eb55bad5 324 # aria2c strips out spaces from the beginning/end of filenames and paths.
325 # We work around this issue by adding a "./" to the beginning of the
326 # filename and relative path, and adding a "/" at the end of the path.
327 # See: https://github.com/yt-dlp/yt-dlp/issues/276
328 # https://github.com/ytdl-org/youtube-dl/issues/20312
329 # https://github.com/aria2/aria2/issues/1373
2b3bf01c 330 dn = os.path.dirname(tmpfilename)
331 if dn:
af7a5eef 332 cmd += ['--dir', self._aria2c_filename(dn) + os.path.sep]
2b3bf01c 333 if 'fragments' not in info_dict:
af7a5eef 334 cmd += ['--out', self._aria2c_filename(os.path.basename(tmpfilename))]
5219cb3e 335 cmd += ['--auto-file-renaming=false']
2b3bf01c 336
d7009caa 337 if 'fragments' in info_dict:
21b25281 338 cmd += ['--uri-selector=inorder']
add96eb9 339 url_list_file = f'{tmpfilename}.frag.urls'
5219cb3e 340 url_list = []
fe845284 341 for frag_index, fragment in enumerate(info_dict['fragments']):
add96eb9 342 fragment_filename = f'{os.path.basename(tmpfilename)}-Frag{frag_index}'
343 url_list.append('{}\n\tout={}'.format(fragment['url'], self._aria2c_filename(fragment_filename)))
205a0654 344 stream, _ = self.sanitize_open(url_list_file, 'wb')
0f06bcd7 345 stream.write('\n'.join(url_list).encode())
539d158c 346 stream.close()
af7a5eef 347 cmd += ['-i', self._aria2c_filename(url_list_file)]
5219cb3e 348 else:
349 cmd += ['--', info_dict['url']]
384b6202
PH
350 return cmd
351
8c53322c
L
352 def aria2c_rpc(self, rpc_port, rpc_secret, method, params=()):
353 # Does not actually need to be UUID, just unique
354 sanitycheck = str(uuid.uuid4())
355 d = json.dumps({
356 'jsonrpc': '2.0',
357 'id': sanitycheck,
358 'method': method,
359 'params': [f'token:{rpc_secret}', *params],
add96eb9 360 }).encode()
3d2623a8 361 request = Request(
8c53322c
L
362 f'http://localhost:{rpc_port}/jsonrpc',
363 data=d, headers={
364 'Content-Type': 'application/json',
365 'Content-Length': f'{len(d)}',
3d2623a8 366 }, proxies={'all': None})
8c53322c
L
367 with self.ydl.urlopen(request) as r:
368 resp = json.load(r)
369 assert resp.get('id') == sanitycheck, 'Something went wrong with RPC server'
370 return resp['result']
371
372 def _call_process(self, cmd, info_dict):
373 if '__rpc' not in info_dict:
374 return super()._call_process(cmd, info_dict)
375
376 send_rpc = functools.partial(self.aria2c_rpc, info_dict['__rpc']['port'], info_dict['__rpc']['secret'])
377 started = time.time()
378
379 fragmented = 'fragments' in info_dict
380 frag_count = len(info_dict['fragments']) if fragmented else 1
381 status = {
382 'filename': info_dict.get('_filename'),
383 'status': 'downloading',
384 'elapsed': 0,
385 'downloaded_bytes': 0,
386 'fragment_count': frag_count if fragmented else None,
387 'fragment_index': 0 if fragmented else None,
388 }
389 self._hook_progress(status, info_dict)
390
391 def get_stat(key, *obj, average=False):
392 val = tuple(filter(None, map(float, traverse_obj(obj, (..., ..., key))))) or [0]
393 return sum(val) / (len(val) if average else 1)
394
395 with Popen(cmd, text=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) as p:
396 # Add a small sleep so that RPC client can receive response,
397 # or the connection stalls infinitely
398 time.sleep(0.2)
399 retval = p.poll()
400 while retval is None:
401 # We don't use tellStatus as we won't know the GID without reading stdout
402 # Ref: https://aria2.github.io/manual/en/html/aria2c.html#aria2.tellActive
403 active = send_rpc('aria2.tellActive')
404 completed = send_rpc('aria2.tellStopped', [0, frag_count])
405
406 downloaded = get_stat('totalLength', completed) + get_stat('completedLength', active)
407 speed = get_stat('downloadSpeed', active)
408 total = frag_count * get_stat('totalLength', active, completed, average=True)
409 if total < downloaded:
410 total = None
411
412 status.update({
413 'downloaded_bytes': int(downloaded),
414 'speed': speed,
415 'total_bytes': None if fragmented else total,
416 'total_bytes_estimate': total,
417 'eta': (total - downloaded) / (speed or 1),
418 'fragment_index': min(frag_count, len(completed) + 1) if fragmented else None,
add96eb9 419 'elapsed': time.time() - started,
8c53322c
L
420 })
421 self._hook_progress(status, info_dict)
422
423 if not active and len(completed) >= frag_count:
424 send_rpc('aria2.shutdown')
425 retval = p.wait()
426 break
427
428 time.sleep(0.1)
429 retval = p.poll()
430
431 return '', p.stderr.read(), retval
432
906e2f0e
JMF
433
434class HttpieFD(ExternalFD):
52a8a1e1 435 AVAILABLE_OPT = '--version'
28787f16 436 EXE_NAME = 'http'
99cbe98c 437
906e2f0e
JMF
438 def _make_cmd(self, tmpfilename, info_dict):
439 cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
002ea8fe 440
441 if info_dict.get('http_headers') is not None:
442 for key, val in info_dict['http_headers'].items():
86e5f3ed 443 cmd += [f'{key}:{val}']
1ceb657b 444
445 # httpie 3.1.0+ removes the Cookie header on redirect, so this should be safe for now. [1]
446 # If we ever need cookie handling for redirects, we can export the cookiejar into a session. [2]
447 # 1: https://github.com/httpie/httpie/security/advisories/GHSA-9w4w-cpc8-h2fq
448 # 2: https://httpie.io/docs/cli/sessions
449 cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
450 if cookie_header:
451 cmd += [f'Cookie:{cookie_header}']
906e2f0e
JMF
452 return cmd
453
12b84ac8 454
455class FFmpegFD(ExternalFD):
6251555f 456 SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'm3u8_native', 'rtsp', 'rtmp', 'rtmp_ffmpeg', 'mms', 'http_dash_segments')
c487cf00 457 SUPPORTED_FEATURES = (Features.TO_STDOUT, Features.MULTIPLE_FORMATS)
12b84ac8 458
99cbe98c 459 @classmethod
52a8a1e1 460 def available(cls, path=None):
461 # TODO: Fix path for ffmpeg
dbf5416a 462 # Fixme: This may be wrong when --ffmpeg-location is used
99cbe98c 463 return FFmpegPostProcessor().available
464
e36d50c5 465 def on_process_started(self, proc, stdin):
466 """ Override this in subclasses """
467 pass
468
dbf5416a 469 @classmethod
d5fe04f5 470 def can_merge_formats(cls, info_dict, params):
dbf5416a 471 return (
472 info_dict.get('requested_formats')
473 and info_dict.get('protocol')
474 and not params.get('allow_unplayable_formats')
475 and 'no-direct-merge' not in params.get('compat_opts', [])
476 and cls.can_download(info_dict))
477
12b84ac8 478 def _call_downloader(self, tmpfilename, info_dict):
12b84ac8 479 ffpp = FFmpegPostProcessor(downloader=self)
77dea16a 480 if not ffpp.available:
e3b771a8 481 self.report_error('m3u8 download detected but ffmpeg could not be found. Please install')
77dea16a 482 return False
12b84ac8 483 ffpp.check_version()
484
485 args = [ffpp.executable, '-y']
486
a609e61a
S
487 for log_level in ('quiet', 'verbose'):
488 if self.params.get(log_level, False):
489 args += ['-loglevel', log_level]
490 break
2ec1759f 491 if not self.params.get('verbose'):
492 args += ['-hide_banner']
a609e61a 493
9c42b7ee 494 args += traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args', ...))
bb36a55c 495
0a5a191a 496 # These exists only for compatibility. Extractors should use
497 # info_dict['downloader_options']['ffmpeg_args'] instead
1d485a1a 498 args += info_dict.get('_ffmpeg_args') or []
36fce548
RA
499 seekable = info_dict.get('_seekable')
500 if seekable is not None:
501 # setting -seekable prevents ffmpeg from guessing if the server
502 # supports seeking(by adding the header `Range: bytes=0-`), which
503 # can cause problems in some cases
067aa17e 504 # https://github.com/ytdl-org/youtube-dl/issues/11800#issuecomment-275037127
36fce548
RA
505 # http://trac.ffmpeg.org/ticket/6125#comment:10
506 args += ['-seekable', '1' if seekable else '0']
507
e62d9c5c
S
508 env = None
509 proxy = self.params.get('proxy')
510 if proxy:
511 if not re.match(r'^[\da-zA-Z]+://', proxy):
add96eb9 512 proxy = f'http://{proxy}'
20bad91d
YCH
513
514 if proxy.startswith('socks'):
515 self.report_warning(
add96eb9 516 f'{self.get_basename()} does not support SOCKS proxies. Downloading is likely to fail. '
517 'Consider adding --hls-prefer-native to your command.')
20bad91d 518
e62d9c5c
S
519 # Since December 2015 ffmpeg supports -http_proxy option (see
520 # http://git.videolan.org/?p=ffmpeg.git;a=commit;h=b4eb1f29ebddd60c41a2eb39f5af701e38e0d3fd)
521 # We could switch to the following code if we are able to detect version properly
522 # args += ['-http_proxy', proxy]
523 env = os.environ.copy()
ac668111 524 env['HTTP_PROXY'] = proxy
525 env['http_proxy'] = proxy
e62d9c5c 526
4230c489 527 protocol = info_dict.get('protocol')
528
529 if protocol == 'rtmp':
530 player_url = info_dict.get('player_url')
531 page_url = info_dict.get('page_url')
532 app = info_dict.get('app')
533 play_path = info_dict.get('play_path')
534 tc_url = info_dict.get('tc_url')
535 flash_version = info_dict.get('flash_version')
536 live = info_dict.get('rtmp_live', False)
d7d86fdd 537 conn = info_dict.get('rtmp_conn')
4230c489 538 if player_url is not None:
539 args += ['-rtmp_swfverify', player_url]
540 if page_url is not None:
541 args += ['-rtmp_pageurl', page_url]
542 if app is not None:
543 args += ['-rtmp_app', app]
544 if play_path is not None:
545 args += ['-rtmp_playpath', play_path]
546 if tc_url is not None:
547 args += ['-rtmp_tcurl', tc_url]
548 if flash_version is not None:
549 args += ['-rtmp_flashver', flash_version]
550 if live:
551 args += ['-rtmp_live', 'live']
d7d86fdd
RA
552 if isinstance(conn, list):
553 for entry in conn:
554 args += ['-rtmp_conn', entry]
c487cf00 555 elif isinstance(conn, str):
d7d86fdd 556 args += ['-rtmp_conn', conn]
4230c489 557
5ec1b6b7 558 start_time, end_time = info_dict.get('section_start') or 0, info_dict.get('section_end')
559
3cf50fa8 560 selected_formats = info_dict.get('requested_formats') or [info_dict]
561 for i, fmt in enumerate(selected_formats):
e57eb982 562 is_http = re.match(r'^https?://', fmt['url'])
563 cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url']) if is_http else []
1ceb657b 564 if cookies:
565 args.extend(['-cookies', ''.join(
566 f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
567 for cookie in cookies)])
e57eb982 568 if fmt.get('http_headers') and is_http:
3cf50fa8 569 # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
570 # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
955c8958 571 args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in fmt['http_headers'].items())])
3cf50fa8 572
5ec1b6b7 573 if start_time:
574 args += ['-ss', str(start_time)]
575 if end_time:
576 args += ['-t', str(end_time - start_time)]
577
add96eb9 578 args += [*self._configuration_args((f'_i{i + 1}', '_i')), '-i', fmt['url']]
6b6c16ca 579
5ec1b6b7 580 if not (start_time or end_time) or not self.params.get('force_keyframes_at_cuts'):
581 args += ['-c', 'copy']
582
6251555f 583 if info_dict.get('requested_formats') or protocol == 'http_dash_segments':
3cf50fa8 584 for i, fmt in enumerate(selected_formats):
6251555f 585 stream_number = fmt.get('manifest_stream_number', 0)
234416e4 586 args.extend(['-map', f'{i}:{stream_number}'])
6d0fe752
JH
587
588 if self.params.get('test', False):
c487cf00 589 args += ['-fs', str(self._TEST_FILE_SIZE)]
6d0fe752 590
e5611e8e 591 ext = info_dict['ext']
f5436c5d 592 if protocol in ('m3u8', 'm3u8_native'):
9bd20204 593 use_mpegts = (tmpfilename == '-') or self.params.get('hls_use_mpegts')
594 if use_mpegts is None:
595 use_mpegts = info_dict.get('is_live')
596 if use_mpegts:
12b84ac8 597 args += ['-f', 'mpegts']
598 else:
8bdc1494 599 args += ['-f', 'mp4']
8913ef74 600 if (ffpp.basename == 'ffmpeg' and ffpp._features.get('needs_adtstoasc')) and (not info_dict.get('acodec') or info_dict['acodec'].split('.')[0] in ('aac', 'mp4a')):
8bdc1494 601 args += ['-bsf:a', 'aac_adtstoasc']
4230c489 602 elif protocol == 'rtmp':
603 args += ['-f', 'flv']
e5611e8e 604 elif ext == 'mp4' and tmpfilename == '-':
605 args += ['-f', 'mpegts']
af6793f8 606 elif ext == 'unknown_video':
607 ext = determine_ext(remove_end(tmpfilename, '.part'))
608 if ext == 'unknown_video':
609 self.report_warning(
610 'The video format is unknown and cannot be downloaded by ffmpeg. '
611 'Explicitly set the extension in the filename to attempt download in that format')
612 else:
613 self.report_warning(f'The video format is unknown. Trying to download as {ext} according to the filename')
614 args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
12b84ac8 615 else:
e5611e8e 616 args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
12b84ac8 617
9c42b7ee 618 args += traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args_out', ...))
619
6251555f 620 args += self._configuration_args(('_o1', '_o', ''))
330690a2 621
12b84ac8 622 args = [encodeArgument(opt) for opt in args]
d868f43c 623 args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
12b84ac8 624 self._debug_cmd(args)
625
3cf50fa8 626 piped = any(fmt['url'] in ('-', 'pipe:') for fmt in selected_formats)
f0c9fb96 627 with Popen(args, stdin=subprocess.PIPE, env=env) as proc:
3cf50fa8 628 if piped:
f0c9fb96 629 self.on_process_started(proc, proc.stdin)
630 try:
631 retval = proc.wait()
632 except BaseException as e:
633 # subprocces.run would send the SIGKILL signal to ffmpeg and the
634 # mp4 file couldn't be played, but if we ask ffmpeg to quit it
635 # produces a file that is playable (this is mostly useful for live
636 # streams). Note that Windows is not affected and produces playable
637 # files (see https://github.com/ytdl-org/youtube-dl/issues/8300).
3cf50fa8 638 if isinstance(e, KeyboardInterrupt) and sys.platform != 'win32' and not piped:
f0c9fb96 639 proc.communicate_or_kill(b'q')
640 else:
641 proc.kill(timeout=None)
642 raise
643 return retval
12b84ac8 644
645
646class AVconvFD(FFmpegFD):
647 pass
648
582be358 649
28787f16 650_BY_NAME = {
651 klass.get_basename(): klass
222516d9 652 for name, klass in globals().items()
1009f67c 653 if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
28787f16 654}
655
222516d9
PH
656
657def list_external_downloaders():
658 return sorted(_BY_NAME.keys())
659
660
661def get_external_downloader(external_downloader):
e1eabd7b 662 """ Given the name of the executable, see whether we support the given downloader """
6c4d20cd 663 bn = os.path.splitext(os.path.basename(external_downloader))[0]
e1eabd7b 664 return _BY_NAME.get(bn) or next((
665 klass for klass in _BY_NAME.values() if klass.EXE_NAME in bn
666 ), None)