]> jfr.im git - yt-dlp.git/blame - yt_dlp/downloader/external.py
[fd/external] Scope cookies
[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
f8271158 13from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor
222516d9 14from ..utils import (
f8271158 15 Popen,
be5c1ae8 16 RetryManager,
f8271158 17 _configuration_args,
18 check_executable,
28787f16 19 classproperty,
f8271158 20 cli_bool_option,
1195a38f
S
21 cli_option,
22 cli_valueless_option,
af6793f8 23 determine_ext,
74f8654a 24 encodeArgument,
f8271158 25 encodeFilename,
8c53322c 26 find_available_port,
af6793f8 27 remove_end,
8c53322c 28 sanitized_Request,
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
58 self.to_screen('[%s] Interrupted by user' % self.get_basename())
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
140 self.ydl.cookiejar.save(self._cookies_tempfile)
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']):
175 fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index)
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()
45806d44 189 self.try_remove(encodeFilename('%s.frag.urls' % tmpfilename))
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']
1ceb657b 202 if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
203 cmd += ['--cookie-jar', self._write_cookies()]
002ea8fe 204 if info_dict.get('http_headers') is not None:
205 for key, val in info_dict['http_headers'].items():
86e5f3ed 206 cmd += ['--header', f'{key}: {val}']
002ea8fe 207
98e698f1
RA
208 cmd += self._bool_option('--continue-at', 'continuedl', '-', '0')
209 cmd += self._valueless_option('--silent', 'noprogress')
210 cmd += self._valueless_option('--verbose', 'verbose')
211 cmd += self._option('--limit-rate', 'ratelimit')
37b239b3
S
212 retry = self._option('--retry', 'retries')
213 if len(retry) == 2:
214 if retry[1] in ('inf', 'infinite'):
215 retry[1] = '2147483647'
216 cmd += retry
98e698f1 217 cmd += self._option('--max-filesize', 'max_filesize')
9f3da138 218 cmd += self._option('--interface', 'source_address')
e7a8c303 219 cmd += self._option('--proxy', 'proxy')
dc534b67 220 cmd += self._valueless_option('--insecure', 'nocheckcertificate')
c75f0b36 221 cmd += self._configuration_args()
384b6202
PH
222 cmd += ['--', info_dict['url']]
223 return cmd
224
225
e0ac5214 226class AxelFD(ExternalFD):
91ee320b 227 AVAILABLE_OPT = '-V'
99cbe98c 228
e0ac5214 229 def _make_cmd(self, tmpfilename, info_dict):
230 cmd = [self.exe, '-o', tmpfilename]
002ea8fe 231 if info_dict.get('http_headers') is not None:
232 for key, val in info_dict['http_headers'].items():
86e5f3ed 233 cmd += ['-H', f'{key}: {val}']
1ceb657b 234 cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
235 if cookie_header:
236 cmd += [f'Cookie: {cookie_header}', '--max-redirect=0']
e0ac5214 237 cmd += self._configuration_args()
238 cmd += ['--', info_dict['url']]
239 return cmd
240
241
222516d9 242class WgetFD(ExternalFD):
91ee320b 243 AVAILABLE_OPT = '--version'
99cbe98c 244
222516d9 245 def _make_cmd(self, tmpfilename, info_dict):
1ceb657b 246 cmd = [self.exe, '-O', tmpfilename, '-nv', '--compression=auto']
247 if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
248 cmd += ['--load-cookies', self._write_cookies()]
002ea8fe 249 if info_dict.get('http_headers') is not None:
250 for key, val in info_dict['http_headers'].items():
86e5f3ed 251 cmd += ['--header', f'{key}: {val}']
8c80603f
S
252 cmd += self._option('--limit-rate', 'ratelimit')
253 retry = self._option('--tries', 'retries')
254 if len(retry) == 2:
255 if retry[1] in ('inf', 'infinite'):
256 retry[1] = '0'
257 cmd += retry
9f3da138 258 cmd += self._option('--bind-address', 'source_address')
8a23db95 259 proxy = self.params.get('proxy')
260 if proxy:
261 for var in ('http_proxy', 'https_proxy'):
86e5f3ed 262 cmd += ['--execute', f'{var}={proxy}']
dc534b67 263 cmd += self._valueless_option('--no-check-certificate', 'nocheckcertificate')
c75f0b36 264 cmd += self._configuration_args()
222516d9
PH
265 cmd += ['--', info_dict['url']]
266 return cmd
267
268
384b6202 269class Aria2cFD(ExternalFD):
91ee320b 270 AVAILABLE_OPT = '-v'
52a8a1e1 271 SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'dash_frag_urls', 'm3u8_frag_urls')
99cbe98c 272
0a473f2f 273 @staticmethod
274 def supports_manifest(manifest):
275 UNSUPPORTED_FEATURES = [
276 r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [1]
277 # 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
278 ]
279 check_results = (not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES)
280 return all(check_results)
281
af7a5eef 282 @staticmethod
283 def _aria2c_filename(fn):
284 return fn if os.path.isabs(fn) else f'.{os.path.sep}{fn}'
285
8c53322c 286 def _call_downloader(self, tmpfilename, info_dict):
ad68b16a 287 # FIXME: Disabled due to https://github.com/yt-dlp/yt-dlp/issues/5931
288 if False and 'no-external-downloader-progress' not in self.params.get('compat_opts', []):
8c53322c
L
289 info_dict['__rpc'] = {
290 'port': find_available_port() or 19190,
291 'secret': str(uuid.uuid4()),
292 }
293 return super()._call_downloader(tmpfilename, info_dict)
294
384b6202 295 def _make_cmd(self, tmpfilename, info_dict):
8a8af356 296 cmd = [self.exe, '-c', '--no-conf',
2b3bf01c 297 '--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
dcd55f76 298 '--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16']
2b3bf01c 299 if 'fragments' in info_dict:
300 cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true']
ff0f78e1 301 else:
302 cmd += ['--min-split-size', '1M']
2b3bf01c 303
1ceb657b 304 if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
305 cmd += [f'--load-cookies={self._write_cookies()}']
002ea8fe 306 if info_dict.get('http_headers') is not None:
307 for key, val in info_dict['http_headers'].items():
86e5f3ed 308 cmd += ['--header', f'{key}: {val}']
691d5823 309 cmd += self._option('--max-overall-download-limit', 'ratelimit')
9f3da138 310 cmd += self._option('--interface', 'source_address')
bf812ef7 311 cmd += self._option('--all-proxy', 'proxy')
266b0ad6 312 cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
71f47617 313 cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
f44afb54 314 cmd += self._bool_option('--show-console-readout', 'noprogress', 'false', 'true', '=')
2b3bf01c 315 cmd += self._configuration_args()
316
8c53322c
L
317 if '__rpc' in info_dict:
318 cmd += [
319 '--enable-rpc',
320 f'--rpc-listen-port={info_dict["__rpc"]["port"]}',
321 f'--rpc-secret={info_dict["__rpc"]["secret"]}']
322
eb55bad5 323 # aria2c strips out spaces from the beginning/end of filenames and paths.
324 # We work around this issue by adding a "./" to the beginning of the
325 # filename and relative path, and adding a "/" at the end of the path.
326 # See: https://github.com/yt-dlp/yt-dlp/issues/276
327 # https://github.com/ytdl-org/youtube-dl/issues/20312
328 # https://github.com/aria2/aria2/issues/1373
2b3bf01c 329 dn = os.path.dirname(tmpfilename)
330 if dn:
af7a5eef 331 cmd += ['--dir', self._aria2c_filename(dn) + os.path.sep]
2b3bf01c 332 if 'fragments' not in info_dict:
af7a5eef 333 cmd += ['--out', self._aria2c_filename(os.path.basename(tmpfilename))]
5219cb3e 334 cmd += ['--auto-file-renaming=false']
2b3bf01c 335
d7009caa 336 if 'fragments' in info_dict:
fe845284 337 cmd += ['--file-allocation=none', '--uri-selector=inorder']
5219cb3e 338 url_list_file = '%s.frag.urls' % tmpfilename
339 url_list = []
fe845284 340 for frag_index, fragment in enumerate(info_dict['fragments']):
341 fragment_filename = '%s-Frag%d' % (os.path.basename(tmpfilename), frag_index)
af7a5eef 342 url_list.append('%s\n\tout=%s' % (fragment['url'], self._aria2c_filename(fragment_filename)))
205a0654 343 stream, _ = self.sanitize_open(url_list_file, 'wb')
0f06bcd7 344 stream.write('\n'.join(url_list).encode())
539d158c 345 stream.close()
af7a5eef 346 cmd += ['-i', self._aria2c_filename(url_list_file)]
5219cb3e 347 else:
348 cmd += ['--', info_dict['url']]
384b6202
PH
349 return cmd
350
8c53322c
L
351 def aria2c_rpc(self, rpc_port, rpc_secret, method, params=()):
352 # Does not actually need to be UUID, just unique
353 sanitycheck = str(uuid.uuid4())
354 d = json.dumps({
355 'jsonrpc': '2.0',
356 'id': sanitycheck,
357 'method': method,
358 'params': [f'token:{rpc_secret}', *params],
359 }).encode('utf-8')
360 request = sanitized_Request(
361 f'http://localhost:{rpc_port}/jsonrpc',
362 data=d, headers={
363 'Content-Type': 'application/json',
364 'Content-Length': f'{len(d)}',
365 'Ytdl-request-proxy': '__noproxy__',
366 })
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,
419 'elapsed': time.time() - started
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
0a5a191a 494 args += traverse_obj(info_dict, ('downloader_options', 'ffmpeg_args'), default=[])
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):
512 proxy = 'http://%s' % proxy
20bad91d
YCH
513
514 if proxy.startswith('socks'):
515 self.report_warning(
6c9b71bc
YCH
516 '%s does not support SOCKS proxies. Downloading is likely to fail. '
517 'Consider adding --hls-prefer-native to your command.' % self.get_basename())
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):
1ceb657b 562 cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url'])
563 if cookies:
564 args.extend(['-cookies', ''.join(
565 f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
566 for cookie in cookies)])
3cf50fa8 567 if fmt.get('http_headers') and re.match(r'^https?://', fmt['url']):
3cf50fa8 568 # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
569 # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
955c8958 570 args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in fmt['http_headers'].items())])
3cf50fa8 571
5ec1b6b7 572 if start_time:
573 args += ['-ss', str(start_time)]
574 if end_time:
575 args += ['-t', str(end_time - start_time)]
576
3cf50fa8 577 args += self._configuration_args((f'_i{i + 1}', '_i')) + ['-i', fmt['url']]
6b6c16ca 578
5ec1b6b7 579 if not (start_time or end_time) or not self.params.get('force_keyframes_at_cuts'):
580 args += ['-c', 'copy']
581
6251555f 582 if info_dict.get('requested_formats') or protocol == 'http_dash_segments':
3cf50fa8 583 for i, fmt in enumerate(selected_formats):
6251555f 584 stream_number = fmt.get('manifest_stream_number', 0)
234416e4 585 args.extend(['-map', f'{i}:{stream_number}'])
6d0fe752
JH
586
587 if self.params.get('test', False):
c487cf00 588 args += ['-fs', str(self._TEST_FILE_SIZE)]
6d0fe752 589
e5611e8e 590 ext = info_dict['ext']
f5436c5d 591 if protocol in ('m3u8', 'm3u8_native'):
9bd20204 592 use_mpegts = (tmpfilename == '-') or self.params.get('hls_use_mpegts')
593 if use_mpegts is None:
594 use_mpegts = info_dict.get('is_live')
595 if use_mpegts:
12b84ac8 596 args += ['-f', 'mpegts']
597 else:
8bdc1494 598 args += ['-f', 'mp4']
8913ef74 599 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 600 args += ['-bsf:a', 'aac_adtstoasc']
4230c489 601 elif protocol == 'rtmp':
602 args += ['-f', 'flv']
e5611e8e 603 elif ext == 'mp4' and tmpfilename == '-':
604 args += ['-f', 'mpegts']
af6793f8 605 elif ext == 'unknown_video':
606 ext = determine_ext(remove_end(tmpfilename, '.part'))
607 if ext == 'unknown_video':
608 self.report_warning(
609 'The video format is unknown and cannot be downloaded by ffmpeg. '
610 'Explicitly set the extension in the filename to attempt download in that format')
611 else:
612 self.report_warning(f'The video format is unknown. Trying to download as {ext} according to the filename')
613 args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
12b84ac8 614 else:
e5611e8e 615 args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
12b84ac8 616
6251555f 617 args += self._configuration_args(('_o1', '_o', ''))
330690a2 618
12b84ac8 619 args = [encodeArgument(opt) for opt in args]
d868f43c 620 args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
12b84ac8 621 self._debug_cmd(args)
622
3cf50fa8 623 piped = any(fmt['url'] in ('-', 'pipe:') for fmt in selected_formats)
f0c9fb96 624 with Popen(args, stdin=subprocess.PIPE, env=env) as proc:
3cf50fa8 625 if piped:
f0c9fb96 626 self.on_process_started(proc, proc.stdin)
627 try:
628 retval = proc.wait()
629 except BaseException as e:
630 # subprocces.run would send the SIGKILL signal to ffmpeg and the
631 # mp4 file couldn't be played, but if we ask ffmpeg to quit it
632 # produces a file that is playable (this is mostly useful for live
633 # streams). Note that Windows is not affected and produces playable
634 # files (see https://github.com/ytdl-org/youtube-dl/issues/8300).
3cf50fa8 635 if isinstance(e, KeyboardInterrupt) and sys.platform != 'win32' and not piped:
f0c9fb96 636 proc.communicate_or_kill(b'q')
637 else:
638 proc.kill(timeout=None)
639 raise
640 return retval
12b84ac8 641
642
643class AVconvFD(FFmpegFD):
644 pass
645
582be358 646
28787f16 647_BY_NAME = {
648 klass.get_basename(): klass
222516d9 649 for name, klass in globals().items()
1009f67c 650 if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
28787f16 651}
652
222516d9
PH
653
654def list_external_downloaders():
655 return sorted(_BY_NAME.keys())
656
657
658def get_external_downloader(external_downloader):
e1eabd7b 659 """ Given the name of the executable, see whether we support the given downloader """
6c4d20cd 660 bn = os.path.splitext(os.path.basename(external_downloader))[0]
e1eabd7b 661 return _BY_NAME.get(bn) or next((
662 klass for klass in _BY_NAME.values() if klass.EXE_NAME in bn
663 ), None)