]> jfr.im git - yt-dlp.git/blob - youtube_dlc/YoutubeDL.py
Release 2021.02.04
[yt-dlp.git] / youtube_dlc / YoutubeDL.py
1 #!/usr/bin/env python
2 # coding: utf-8
3
4 from __future__ import absolute_import, unicode_literals
5
6 import collections
7 import contextlib
8 import copy
9 import datetime
10 import errno
11 import fileinput
12 import io
13 import itertools
14 import json
15 import locale
16 import operator
17 import os
18 import platform
19 import re
20 import shutil
21 import subprocess
22 import socket
23 import sys
24 import time
25 import tokenize
26 import traceback
27 import random
28
29 from string import ascii_letters
30
31 from .compat import (
32 compat_basestring,
33 compat_cookiejar,
34 compat_get_terminal_size,
35 compat_http_client,
36 compat_kwargs,
37 compat_numeric_types,
38 compat_os_name,
39 compat_str,
40 compat_tokenize_tokenize,
41 compat_urllib_error,
42 compat_urllib_request,
43 compat_urllib_request_DataHandler,
44 )
45 from .utils import (
46 age_restricted,
47 args_to_str,
48 ContentTooShortError,
49 date_from_str,
50 DateRange,
51 DEFAULT_OUTTMPL,
52 OUTTMPL_TYPES,
53 determine_ext,
54 determine_protocol,
55 DOT_DESKTOP_LINK_TEMPLATE,
56 DOT_URL_LINK_TEMPLATE,
57 DOT_WEBLOC_LINK_TEMPLATE,
58 DownloadError,
59 encode_compat_str,
60 encodeFilename,
61 error_to_compat_str,
62 ExistingVideoReached,
63 expand_path,
64 ExtractorError,
65 float_or_none,
66 format_bytes,
67 format_field,
68 formatSeconds,
69 GeoRestrictedError,
70 int_or_none,
71 iri_to_uri,
72 ISO3166Utils,
73 locked_file,
74 make_dir,
75 make_HTTPS_handler,
76 MaxDownloadsReached,
77 orderedSet,
78 PagedList,
79 parse_filesize,
80 PerRequestProxyHandler,
81 platform_name,
82 PostProcessingError,
83 preferredencoding,
84 prepend_extension,
85 register_socks_protocols,
86 render_table,
87 replace_extension,
88 RejectedVideoReached,
89 SameFileError,
90 sanitize_filename,
91 sanitize_path,
92 sanitize_url,
93 sanitized_Request,
94 std_headers,
95 str_or_none,
96 strftime_or_none,
97 subtitles_filename,
98 to_high_limit_path,
99 UnavailableVideoError,
100 url_basename,
101 version_tuple,
102 write_json_file,
103 write_string,
104 YoutubeDLCookieJar,
105 YoutubeDLCookieProcessor,
106 YoutubeDLHandler,
107 YoutubeDLRedirectHandler,
108 process_communicate_or_kill,
109 )
110 from .cache import Cache
111 from .extractor import get_info_extractor, gen_extractor_classes, _LAZY_LOADER, _PLUGIN_CLASSES
112 from .extractor.openload import PhantomJSwrapper
113 from .downloader import get_suitable_downloader
114 from .downloader.rtmp import rtmpdump_version
115 from .postprocessor import (
116 FFmpegFixupM3u8PP,
117 FFmpegFixupM4aPP,
118 FFmpegFixupStretchedPP,
119 FFmpegMergerPP,
120 FFmpegPostProcessor,
121 # FFmpegSubtitlesConvertorPP,
122 get_postprocessor,
123 MoveFilesAfterDownloadPP,
124 )
125 from .version import __version__
126
127 if compat_os_name == 'nt':
128 import ctypes
129
130
131 class YoutubeDL(object):
132 """YoutubeDL class.
133
134 YoutubeDL objects are the ones responsible of downloading the
135 actual video file and writing it to disk if the user has requested
136 it, among some other tasks. In most cases there should be one per
137 program. As, given a video URL, the downloader doesn't know how to
138 extract all the needed information, task that InfoExtractors do, it
139 has to pass the URL to one of them.
140
141 For this, YoutubeDL objects have a method that allows
142 InfoExtractors to be registered in a given order. When it is passed
143 a URL, the YoutubeDL object handles it to the first InfoExtractor it
144 finds that reports being able to handle it. The InfoExtractor extracts
145 all the information about the video or videos the URL refers to, and
146 YoutubeDL process the extracted information, possibly using a File
147 Downloader to download the video.
148
149 YoutubeDL objects accept a lot of parameters. In order not to saturate
150 the object constructor with arguments, it receives a dictionary of
151 options instead. These options are available through the params
152 attribute for the InfoExtractors to use. The YoutubeDL also
153 registers itself as the downloader in charge for the InfoExtractors
154 that are added to it, so this is a "mutual registration".
155
156 Available options:
157
158 username: Username for authentication purposes.
159 password: Password for authentication purposes.
160 videopassword: Password for accessing a video.
161 ap_mso: Adobe Pass multiple-system operator identifier.
162 ap_username: Multiple-system operator account username.
163 ap_password: Multiple-system operator account password.
164 usenetrc: Use netrc for authentication instead.
165 verbose: Print additional info to stdout.
166 quiet: Do not print messages to stdout.
167 no_warnings: Do not print out anything for warnings.
168 forceurl: Force printing final URL.
169 forcetitle: Force printing title.
170 forceid: Force printing ID.
171 forcethumbnail: Force printing thumbnail URL.
172 forcedescription: Force printing description.
173 forcefilename: Force printing final filename.
174 forceduration: Force printing duration.
175 forcejson: Force printing info_dict as JSON.
176 dump_single_json: Force printing the info_dict of the whole playlist
177 (or video) as a single JSON line.
178 force_write_download_archive: Force writing download archive regardless
179 of 'skip_download' or 'simulate'.
180 simulate: Do not download the video files.
181 format: Video format code. see "FORMAT SELECTION" for more details.
182 format_sort: How to sort the video formats. see "Sorting Formats"
183 for more details.
184 format_sort_force: Force the given format_sort. see "Sorting Formats"
185 for more details.
186 allow_multiple_video_streams: Allow multiple video streams to be merged
187 into a single file
188 allow_multiple_audio_streams: Allow multiple audio streams to be merged
189 into a single file
190 outtmpl: Dictionary of templates for output names. Allowed keys
191 are 'default' and the keys of OUTTMPL_TYPES (in utils.py)
192 outtmpl_na_placeholder: Placeholder for unavailable meta fields.
193 restrictfilenames: Do not allow "&" and spaces in file names
194 trim_file_name: Limit length of filename (extension excluded)
195 ignoreerrors: Do not stop on download errors
196 (Default True when running youtube-dlc,
197 but False when directly accessing YoutubeDL class)
198 force_generic_extractor: Force downloader to use the generic extractor
199 overwrites: Overwrite all video and metadata files if True,
200 overwrite only non-video files if None
201 and don't overwrite any file if False
202 playliststart: Playlist item to start at.
203 playlistend: Playlist item to end at.
204 playlist_items: Specific indices of playlist to download.
205 playlistreverse: Download playlist items in reverse order.
206 playlistrandom: Download playlist items in random order.
207 matchtitle: Download only matching titles.
208 rejecttitle: Reject downloads for matching titles.
209 logger: Log messages to a logging.Logger instance.
210 logtostderr: Log messages to stderr instead of stdout.
211 writedescription: Write the video description to a .description file
212 writeinfojson: Write the video description to a .info.json file
213 writecomments: Extract video comments. This will not be written to disk
214 unless writeinfojson is also given
215 writeannotations: Write the video annotations to a .annotations.xml file
216 writethumbnail: Write the thumbnail image to a file
217 allow_playlist_files: Whether to write playlists' description, infojson etc
218 also to disk when using the 'write*' options
219 write_all_thumbnails: Write all thumbnail formats to files
220 writelink: Write an internet shortcut file, depending on the
221 current platform (.url/.webloc/.desktop)
222 writeurllink: Write a Windows internet shortcut file (.url)
223 writewebloclink: Write a macOS internet shortcut file (.webloc)
224 writedesktoplink: Write a Linux internet shortcut file (.desktop)
225 writesubtitles: Write the video subtitles to a file
226 writeautomaticsub: Write the automatically generated subtitles to a file
227 allsubtitles: Downloads all the subtitles of the video
228 (requires writesubtitles or writeautomaticsub)
229 listsubtitles: Lists all available subtitles for the video
230 subtitlesformat: The format code for subtitles
231 subtitleslangs: List of languages of the subtitles to download
232 keepvideo: Keep the video file after post-processing
233 daterange: A DateRange object, download only if the upload_date is in the range.
234 skip_download: Skip the actual download of the video file
235 cachedir: Location of the cache files in the filesystem.
236 False to disable filesystem cache.
237 noplaylist: Download single video instead of a playlist if in doubt.
238 age_limit: An integer representing the user's age in years.
239 Unsuitable videos for the given age are skipped.
240 min_views: An integer representing the minimum view count the video
241 must have in order to not be skipped.
242 Videos without view count information are always
243 downloaded. None for no limit.
244 max_views: An integer representing the maximum view count.
245 Videos that are more popular than that are not
246 downloaded.
247 Videos without view count information are always
248 downloaded. None for no limit.
249 download_archive: File name of a file where all downloads are recorded.
250 Videos already present in the file are not downloaded
251 again.
252 break_on_existing: Stop the download process after attempting to download a
253 file that is in the archive.
254 break_on_reject: Stop the download process when encountering a video that
255 has been filtered out.
256 cookiefile: File name where cookies should be read from and dumped to
257 nocheckcertificate:Do not verify SSL certificates
258 prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
259 At the moment, this is only supported by YouTube.
260 proxy: URL of the proxy server to use
261 geo_verification_proxy: URL of the proxy to use for IP address verification
262 on geo-restricted sites.
263 socket_timeout: Time to wait for unresponsive hosts, in seconds
264 bidi_workaround: Work around buggy terminals without bidirectional text
265 support, using fridibi
266 debug_printtraffic:Print out sent and received HTTP traffic
267 include_ads: Download ads as well
268 default_search: Prepend this string if an input url is not valid.
269 'auto' for elaborate guessing
270 encoding: Use this encoding instead of the system-specified.
271 extract_flat: Do not resolve URLs, return the immediate result.
272 Pass in 'in_playlist' to only show this behavior for
273 playlist items.
274 postprocessors: A list of dictionaries, each with an entry
275 * key: The name of the postprocessor. See
276 youtube_dlc/postprocessor/__init__.py for a list.
277 * _after_move: Optional. If True, run this post_processor
278 after 'MoveFilesAfterDownload'
279 as well as any further keyword arguments for the
280 postprocessor.
281 post_hooks: A list of functions that get called as the final step
282 for each video file, after all postprocessors have been
283 called. The filename will be passed as the only argument.
284 progress_hooks: A list of functions that get called on download
285 progress, with a dictionary with the entries
286 * status: One of "downloading", "error", or "finished".
287 Check this first and ignore unknown values.
288
289 If status is one of "downloading", or "finished", the
290 following properties may also be present:
291 * filename: The final filename (always present)
292 * tmpfilename: The filename we're currently writing to
293 * downloaded_bytes: Bytes on disk
294 * total_bytes: Size of the whole file, None if unknown
295 * total_bytes_estimate: Guess of the eventual file size,
296 None if unavailable.
297 * elapsed: The number of seconds since download started.
298 * eta: The estimated time in seconds, None if unknown
299 * speed: The download speed in bytes/second, None if
300 unknown
301 * fragment_index: The counter of the currently
302 downloaded video fragment.
303 * fragment_count: The number of fragments (= individual
304 files that will be merged)
305
306 Progress hooks are guaranteed to be called at least once
307 (with status "finished") if the download is successful.
308 merge_output_format: Extension to use when merging formats.
309 final_ext: Expected final extension; used to detect when the file was
310 already downloaded and converted. "merge_output_format" is
311 replaced by this extension when given
312 fixup: Automatically correct known faults of the file.
313 One of:
314 - "never": do nothing
315 - "warn": only emit a warning
316 - "detect_or_warn": check whether we can do anything
317 about it, warn otherwise (default)
318 source_address: Client-side IP address to bind to.
319 call_home: Boolean, true iff we are allowed to contact the
320 youtube-dlc servers for debugging.
321 sleep_interval: Number of seconds to sleep before each download when
322 used alone or a lower bound of a range for randomized
323 sleep before each download (minimum possible number
324 of seconds to sleep) when used along with
325 max_sleep_interval.
326 max_sleep_interval:Upper bound of a range for randomized sleep before each
327 download (maximum possible number of seconds to sleep).
328 Must only be used along with sleep_interval.
329 Actual sleep time will be a random float from range
330 [sleep_interval; max_sleep_interval].
331 listformats: Print an overview of available video formats and exit.
332 list_thumbnails: Print a table of all thumbnails and exit.
333 match_filter: A function that gets called with the info_dict of
334 every video.
335 If it returns a message, the video is ignored.
336 If it returns None, the video is downloaded.
337 match_filter_func in utils.py is one example for this.
338 no_color: Do not emit color codes in output.
339 geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
340 HTTP header
341 geo_bypass_country:
342 Two-letter ISO 3166-2 country code that will be used for
343 explicit geographic restriction bypassing via faking
344 X-Forwarded-For HTTP header
345 geo_bypass_ip_block:
346 IP range in CIDR notation that will be used similarly to
347 geo_bypass_country
348
349 The following options determine which downloader is picked:
350 external_downloader: Executable of the external downloader to call.
351 None or unset for standard (built-in) downloader.
352 hls_prefer_native: Use the native HLS downloader instead of ffmpeg/avconv
353 if True, otherwise use ffmpeg/avconv if False, otherwise
354 use downloader suggested by extractor if None.
355
356 The following parameters are not used by YoutubeDL itself, they are used by
357 the downloader (see youtube_dlc/downloader/common.py):
358 nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
359 noresizebuffer, retries, continuedl, noprogress, consoletitle,
360 xattr_set_filesize, external_downloader_args, hls_use_mpegts,
361 http_chunk_size.
362
363 The following options are used by the post processors:
364 prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available,
365 otherwise prefer ffmpeg. (avconv support is deprecated)
366 ffmpeg_location: Location of the ffmpeg/avconv binary; either the path
367 to the binary or its containing directory.
368 postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
369 and a list of additional command-line arguments for the
370 postprocessor/executable. The dict can also have "PP+EXE" keys
371 which are used when the given exe is used by the given PP.
372 Use 'default' as the name for arguments to passed to all PP
373 The following options are used by the Youtube extractor:
374 youtube_include_dash_manifest: If True (default), DASH manifests and related
375 data will be downloaded and processed by extractor.
376 You can reduce network I/O by disabling it if you don't
377 care about DASH.
378 """
379
380 _NUMERIC_FIELDS = set((
381 'width', 'height', 'tbr', 'abr', 'asr', 'vbr', 'fps', 'filesize', 'filesize_approx',
382 'timestamp', 'upload_year', 'upload_month', 'upload_day',
383 'duration', 'view_count', 'like_count', 'dislike_count', 'repost_count',
384 'average_rating', 'comment_count', 'age_limit',
385 'start_time', 'end_time',
386 'chapter_number', 'season_number', 'episode_number',
387 'track_number', 'disc_number', 'release_year',
388 'playlist_index',
389 ))
390
391 params = None
392 _ies = []
393 _pps = {'beforedl': [], 'aftermove': [], 'normal': []}
394 __prepare_filename_warned = False
395 _download_retcode = None
396 _num_downloads = None
397 _playlist_level = 0
398 _playlist_urls = set()
399 _screen_file = None
400
401 def __init__(self, params=None, auto_init=True):
402 """Create a FileDownloader object with the given options."""
403 if params is None:
404 params = {}
405 self._ies = []
406 self._ies_instances = {}
407 self._pps = {'beforedl': [], 'aftermove': [], 'normal': []}
408 self.__prepare_filename_warned = False
409 self._post_hooks = []
410 self._progress_hooks = []
411 self._download_retcode = 0
412 self._num_downloads = 0
413 self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
414 self._err_file = sys.stderr
415 self.params = {
416 # Default parameters
417 'nocheckcertificate': False,
418 }
419 self.params.update(params)
420 self.cache = Cache(self)
421 self.archive = set()
422
423 """Preload the archive, if any is specified"""
424 def preload_download_archive(self):
425 fn = self.params.get('download_archive')
426 if fn is None:
427 return False
428 try:
429 with locked_file(fn, 'r', encoding='utf-8') as archive_file:
430 for line in archive_file:
431 self.archive.add(line.strip())
432 except IOError as ioe:
433 if ioe.errno != errno.ENOENT:
434 raise
435 return False
436 return True
437
438 def check_deprecated(param, option, suggestion):
439 if self.params.get(param) is not None:
440 self.report_warning(
441 '%s is deprecated. Use %s instead.' % (option, suggestion))
442 return True
443 return False
444
445 if self.params.get('verbose'):
446 self.to_stdout('[debug] Loading archive file %r' % self.params.get('download_archive'))
447
448 preload_download_archive(self)
449
450 if check_deprecated('cn_verification_proxy', '--cn-verification-proxy', '--geo-verification-proxy'):
451 if self.params.get('geo_verification_proxy') is None:
452 self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
453
454 if self.params.get('final_ext'):
455 if self.params.get('merge_output_format'):
456 self.report_warning('--merge-output-format will be ignored since --remux-video or --recode-video is given')
457 self.params['merge_output_format'] = self.params['final_ext']
458
459 if 'overwrites' in self.params and self.params['overwrites'] is None:
460 del self.params['overwrites']
461
462 check_deprecated('autonumber_size', '--autonumber-size', 'output template with %(autonumber)0Nd, where N in the number of digits')
463 check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
464 check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
465
466 if params.get('bidi_workaround', False):
467 try:
468 import pty
469 master, slave = pty.openpty()
470 width = compat_get_terminal_size().columns
471 if width is None:
472 width_args = []
473 else:
474 width_args = ['-w', str(width)]
475 sp_kwargs = dict(
476 stdin=subprocess.PIPE,
477 stdout=slave,
478 stderr=self._err_file)
479 try:
480 self._output_process = subprocess.Popen(
481 ['bidiv'] + width_args, **sp_kwargs
482 )
483 except OSError:
484 self._output_process = subprocess.Popen(
485 ['fribidi', '-c', 'UTF-8'] + width_args, **sp_kwargs)
486 self._output_channel = os.fdopen(master, 'rb')
487 except OSError as ose:
488 if ose.errno == errno.ENOENT:
489 self.report_warning('Could not find fribidi executable, ignoring --bidi-workaround . Make sure that fribidi is an executable file in one of the directories in your $PATH.')
490 else:
491 raise
492
493 if (sys.platform != 'win32'
494 and sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
495 and not params.get('restrictfilenames', False)):
496 # Unicode filesystem API will throw errors (#1474, #13027)
497 self.report_warning(
498 'Assuming --restrict-filenames since file system encoding '
499 'cannot encode all characters. '
500 'Set the LC_ALL environment variable to fix this.')
501 self.params['restrictfilenames'] = True
502
503 self.outtmpl_dict = self.parse_outtmpl()
504
505 self._setup_opener()
506
507 if auto_init:
508 self.print_debug_header()
509 self.add_default_info_extractors()
510
511 for pp_def_raw in self.params.get('postprocessors', []):
512 pp_class = get_postprocessor(pp_def_raw['key'])
513 pp_def = dict(pp_def_raw)
514 del pp_def['key']
515 if 'when' in pp_def:
516 when = pp_def['when']
517 del pp_def['when']
518 else:
519 when = 'normal'
520 pp = pp_class(self, **compat_kwargs(pp_def))
521 self.add_post_processor(pp, when=when)
522
523 for ph in self.params.get('post_hooks', []):
524 self.add_post_hook(ph)
525
526 for ph in self.params.get('progress_hooks', []):
527 self.add_progress_hook(ph)
528
529 register_socks_protocols()
530
531 def warn_if_short_id(self, argv):
532 # short YouTube ID starting with dash?
533 idxs = [
534 i for i, a in enumerate(argv)
535 if re.match(r'^-[0-9A-Za-z_-]{10}$', a)]
536 if idxs:
537 correct_argv = (
538 ['youtube-dlc']
539 + [a for i, a in enumerate(argv) if i not in idxs]
540 + ['--'] + [argv[i] for i in idxs]
541 )
542 self.report_warning(
543 'Long argument string detected. '
544 'Use -- to separate parameters and URLs, like this:\n%s\n' %
545 args_to_str(correct_argv))
546
547 def add_info_extractor(self, ie):
548 """Add an InfoExtractor object to the end of the list."""
549 self._ies.append(ie)
550 if not isinstance(ie, type):
551 self._ies_instances[ie.ie_key()] = ie
552 ie.set_downloader(self)
553
554 def get_info_extractor(self, ie_key):
555 """
556 Get an instance of an IE with name ie_key, it will try to get one from
557 the _ies list, if there's no instance it will create a new one and add
558 it to the extractor list.
559 """
560 ie = self._ies_instances.get(ie_key)
561 if ie is None:
562 ie = get_info_extractor(ie_key)()
563 self.add_info_extractor(ie)
564 return ie
565
566 def add_default_info_extractors(self):
567 """
568 Add the InfoExtractors returned by gen_extractors to the end of the list
569 """
570 for ie in gen_extractor_classes():
571 self.add_info_extractor(ie)
572
573 def add_post_processor(self, pp, when='normal'):
574 """Add a PostProcessor object to the end of the chain."""
575 self._pps[when].append(pp)
576 pp.set_downloader(self)
577
578 def add_post_hook(self, ph):
579 """Add the post hook"""
580 self._post_hooks.append(ph)
581
582 def add_progress_hook(self, ph):
583 """Add the progress hook (currently only for the file downloader)"""
584 self._progress_hooks.append(ph)
585
586 def _bidi_workaround(self, message):
587 if not hasattr(self, '_output_channel'):
588 return message
589
590 assert hasattr(self, '_output_process')
591 assert isinstance(message, compat_str)
592 line_count = message.count('\n') + 1
593 self._output_process.stdin.write((message + '\n').encode('utf-8'))
594 self._output_process.stdin.flush()
595 res = ''.join(self._output_channel.readline().decode('utf-8')
596 for _ in range(line_count))
597 return res[:-len('\n')]
598
599 def to_screen(self, message, skip_eol=False):
600 """Print message to stdout if not in quiet mode."""
601 return self.to_stdout(message, skip_eol, check_quiet=True)
602
603 def _write_string(self, s, out=None):
604 write_string(s, out=out, encoding=self.params.get('encoding'))
605
606 def to_stdout(self, message, skip_eol=False, check_quiet=False):
607 """Print message to stdout if not in quiet mode."""
608 if self.params.get('logger'):
609 self.params['logger'].debug(message)
610 elif not check_quiet or not self.params.get('quiet', False):
611 message = self._bidi_workaround(message)
612 terminator = ['\n', ''][skip_eol]
613 output = message + terminator
614
615 self._write_string(output, self._screen_file)
616
617 def to_stderr(self, message):
618 """Print message to stderr."""
619 assert isinstance(message, compat_str)
620 if self.params.get('logger'):
621 self.params['logger'].error(message)
622 else:
623 message = self._bidi_workaround(message)
624 output = message + '\n'
625 self._write_string(output, self._err_file)
626
627 def to_console_title(self, message):
628 if not self.params.get('consoletitle', False):
629 return
630 if compat_os_name == 'nt':
631 if ctypes.windll.kernel32.GetConsoleWindow():
632 # c_wchar_p() might not be necessary if `message` is
633 # already of type unicode()
634 ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
635 elif 'TERM' in os.environ:
636 self._write_string('\033]0;%s\007' % message, self._screen_file)
637
638 def save_console_title(self):
639 if not self.params.get('consoletitle', False):
640 return
641 if self.params.get('simulate', False):
642 return
643 if compat_os_name != 'nt' and 'TERM' in os.environ:
644 # Save the title on stack
645 self._write_string('\033[22;0t', self._screen_file)
646
647 def restore_console_title(self):
648 if not self.params.get('consoletitle', False):
649 return
650 if self.params.get('simulate', False):
651 return
652 if compat_os_name != 'nt' and 'TERM' in os.environ:
653 # Restore the title from stack
654 self._write_string('\033[23;0t', self._screen_file)
655
656 def __enter__(self):
657 self.save_console_title()
658 return self
659
660 def __exit__(self, *args):
661 self.restore_console_title()
662
663 if self.params.get('cookiefile') is not None:
664 self.cookiejar.save(ignore_discard=True, ignore_expires=True)
665
666 def trouble(self, message=None, tb=None):
667 """Determine action to take when a download problem appears.
668
669 Depending on if the downloader has been configured to ignore
670 download errors or not, this method may throw an exception or
671 not when errors are found, after printing the message.
672
673 tb, if given, is additional traceback information.
674 """
675 if message is not None:
676 self.to_stderr(message)
677 if self.params.get('verbose'):
678 if tb is None:
679 if sys.exc_info()[0]: # if .trouble has been called from an except block
680 tb = ''
681 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
682 tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
683 tb += encode_compat_str(traceback.format_exc())
684 else:
685 tb_data = traceback.format_list(traceback.extract_stack())
686 tb = ''.join(tb_data)
687 self.to_stderr(tb)
688 if not self.params.get('ignoreerrors', False):
689 if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
690 exc_info = sys.exc_info()[1].exc_info
691 else:
692 exc_info = sys.exc_info()
693 raise DownloadError(message, exc_info)
694 self._download_retcode = 1
695
696 def report_warning(self, message):
697 '''
698 Print the message to stderr, it will be prefixed with 'WARNING:'
699 If stderr is a tty file the 'WARNING:' will be colored
700 '''
701 if self.params.get('logger') is not None:
702 self.params['logger'].warning(message)
703 else:
704 if self.params.get('no_warnings'):
705 return
706 if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
707 _msg_header = '\033[0;33mWARNING:\033[0m'
708 else:
709 _msg_header = 'WARNING:'
710 warning_message = '%s %s' % (_msg_header, message)
711 self.to_stderr(warning_message)
712
713 def report_error(self, message, tb=None):
714 '''
715 Do the same as trouble, but prefixes the message with 'ERROR:', colored
716 in red if stderr is a tty file.
717 '''
718 if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
719 _msg_header = '\033[0;31mERROR:\033[0m'
720 else:
721 _msg_header = 'ERROR:'
722 error_message = '%s %s' % (_msg_header, message)
723 self.trouble(error_message, tb)
724
725 def report_file_already_downloaded(self, file_name):
726 """Report file has already been fully downloaded."""
727 try:
728 self.to_screen('[download] %s has already been downloaded' % file_name)
729 except UnicodeEncodeError:
730 self.to_screen('[download] The file has already been downloaded')
731
732 def report_file_delete(self, file_name):
733 """Report that existing file will be deleted."""
734 try:
735 self.to_screen('Deleting existing file %s' % file_name)
736 except UnicodeEncodeError:
737 self.to_screen('Deleting existing file')
738
739 def parse_outtmpl(self):
740 outtmpl_dict = self.params.get('outtmpl', {})
741 if not isinstance(outtmpl_dict, dict):
742 outtmpl_dict = {'default': outtmpl_dict}
743 outtmpl_dict.update({
744 k: v for k, v in DEFAULT_OUTTMPL.items()
745 if not outtmpl_dict.get(k)})
746 for key, val in outtmpl_dict.items():
747 if isinstance(val, bytes):
748 self.report_warning(
749 'Parameter outtmpl is bytes, but should be a unicode string. '
750 'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
751 return outtmpl_dict
752
753 def _prepare_filename(self, info_dict, tmpl_type='default'):
754 try:
755 template_dict = dict(info_dict)
756
757 template_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs
758 formatSeconds(info_dict['duration'], '-')
759 if info_dict.get('duration', None) is not None
760 else None)
761
762 template_dict['epoch'] = int(time.time())
763 autonumber_size = self.params.get('autonumber_size')
764 if autonumber_size is None:
765 autonumber_size = 5
766 template_dict['autonumber'] = self.params.get('autonumber_start', 1) - 1 + self._num_downloads
767 if template_dict.get('resolution') is None:
768 if template_dict.get('width') and template_dict.get('height'):
769 template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
770 elif template_dict.get('height'):
771 template_dict['resolution'] = '%sp' % template_dict['height']
772 elif template_dict.get('width'):
773 template_dict['resolution'] = '%dx?' % template_dict['width']
774
775 sanitize = lambda k, v: sanitize_filename(
776 compat_str(v),
777 restricted=self.params.get('restrictfilenames'),
778 is_id=(k == 'id' or k.endswith('_id')))
779 template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
780 for k, v in template_dict.items()
781 if v is not None and not isinstance(v, (list, tuple, dict)))
782 na = self.params.get('outtmpl_na_placeholder', 'NA')
783 template_dict = collections.defaultdict(lambda: na, template_dict)
784
785 outtmpl = self.outtmpl_dict.get(tmpl_type, self.outtmpl_dict['default'])
786 force_ext = OUTTMPL_TYPES.get(tmpl_type)
787
788 # For fields playlist_index and autonumber convert all occurrences
789 # of %(field)s to %(field)0Nd for backward compatibility
790 field_size_compat_map = {
791 'playlist_index': len(str(template_dict['n_entries'])),
792 'autonumber': autonumber_size,
793 }
794 FIELD_SIZE_COMPAT_RE = r'(?<!%)%\((?P<field>autonumber|playlist_index)\)s'
795 mobj = re.search(FIELD_SIZE_COMPAT_RE, outtmpl)
796 if mobj:
797 outtmpl = re.sub(
798 FIELD_SIZE_COMPAT_RE,
799 r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
800 outtmpl)
801
802 # As of [1] format syntax is:
803 # %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
804 # 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
805 FORMAT_RE = r'''(?x)
806 (?<!%)
807 %
808 \({0}\) # mapping key
809 (?:[#0\-+ ]+)? # conversion flags (optional)
810 (?:\d+)? # minimum field width (optional)
811 (?:\.\d+)? # precision (optional)
812 [hlL]? # length modifier (optional)
813 (?P<type>[diouxXeEfFgGcrs%]) # conversion type
814 '''
815
816 numeric_fields = list(self._NUMERIC_FIELDS)
817
818 # Format date
819 FORMAT_DATE_RE = FORMAT_RE.format(r'(?P<key>(?P<field>\w+)>(?P<format>.+?))')
820 for mobj in re.finditer(FORMAT_DATE_RE, outtmpl):
821 conv_type, field, frmt, key = mobj.group('type', 'field', 'format', 'key')
822 if key in template_dict:
823 continue
824 value = strftime_or_none(template_dict.get(field), frmt, na)
825 if conv_type in 'crs': # string
826 value = sanitize(field, value)
827 else: # number
828 numeric_fields.append(key)
829 value = float_or_none(value, default=None)
830 if value is not None:
831 template_dict[key] = value
832
833 # Missing numeric fields used together with integer presentation types
834 # in format specification will break the argument substitution since
835 # string NA placeholder is returned for missing fields. We will patch
836 # output template for missing fields to meet string presentation type.
837 for numeric_field in numeric_fields:
838 if numeric_field not in template_dict:
839 outtmpl = re.sub(
840 FORMAT_RE.format(re.escape(numeric_field)),
841 r'%({0})s'.format(numeric_field), outtmpl)
842
843 # expand_path translates '%%' into '%' and '$$' into '$'
844 # correspondingly that is not what we want since we need to keep
845 # '%%' intact for template dict substitution step. Working around
846 # with boundary-alike separator hack.
847 sep = ''.join([random.choice(ascii_letters) for _ in range(32)])
848 outtmpl = outtmpl.replace('%%', '%{0}%'.format(sep)).replace('$$', '${0}$'.format(sep))
849
850 # outtmpl should be expand_path'ed before template dict substitution
851 # because meta fields may contain env variables we don't want to
852 # be expanded. For example, for outtmpl "%(title)s.%(ext)s" and
853 # title "Hello $PATH", we don't want `$PATH` to be expanded.
854 filename = expand_path(outtmpl).replace(sep, '') % template_dict
855
856 if force_ext is not None:
857 filename = replace_extension(filename, force_ext, template_dict.get('ext'))
858
859 # https://github.com/blackjack4494/youtube-dlc/issues/85
860 trim_file_name = self.params.get('trim_file_name', False)
861 if trim_file_name:
862 fn_groups = filename.rsplit('.')
863 ext = fn_groups[-1]
864 sub_ext = ''
865 if len(fn_groups) > 2:
866 sub_ext = fn_groups[-2]
867 filename = '.'.join(filter(None, [fn_groups[0][:trim_file_name], sub_ext, ext]))
868
869 # Temporary fix for #4787
870 # 'Treat' all problem characters by passing filename through preferredencoding
871 # to workaround encoding issues with subprocess on python2 @ Windows
872 if sys.version_info < (3, 0) and sys.platform == 'win32':
873 filename = encodeFilename(filename, True).decode(preferredencoding())
874 filename = sanitize_path(filename)
875
876 return filename
877 except ValueError as err:
878 self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
879 return None
880
881 def prepare_filename(self, info_dict, dir_type='', warn=False):
882 """Generate the output filename."""
883 paths = self.params.get('paths', {})
884 assert isinstance(paths, dict)
885 filename = self._prepare_filename(info_dict, dir_type or 'default')
886
887 if warn and not self.__prepare_filename_warned:
888 if not paths:
889 pass
890 elif filename == '-':
891 self.report_warning('--paths is ignored when an outputting to stdout')
892 elif os.path.isabs(filename):
893 self.report_warning('--paths is ignored since an absolute path is given in output template')
894 self.__prepare_filename_warned = True
895 if filename == '-' or not filename:
896 return filename
897
898 homepath = expand_path(paths.get('home', '').strip())
899 assert isinstance(homepath, compat_str)
900 subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else ''
901 assert isinstance(subdir, compat_str)
902 return sanitize_path(os.path.join(homepath, subdir, filename))
903
904 def _match_entry(self, info_dict, incomplete):
905 """ Returns None if the file should be downloaded """
906
907 def check_filter():
908 video_title = info_dict.get('title', info_dict.get('id', 'video'))
909 if 'title' in info_dict:
910 # This can happen when we're just evaluating the playlist
911 title = info_dict['title']
912 matchtitle = self.params.get('matchtitle', False)
913 if matchtitle:
914 if not re.search(matchtitle, title, re.IGNORECASE):
915 return '"' + title + '" title did not match pattern "' + matchtitle + '"'
916 rejecttitle = self.params.get('rejecttitle', False)
917 if rejecttitle:
918 if re.search(rejecttitle, title, re.IGNORECASE):
919 return '"' + title + '" title matched reject pattern "' + rejecttitle + '"'
920 date = info_dict.get('upload_date')
921 if date is not None:
922 dateRange = self.params.get('daterange', DateRange())
923 if date not in dateRange:
924 return '%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
925 view_count = info_dict.get('view_count')
926 if view_count is not None:
927 min_views = self.params.get('min_views')
928 if min_views is not None and view_count < min_views:
929 return 'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
930 max_views = self.params.get('max_views')
931 if max_views is not None and view_count > max_views:
932 return 'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
933 if age_restricted(info_dict.get('age_limit'), self.params.get('age_limit')):
934 return 'Skipping "%s" because it is age restricted' % video_title
935 if self.in_download_archive(info_dict):
936 return '%s has already been recorded in archive' % video_title
937
938 if not incomplete:
939 match_filter = self.params.get('match_filter')
940 if match_filter is not None:
941 ret = match_filter(info_dict)
942 if ret is not None:
943 return ret
944 return None
945
946 reason = check_filter()
947 if reason is not None:
948 self.to_screen('[download] ' + reason)
949 if reason.endswith('has already been recorded in the archive') and self.params.get('break_on_existing', False):
950 raise ExistingVideoReached()
951 elif self.params.get('break_on_reject', False):
952 raise RejectedVideoReached()
953 return reason
954
955 @staticmethod
956 def add_extra_info(info_dict, extra_info):
957 '''Set the keys from extra_info in info dict if they are missing'''
958 for key, value in extra_info.items():
959 info_dict.setdefault(key, value)
960
961 def extract_info(self, url, download=True, ie_key=None, info_dict=None, extra_info={},
962 process=True, force_generic_extractor=False):
963 '''
964 Returns a list with a dictionary for each video we find.
965 If 'download', also downloads the videos.
966 extra_info is a dict containing the extra values to add to each result
967 '''
968
969 if not ie_key and force_generic_extractor:
970 ie_key = 'Generic'
971
972 if ie_key:
973 ies = [self.get_info_extractor(ie_key)]
974 else:
975 ies = self._ies
976
977 for ie in ies:
978 if not ie.suitable(url):
979 continue
980
981 ie_key = ie.ie_key()
982 ie = self.get_info_extractor(ie_key)
983 if not ie.working():
984 self.report_warning('The program functionality for this site has been marked as broken, '
985 'and will probably not work.')
986
987 try:
988 temp_id = str_or_none(
989 ie.extract_id(url) if callable(getattr(ie, 'extract_id', None))
990 else ie._match_id(url))
991 except (AssertionError, IndexError, AttributeError):
992 temp_id = None
993 if temp_id is not None and self.in_download_archive({'id': temp_id, 'ie_key': ie_key}):
994 self.to_screen("[%s] %s: has already been recorded in archive" % (
995 ie_key, temp_id))
996 break
997 return self.__extract_info(url, ie, download, extra_info, process, info_dict)
998 else:
999 self.report_error('no suitable InfoExtractor for URL %s' % url)
1000
1001 def __handle_extraction_exceptions(func):
1002 def wrapper(self, *args, **kwargs):
1003 try:
1004 return func(self, *args, **kwargs)
1005 except GeoRestrictedError as e:
1006 msg = e.msg
1007 if e.countries:
1008 msg += '\nThis video is available in %s.' % ', '.join(
1009 map(ISO3166Utils.short2full, e.countries))
1010 msg += '\nYou might want to use a VPN or a proxy server (with --proxy) to workaround.'
1011 self.report_error(msg)
1012 except ExtractorError as e: # An error we somewhat expected
1013 self.report_error(compat_str(e), e.format_traceback())
1014 except (MaxDownloadsReached, ExistingVideoReached, RejectedVideoReached):
1015 raise
1016 except Exception as e:
1017 if self.params.get('ignoreerrors', False):
1018 self.report_error(error_to_compat_str(e), tb=encode_compat_str(traceback.format_exc()))
1019 else:
1020 raise
1021 return wrapper
1022
1023 @__handle_extraction_exceptions
1024 def __extract_info(self, url, ie, download, extra_info, process, info_dict):
1025 ie_result = ie.extract(url)
1026 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
1027 return
1028 if isinstance(ie_result, list):
1029 # Backwards compatibility: old IE result format
1030 ie_result = {
1031 '_type': 'compat_list',
1032 'entries': ie_result,
1033 }
1034 if info_dict:
1035 if info_dict.get('id'):
1036 ie_result['id'] = info_dict['id']
1037 if info_dict.get('title'):
1038 ie_result['title'] = info_dict['title']
1039 self.add_default_extra_info(ie_result, ie, url)
1040 if process:
1041 return self.process_ie_result(ie_result, download, extra_info)
1042 else:
1043 return ie_result
1044
1045 def add_default_extra_info(self, ie_result, ie, url):
1046 self.add_extra_info(ie_result, {
1047 'extractor': ie.IE_NAME,
1048 'webpage_url': url,
1049 'webpage_url_basename': url_basename(url),
1050 'extractor_key': ie.ie_key(),
1051 })
1052
1053 def process_ie_result(self, ie_result, download=True, extra_info={}):
1054 """
1055 Take the result of the ie(may be modified) and resolve all unresolved
1056 references (URLs, playlist items).
1057
1058 It will also download the videos if 'download'.
1059 Returns the resolved ie_result.
1060 """
1061 result_type = ie_result.get('_type', 'video')
1062
1063 if result_type in ('url', 'url_transparent'):
1064 ie_result['url'] = sanitize_url(ie_result['url'])
1065 extract_flat = self.params.get('extract_flat', False)
1066 if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
1067 or extract_flat is True):
1068 self.__forced_printings(ie_result, self.prepare_filename(ie_result), incomplete=True)
1069 return ie_result
1070
1071 if result_type == 'video':
1072 self.add_extra_info(ie_result, extra_info)
1073 return self.process_video_result(ie_result, download=download)
1074 elif result_type == 'url':
1075 # We have to add extra_info to the results because it may be
1076 # contained in a playlist
1077 return self.extract_info(ie_result['url'],
1078 download, info_dict=ie_result,
1079 ie_key=ie_result.get('ie_key'),
1080 extra_info=extra_info)
1081 elif result_type == 'url_transparent':
1082 # Use the information from the embedding page
1083 info = self.extract_info(
1084 ie_result['url'], ie_key=ie_result.get('ie_key'),
1085 extra_info=extra_info, download=False, process=False)
1086
1087 # extract_info may return None when ignoreerrors is enabled and
1088 # extraction failed with an error, don't crash and return early
1089 # in this case
1090 if not info:
1091 return info
1092
1093 force_properties = dict(
1094 (k, v) for k, v in ie_result.items() if v is not None)
1095 for f in ('_type', 'url', 'id', 'extractor', 'extractor_key', 'ie_key'):
1096 if f in force_properties:
1097 del force_properties[f]
1098 new_result = info.copy()
1099 new_result.update(force_properties)
1100
1101 # Extracted info may not be a video result (i.e.
1102 # info.get('_type', 'video') != video) but rather an url or
1103 # url_transparent. In such cases outer metadata (from ie_result)
1104 # should be propagated to inner one (info). For this to happen
1105 # _type of info should be overridden with url_transparent. This
1106 # fixes issue from https://github.com/ytdl-org/youtube-dl/pull/11163.
1107 if new_result.get('_type') == 'url':
1108 new_result['_type'] = 'url_transparent'
1109
1110 return self.process_ie_result(
1111 new_result, download=download, extra_info=extra_info)
1112 elif result_type in ('playlist', 'multi_video'):
1113 # Protect from infinite recursion due to recursively nested playlists
1114 # (see https://github.com/ytdl-org/youtube-dl/issues/27833)
1115 webpage_url = ie_result['webpage_url']
1116 if webpage_url in self._playlist_urls:
1117 self.to_screen(
1118 '[download] Skipping already downloaded playlist: %s'
1119 % ie_result.get('title') or ie_result.get('id'))
1120 return
1121
1122 self._playlist_level += 1
1123 self._playlist_urls.add(webpage_url)
1124 try:
1125 return self.__process_playlist(ie_result, download)
1126 finally:
1127 self._playlist_level -= 1
1128 if not self._playlist_level:
1129 self._playlist_urls.clear()
1130 elif result_type == 'compat_list':
1131 self.report_warning(
1132 'Extractor %s returned a compat_list result. '
1133 'It needs to be updated.' % ie_result.get('extractor'))
1134
1135 def _fixup(r):
1136 self.add_extra_info(
1137 r,
1138 {
1139 'extractor': ie_result['extractor'],
1140 'webpage_url': ie_result['webpage_url'],
1141 'webpage_url_basename': url_basename(ie_result['webpage_url']),
1142 'extractor_key': ie_result['extractor_key'],
1143 }
1144 )
1145 return r
1146 ie_result['entries'] = [
1147 self.process_ie_result(_fixup(r), download, extra_info)
1148 for r in ie_result['entries']
1149 ]
1150 return ie_result
1151 else:
1152 raise Exception('Invalid result type: %s' % result_type)
1153
1154 def __process_playlist(self, ie_result, download):
1155 # We process each entry in the playlist
1156 playlist = ie_result.get('title') or ie_result.get('id')
1157 self.to_screen('[download] Downloading playlist: %s' % playlist)
1158
1159 if self.params.get('allow_playlist_files', True):
1160 ie_copy = {
1161 'playlist': playlist,
1162 'playlist_id': ie_result.get('id'),
1163 'playlist_title': ie_result.get('title'),
1164 'playlist_uploader': ie_result.get('uploader'),
1165 'playlist_uploader_id': ie_result.get('uploader_id'),
1166 'playlist_index': 0
1167 }
1168 ie_copy.update(dict(ie_result))
1169
1170 def ensure_dir_exists(path):
1171 return make_dir(path, self.report_error)
1172
1173 if self.params.get('writeinfojson', False):
1174 infofn = self.prepare_filename(ie_copy, 'pl_infojson')
1175 if not ensure_dir_exists(encodeFilename(infofn)):
1176 return
1177 if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
1178 self.to_screen('[info] Playlist metadata is already present')
1179 else:
1180 playlist_info = dict(ie_result)
1181 # playlist_info['entries'] = list(playlist_info['entries']) # Entries is a generator which shouldnot be resolved here
1182 del playlist_info['entries']
1183 self.to_screen('[info] Writing playlist metadata as JSON to: ' + infofn)
1184 try:
1185 write_json_file(self.filter_requested_info(playlist_info), infofn)
1186 except (OSError, IOError):
1187 self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
1188
1189 if self.params.get('writedescription', False):
1190 descfn = self.prepare_filename(ie_copy, 'pl_description')
1191 if not ensure_dir_exists(encodeFilename(descfn)):
1192 return
1193 if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
1194 self.to_screen('[info] Playlist description is already present')
1195 elif ie_result.get('description') is None:
1196 self.report_warning('There\'s no playlist description to write.')
1197 else:
1198 try:
1199 self.to_screen('[info] Writing playlist description to: ' + descfn)
1200 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
1201 descfile.write(ie_result['description'])
1202 except (OSError, IOError):
1203 self.report_error('Cannot write playlist description file ' + descfn)
1204 return
1205
1206 playlist_results = []
1207
1208 playliststart = self.params.get('playliststart', 1) - 1
1209 playlistend = self.params.get('playlistend')
1210 # For backwards compatibility, interpret -1 as whole list
1211 if playlistend == -1:
1212 playlistend = None
1213
1214 playlistitems_str = self.params.get('playlist_items')
1215 playlistitems = None
1216 if playlistitems_str is not None:
1217 def iter_playlistitems(format):
1218 for string_segment in format.split(','):
1219 if '-' in string_segment:
1220 start, end = string_segment.split('-')
1221 for item in range(int(start), int(end) + 1):
1222 yield int(item)
1223 else:
1224 yield int(string_segment)
1225 playlistitems = orderedSet(iter_playlistitems(playlistitems_str))
1226
1227 ie_entries = ie_result['entries']
1228
1229 def make_playlistitems_entries(list_ie_entries):
1230 num_entries = len(list_ie_entries)
1231 return [
1232 list_ie_entries[i - 1] for i in playlistitems
1233 if -num_entries <= i - 1 < num_entries]
1234
1235 def report_download(num_entries):
1236 self.to_screen(
1237 '[%s] playlist %s: Downloading %d videos' %
1238 (ie_result['extractor'], playlist, num_entries))
1239
1240 if isinstance(ie_entries, list):
1241 n_all_entries = len(ie_entries)
1242 if playlistitems:
1243 entries = make_playlistitems_entries(ie_entries)
1244 else:
1245 entries = ie_entries[playliststart:playlistend]
1246 n_entries = len(entries)
1247 self.to_screen(
1248 '[%s] playlist %s: Collected %d video ids (downloading %d of them)' %
1249 (ie_result['extractor'], playlist, n_all_entries, n_entries))
1250 elif isinstance(ie_entries, PagedList):
1251 if playlistitems:
1252 entries = []
1253 for item in playlistitems:
1254 entries.extend(ie_entries.getslice(
1255 item - 1, item
1256 ))
1257 else:
1258 entries = ie_entries.getslice(
1259 playliststart, playlistend)
1260 n_entries = len(entries)
1261 report_download(n_entries)
1262 else: # iterable
1263 if playlistitems:
1264 entries = make_playlistitems_entries(list(itertools.islice(
1265 ie_entries, 0, max(playlistitems))))
1266 else:
1267 entries = list(itertools.islice(
1268 ie_entries, playliststart, playlistend))
1269 n_entries = len(entries)
1270 report_download(n_entries)
1271
1272 if self.params.get('playlistreverse', False):
1273 entries = entries[::-1]
1274
1275 if self.params.get('playlistrandom', False):
1276 random.shuffle(entries)
1277
1278 x_forwarded_for = ie_result.get('__x_forwarded_for_ip')
1279
1280 for i, entry in enumerate(entries, 1):
1281 self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
1282 # This __x_forwarded_for_ip thing is a bit ugly but requires
1283 # minimal changes
1284 if x_forwarded_for:
1285 entry['__x_forwarded_for_ip'] = x_forwarded_for
1286 extra = {
1287 'n_entries': n_entries,
1288 'playlist': playlist,
1289 'playlist_id': ie_result.get('id'),
1290 'playlist_title': ie_result.get('title'),
1291 'playlist_uploader': ie_result.get('uploader'),
1292 'playlist_uploader_id': ie_result.get('uploader_id'),
1293 'playlist_index': playlistitems[i - 1] if playlistitems else i + playliststart,
1294 'extractor': ie_result['extractor'],
1295 'webpage_url': ie_result['webpage_url'],
1296 'webpage_url_basename': url_basename(ie_result['webpage_url']),
1297 'extractor_key': ie_result['extractor_key'],
1298 }
1299
1300 if self._match_entry(entry, incomplete=True) is not None:
1301 continue
1302
1303 entry_result = self.__process_iterable_entry(entry, download, extra)
1304 # TODO: skip failed (empty) entries?
1305 playlist_results.append(entry_result)
1306 ie_result['entries'] = playlist_results
1307 self.to_screen('[download] Finished downloading playlist: %s' % playlist)
1308 return ie_result
1309
1310 @__handle_extraction_exceptions
1311 def __process_iterable_entry(self, entry, download, extra_info):
1312 return self.process_ie_result(
1313 entry, download=download, extra_info=extra_info)
1314
1315 def _build_format_filter(self, filter_spec):
1316 " Returns a function to filter the formats according to the filter_spec "
1317
1318 OPERATORS = {
1319 '<': operator.lt,
1320 '<=': operator.le,
1321 '>': operator.gt,
1322 '>=': operator.ge,
1323 '=': operator.eq,
1324 '!=': operator.ne,
1325 }
1326 operator_rex = re.compile(r'''(?x)\s*
1327 (?P<key>width|height|tbr|abr|vbr|asr|filesize|filesize_approx|fps)
1328 \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
1329 (?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
1330 $
1331 ''' % '|'.join(map(re.escape, OPERATORS.keys())))
1332 m = operator_rex.search(filter_spec)
1333 if m:
1334 try:
1335 comparison_value = int(m.group('value'))
1336 except ValueError:
1337 comparison_value = parse_filesize(m.group('value'))
1338 if comparison_value is None:
1339 comparison_value = parse_filesize(m.group('value') + 'B')
1340 if comparison_value is None:
1341 raise ValueError(
1342 'Invalid value %r in format specification %r' % (
1343 m.group('value'), filter_spec))
1344 op = OPERATORS[m.group('op')]
1345
1346 if not m:
1347 STR_OPERATORS = {
1348 '=': operator.eq,
1349 '^=': lambda attr, value: attr.startswith(value),
1350 '$=': lambda attr, value: attr.endswith(value),
1351 '*=': lambda attr, value: value in attr,
1352 }
1353 str_operator_rex = re.compile(r'''(?x)
1354 \s*(?P<key>[a-zA-Z0-9._-]+)
1355 \s*(?P<negation>!\s*)?(?P<op>%s)(?P<none_inclusive>\s*\?)?
1356 \s*(?P<value>[a-zA-Z0-9._-]+)
1357 \s*$
1358 ''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
1359 m = str_operator_rex.search(filter_spec)
1360 if m:
1361 comparison_value = m.group('value')
1362 str_op = STR_OPERATORS[m.group('op')]
1363 if m.group('negation'):
1364 op = lambda attr, value: not str_op(attr, value)
1365 else:
1366 op = str_op
1367
1368 if not m:
1369 raise ValueError('Invalid filter specification %r' % filter_spec)
1370
1371 def _filter(f):
1372 actual_value = f.get(m.group('key'))
1373 if actual_value is None:
1374 return m.group('none_inclusive')
1375 return op(actual_value, comparison_value)
1376 return _filter
1377
1378 def _default_format_spec(self, info_dict, download=True):
1379
1380 def can_merge():
1381 merger = FFmpegMergerPP(self)
1382 return merger.available and merger.can_merge()
1383
1384 prefer_best = (
1385 not self.params.get('simulate', False)
1386 and download
1387 and (
1388 not can_merge()
1389 or info_dict.get('is_live', False)
1390 or self.outtmpl_dict['default'] == '-'))
1391
1392 return (
1393 'best/bestvideo+bestaudio'
1394 if prefer_best
1395 else 'bestvideo*+bestaudio/best'
1396 if not self.params.get('allow_multiple_audio_streams', False)
1397 else 'bestvideo+bestaudio/best')
1398
1399 def build_format_selector(self, format_spec):
1400 def syntax_error(note, start):
1401 message = (
1402 'Invalid format specification: '
1403 '{0}\n\t{1}\n\t{2}^'.format(note, format_spec, ' ' * start[1]))
1404 return SyntaxError(message)
1405
1406 PICKFIRST = 'PICKFIRST'
1407 MERGE = 'MERGE'
1408 SINGLE = 'SINGLE'
1409 GROUP = 'GROUP'
1410 FormatSelector = collections.namedtuple('FormatSelector', ['type', 'selector', 'filters'])
1411
1412 allow_multiple_streams = {'audio': self.params.get('allow_multiple_audio_streams', False),
1413 'video': self.params.get('allow_multiple_video_streams', False)}
1414
1415 def _parse_filter(tokens):
1416 filter_parts = []
1417 for type, string, start, _, _ in tokens:
1418 if type == tokenize.OP and string == ']':
1419 return ''.join(filter_parts)
1420 else:
1421 filter_parts.append(string)
1422
1423 def _remove_unused_ops(tokens):
1424 # Remove operators that we don't use and join them with the surrounding strings
1425 # for example: 'mp4' '-' 'baseline' '-' '16x9' is converted to 'mp4-baseline-16x9'
1426 ALLOWED_OPS = ('/', '+', ',', '(', ')')
1427 last_string, last_start, last_end, last_line = None, None, None, None
1428 for type, string, start, end, line in tokens:
1429 if type == tokenize.OP and string == '[':
1430 if last_string:
1431 yield tokenize.NAME, last_string, last_start, last_end, last_line
1432 last_string = None
1433 yield type, string, start, end, line
1434 # everything inside brackets will be handled by _parse_filter
1435 for type, string, start, end, line in tokens:
1436 yield type, string, start, end, line
1437 if type == tokenize.OP and string == ']':
1438 break
1439 elif type == tokenize.OP and string in ALLOWED_OPS:
1440 if last_string:
1441 yield tokenize.NAME, last_string, last_start, last_end, last_line
1442 last_string = None
1443 yield type, string, start, end, line
1444 elif type in [tokenize.NAME, tokenize.NUMBER, tokenize.OP]:
1445 if not last_string:
1446 last_string = string
1447 last_start = start
1448 last_end = end
1449 else:
1450 last_string += string
1451 if last_string:
1452 yield tokenize.NAME, last_string, last_start, last_end, last_line
1453
1454 def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, inside_group=False):
1455 selectors = []
1456 current_selector = None
1457 for type, string, start, _, _ in tokens:
1458 # ENCODING is only defined in python 3.x
1459 if type == getattr(tokenize, 'ENCODING', None):
1460 continue
1461 elif type in [tokenize.NAME, tokenize.NUMBER]:
1462 current_selector = FormatSelector(SINGLE, string, [])
1463 elif type == tokenize.OP:
1464 if string == ')':
1465 if not inside_group:
1466 # ')' will be handled by the parentheses group
1467 tokens.restore_last_token()
1468 break
1469 elif inside_merge and string in ['/', ',']:
1470 tokens.restore_last_token()
1471 break
1472 elif inside_choice and string == ',':
1473 tokens.restore_last_token()
1474 break
1475 elif string == ',':
1476 if not current_selector:
1477 raise syntax_error('"," must follow a format selector', start)
1478 selectors.append(current_selector)
1479 current_selector = None
1480 elif string == '/':
1481 if not current_selector:
1482 raise syntax_error('"/" must follow a format selector', start)
1483 first_choice = current_selector
1484 second_choice = _parse_format_selection(tokens, inside_choice=True)
1485 current_selector = FormatSelector(PICKFIRST, (first_choice, second_choice), [])
1486 elif string == '[':
1487 if not current_selector:
1488 current_selector = FormatSelector(SINGLE, 'best', [])
1489 format_filter = _parse_filter(tokens)
1490 current_selector.filters.append(format_filter)
1491 elif string == '(':
1492 if current_selector:
1493 raise syntax_error('Unexpected "("', start)
1494 group = _parse_format_selection(tokens, inside_group=True)
1495 current_selector = FormatSelector(GROUP, group, [])
1496 elif string == '+':
1497 if not current_selector:
1498 raise syntax_error('Unexpected "+"', start)
1499 selector_1 = current_selector
1500 selector_2 = _parse_format_selection(tokens, inside_merge=True)
1501 if not selector_2:
1502 raise syntax_error('Expected a selector', start)
1503 current_selector = FormatSelector(MERGE, (selector_1, selector_2), [])
1504 else:
1505 raise syntax_error('Operator not recognized: "{0}"'.format(string), start)
1506 elif type == tokenize.ENDMARKER:
1507 break
1508 if current_selector:
1509 selectors.append(current_selector)
1510 return selectors
1511
1512 def _build_selector_function(selector):
1513 if isinstance(selector, list): # ,
1514 fs = [_build_selector_function(s) for s in selector]
1515
1516 def selector_function(ctx):
1517 for f in fs:
1518 for format in f(ctx):
1519 yield format
1520 return selector_function
1521
1522 elif selector.type == GROUP: # ()
1523 selector_function = _build_selector_function(selector.selector)
1524
1525 elif selector.type == PICKFIRST: # /
1526 fs = [_build_selector_function(s) for s in selector.selector]
1527
1528 def selector_function(ctx):
1529 for f in fs:
1530 picked_formats = list(f(ctx))
1531 if picked_formats:
1532 return picked_formats
1533 return []
1534
1535 elif selector.type == SINGLE: # atom
1536 format_spec = selector.selector if selector.selector is not None else 'best'
1537
1538 if format_spec == 'all':
1539 def selector_function(ctx):
1540 formats = list(ctx['formats'])
1541 if formats:
1542 for f in formats:
1543 yield f
1544
1545 else:
1546 format_fallback = False
1547 format_spec_obj = re.match(r'(best|worst|b|w)(video|audio|v|a)?(\*)?$', format_spec)
1548 if format_spec_obj is not None:
1549 format_idx = 0 if format_spec_obj.group(1)[0] == 'w' else -1
1550 format_type = format_spec_obj.group(2)[0] if format_spec_obj.group(2) else False
1551 not_format_type = 'v' if format_type == 'a' else 'a'
1552 format_modified = format_spec_obj.group(3) is not None
1553
1554 format_fallback = not format_type and not format_modified # for b, w
1555 filter_f = ((lambda f: f.get(format_type + 'codec') != 'none')
1556 if format_type and format_modified # bv*, ba*, wv*, wa*
1557 else (lambda f: f.get(not_format_type + 'codec') == 'none')
1558 if format_type # bv, ba, wv, wa
1559 else (lambda f: f.get('vcodec') != 'none' and f.get('acodec') != 'none')
1560 if not format_modified # b, w
1561 else None) # b*, w*
1562 else:
1563 format_idx = -1
1564 filter_f = ((lambda f: f.get('ext') == format_spec)
1565 if format_spec in ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav'] # extension
1566 else (lambda f: f.get('format_id') == format_spec)) # id
1567
1568 def selector_function(ctx):
1569 formats = list(ctx['formats'])
1570 if not formats:
1571 return
1572 matches = list(filter(filter_f, formats)) if filter_f is not None else formats
1573 if matches:
1574 yield matches[format_idx]
1575 elif format_fallback == 'force' or (format_fallback and ctx['incomplete_formats']):
1576 # for extractors with incomplete formats (audio only (soundcloud)
1577 # or video only (imgur)) best/worst will fallback to
1578 # best/worst {video,audio}-only format
1579 yield formats[format_idx]
1580
1581 elif selector.type == MERGE: # +
1582 def _merge(formats_pair):
1583 format_1, format_2 = formats_pair
1584
1585 formats_info = []
1586 formats_info.extend(format_1.get('requested_formats', (format_1,)))
1587 formats_info.extend(format_2.get('requested_formats', (format_2,)))
1588
1589 if not allow_multiple_streams['video'] or not allow_multiple_streams['audio']:
1590 get_no_more = {"video": False, "audio": False}
1591 for (i, fmt_info) in enumerate(formats_info):
1592 for aud_vid in ["audio", "video"]:
1593 if not allow_multiple_streams[aud_vid] and fmt_info.get(aud_vid[0] + 'codec') != 'none':
1594 if get_no_more[aud_vid]:
1595 formats_info.pop(i)
1596 get_no_more[aud_vid] = True
1597
1598 if len(formats_info) == 1:
1599 return formats_info[0]
1600
1601 video_fmts = [fmt_info for fmt_info in formats_info if fmt_info.get('vcodec') != 'none']
1602 audio_fmts = [fmt_info for fmt_info in formats_info if fmt_info.get('acodec') != 'none']
1603
1604 the_only_video = video_fmts[0] if len(video_fmts) == 1 else None
1605 the_only_audio = audio_fmts[0] if len(audio_fmts) == 1 else None
1606
1607 output_ext = self.params.get('merge_output_format')
1608 if not output_ext:
1609 if the_only_video:
1610 output_ext = the_only_video['ext']
1611 elif the_only_audio and not video_fmts:
1612 output_ext = the_only_audio['ext']
1613 else:
1614 output_ext = 'mkv'
1615
1616 new_dict = {
1617 'requested_formats': formats_info,
1618 'format': '+'.join(fmt_info.get('format') for fmt_info in formats_info),
1619 'format_id': '+'.join(fmt_info.get('format_id') for fmt_info in formats_info),
1620 'ext': output_ext,
1621 }
1622
1623 if the_only_video:
1624 new_dict.update({
1625 'width': the_only_video.get('width'),
1626 'height': the_only_video.get('height'),
1627 'resolution': the_only_video.get('resolution'),
1628 'fps': the_only_video.get('fps'),
1629 'vcodec': the_only_video.get('vcodec'),
1630 'vbr': the_only_video.get('vbr'),
1631 'stretched_ratio': the_only_video.get('stretched_ratio'),
1632 })
1633
1634 if the_only_audio:
1635 new_dict.update({
1636 'acodec': the_only_audio.get('acodec'),
1637 'abr': the_only_audio.get('abr'),
1638 })
1639
1640 return new_dict
1641
1642 selector_1, selector_2 = map(_build_selector_function, selector.selector)
1643
1644 def selector_function(ctx):
1645 for pair in itertools.product(
1646 selector_1(copy.deepcopy(ctx)), selector_2(copy.deepcopy(ctx))):
1647 yield _merge(pair)
1648
1649 filters = [self._build_format_filter(f) for f in selector.filters]
1650
1651 def final_selector(ctx):
1652 ctx_copy = copy.deepcopy(ctx)
1653 for _filter in filters:
1654 ctx_copy['formats'] = list(filter(_filter, ctx_copy['formats']))
1655 return selector_function(ctx_copy)
1656 return final_selector
1657
1658 stream = io.BytesIO(format_spec.encode('utf-8'))
1659 try:
1660 tokens = list(_remove_unused_ops(compat_tokenize_tokenize(stream.readline)))
1661 except tokenize.TokenError:
1662 raise syntax_error('Missing closing/opening brackets or parenthesis', (0, len(format_spec)))
1663
1664 class TokenIterator(object):
1665 def __init__(self, tokens):
1666 self.tokens = tokens
1667 self.counter = 0
1668
1669 def __iter__(self):
1670 return self
1671
1672 def __next__(self):
1673 if self.counter >= len(self.tokens):
1674 raise StopIteration()
1675 value = self.tokens[self.counter]
1676 self.counter += 1
1677 return value
1678
1679 next = __next__
1680
1681 def restore_last_token(self):
1682 self.counter -= 1
1683
1684 parsed_selector = _parse_format_selection(iter(TokenIterator(tokens)))
1685 return _build_selector_function(parsed_selector)
1686
1687 def _calc_headers(self, info_dict):
1688 res = std_headers.copy()
1689
1690 add_headers = info_dict.get('http_headers')
1691 if add_headers:
1692 res.update(add_headers)
1693
1694 cookies = self._calc_cookies(info_dict)
1695 if cookies:
1696 res['Cookie'] = cookies
1697
1698 if 'X-Forwarded-For' not in res:
1699 x_forwarded_for_ip = info_dict.get('__x_forwarded_for_ip')
1700 if x_forwarded_for_ip:
1701 res['X-Forwarded-For'] = x_forwarded_for_ip
1702
1703 return res
1704
1705 def _calc_cookies(self, info_dict):
1706 pr = sanitized_Request(info_dict['url'])
1707 self.cookiejar.add_cookie_header(pr)
1708 return pr.get_header('Cookie')
1709
1710 def process_video_result(self, info_dict, download=True):
1711 assert info_dict.get('_type', 'video') == 'video'
1712
1713 if 'id' not in info_dict:
1714 raise ExtractorError('Missing "id" field in extractor result')
1715 if 'title' not in info_dict:
1716 raise ExtractorError('Missing "title" field in extractor result')
1717
1718 def report_force_conversion(field, field_not, conversion):
1719 self.report_warning(
1720 '"%s" field is not %s - forcing %s conversion, there is an error in extractor'
1721 % (field, field_not, conversion))
1722
1723 def sanitize_string_field(info, string_field):
1724 field = info.get(string_field)
1725 if field is None or isinstance(field, compat_str):
1726 return
1727 report_force_conversion(string_field, 'a string', 'string')
1728 info[string_field] = compat_str(field)
1729
1730 def sanitize_numeric_fields(info):
1731 for numeric_field in self._NUMERIC_FIELDS:
1732 field = info.get(numeric_field)
1733 if field is None or isinstance(field, compat_numeric_types):
1734 continue
1735 report_force_conversion(numeric_field, 'numeric', 'int')
1736 info[numeric_field] = int_or_none(field)
1737
1738 sanitize_string_field(info_dict, 'id')
1739 sanitize_numeric_fields(info_dict)
1740
1741 if 'playlist' not in info_dict:
1742 # It isn't part of a playlist
1743 info_dict['playlist'] = None
1744 info_dict['playlist_index'] = None
1745
1746 thumbnails = info_dict.get('thumbnails')
1747 if thumbnails is None:
1748 thumbnail = info_dict.get('thumbnail')
1749 if thumbnail:
1750 info_dict['thumbnails'] = thumbnails = [{'url': thumbnail}]
1751 if thumbnails:
1752 thumbnails.sort(key=lambda t: (
1753 t.get('preference') if t.get('preference') is not None else -1,
1754 t.get('width') if t.get('width') is not None else -1,
1755 t.get('height') if t.get('height') is not None else -1,
1756 t.get('id') if t.get('id') is not None else '', t.get('url')))
1757 for i, t in enumerate(thumbnails):
1758 t['url'] = sanitize_url(t['url'])
1759 if t.get('width') and t.get('height'):
1760 t['resolution'] = '%dx%d' % (t['width'], t['height'])
1761 if t.get('id') is None:
1762 t['id'] = '%d' % i
1763
1764 if self.params.get('list_thumbnails'):
1765 self.list_thumbnails(info_dict)
1766 return
1767
1768 thumbnail = info_dict.get('thumbnail')
1769 if thumbnail:
1770 info_dict['thumbnail'] = sanitize_url(thumbnail)
1771 elif thumbnails:
1772 info_dict['thumbnail'] = thumbnails[-1]['url']
1773
1774 if 'display_id' not in info_dict and 'id' in info_dict:
1775 info_dict['display_id'] = info_dict['id']
1776
1777 if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
1778 # Working around out-of-range timestamp values (e.g. negative ones on Windows,
1779 # see http://bugs.python.org/issue1646728)
1780 try:
1781 upload_date = datetime.datetime.utcfromtimestamp(info_dict['timestamp'])
1782 info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
1783 except (ValueError, OverflowError, OSError):
1784 pass
1785
1786 # Auto generate title fields corresponding to the *_number fields when missing
1787 # in order to always have clean titles. This is very common for TV series.
1788 for field in ('chapter', 'season', 'episode'):
1789 if info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
1790 info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field])
1791
1792 for cc_kind in ('subtitles', 'automatic_captions'):
1793 cc = info_dict.get(cc_kind)
1794 if cc:
1795 for _, subtitle in cc.items():
1796 for subtitle_format in subtitle:
1797 if subtitle_format.get('url'):
1798 subtitle_format['url'] = sanitize_url(subtitle_format['url'])
1799 if subtitle_format.get('ext') is None:
1800 subtitle_format['ext'] = determine_ext(subtitle_format['url']).lower()
1801
1802 automatic_captions = info_dict.get('automatic_captions')
1803 subtitles = info_dict.get('subtitles')
1804
1805 if self.params.get('listsubtitles', False):
1806 if 'automatic_captions' in info_dict:
1807 self.list_subtitles(
1808 info_dict['id'], automatic_captions, 'automatic captions')
1809 self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
1810 return
1811
1812 info_dict['requested_subtitles'] = self.process_subtitles(
1813 info_dict['id'], subtitles, automatic_captions)
1814
1815 # We now pick which formats have to be downloaded
1816 if info_dict.get('formats') is None:
1817 # There's only one format available
1818 formats = [info_dict]
1819 else:
1820 formats = info_dict['formats']
1821
1822 if not formats:
1823 raise ExtractorError('No video formats found!')
1824
1825 def is_wellformed(f):
1826 url = f.get('url')
1827 if not url:
1828 self.report_warning(
1829 '"url" field is missing or empty - skipping format, '
1830 'there is an error in extractor')
1831 return False
1832 if isinstance(url, bytes):
1833 sanitize_string_field(f, 'url')
1834 return True
1835
1836 # Filter out malformed formats for better extraction robustness
1837 formats = list(filter(is_wellformed, formats))
1838
1839 formats_dict = {}
1840
1841 # We check that all the formats have the format and format_id fields
1842 for i, format in enumerate(formats):
1843 sanitize_string_field(format, 'format_id')
1844 sanitize_numeric_fields(format)
1845 format['url'] = sanitize_url(format['url'])
1846 if not format.get('format_id'):
1847 format['format_id'] = compat_str(i)
1848 else:
1849 # Sanitize format_id from characters used in format selector expression
1850 format['format_id'] = re.sub(r'[\s,/+\[\]()]', '_', format['format_id'])
1851 format_id = format['format_id']
1852 if format_id not in formats_dict:
1853 formats_dict[format_id] = []
1854 formats_dict[format_id].append(format)
1855
1856 # Make sure all formats have unique format_id
1857 for format_id, ambiguous_formats in formats_dict.items():
1858 if len(ambiguous_formats) > 1:
1859 for i, format in enumerate(ambiguous_formats):
1860 format['format_id'] = '%s-%d' % (format_id, i)
1861
1862 for i, format in enumerate(formats):
1863 if format.get('format') is None:
1864 format['format'] = '{id} - {res}{note}'.format(
1865 id=format['format_id'],
1866 res=self.format_resolution(format),
1867 note=' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
1868 )
1869 # Automatically determine file extension if missing
1870 if format.get('ext') is None:
1871 format['ext'] = determine_ext(format['url']).lower()
1872 # Automatically determine protocol if missing (useful for format
1873 # selection purposes)
1874 if format.get('protocol') is None:
1875 format['protocol'] = determine_protocol(format)
1876 # Add HTTP headers, so that external programs can use them from the
1877 # json output
1878 full_format_info = info_dict.copy()
1879 full_format_info.update(format)
1880 format['http_headers'] = self._calc_headers(full_format_info)
1881 # Remove private housekeeping stuff
1882 if '__x_forwarded_for_ip' in info_dict:
1883 del info_dict['__x_forwarded_for_ip']
1884
1885 # TODO Central sorting goes here
1886
1887 if formats[0] is not info_dict:
1888 # only set the 'formats' fields if the original info_dict list them
1889 # otherwise we end up with a circular reference, the first (and unique)
1890 # element in the 'formats' field in info_dict is info_dict itself,
1891 # which can't be exported to json
1892 info_dict['formats'] = formats
1893 if self.params.get('listformats'):
1894 self.list_formats(info_dict)
1895 return
1896
1897 req_format = self.params.get('format')
1898 if req_format is None:
1899 req_format = self._default_format_spec(info_dict, download=download)
1900 if self.params.get('verbose'):
1901 self.to_screen('[debug] Default format spec: %s' % req_format)
1902
1903 format_selector = self.build_format_selector(req_format)
1904
1905 # While in format selection we may need to have an access to the original
1906 # format set in order to calculate some metrics or do some processing.
1907 # For now we need to be able to guess whether original formats provided
1908 # by extractor are incomplete or not (i.e. whether extractor provides only
1909 # video-only or audio-only formats) for proper formats selection for
1910 # extractors with such incomplete formats (see
1911 # https://github.com/ytdl-org/youtube-dl/pull/5556).
1912 # Since formats may be filtered during format selection and may not match
1913 # the original formats the results may be incorrect. Thus original formats
1914 # or pre-calculated metrics should be passed to format selection routines
1915 # as well.
1916 # We will pass a context object containing all necessary additional data
1917 # instead of just formats.
1918 # This fixes incorrect format selection issue (see
1919 # https://github.com/ytdl-org/youtube-dl/issues/10083).
1920 incomplete_formats = (
1921 # All formats are video-only or
1922 all(f.get('vcodec') != 'none' and f.get('acodec') == 'none' for f in formats)
1923 # all formats are audio-only
1924 or all(f.get('vcodec') == 'none' and f.get('acodec') != 'none' for f in formats))
1925
1926 ctx = {
1927 'formats': formats,
1928 'incomplete_formats': incomplete_formats,
1929 }
1930
1931 formats_to_download = list(format_selector(ctx))
1932 if not formats_to_download:
1933 raise ExtractorError('requested format not available',
1934 expected=True)
1935
1936 if download:
1937 self.to_screen('[info] Downloading format(s) %s' % ", ".join([f['format_id'] for f in formats_to_download]))
1938 if len(formats_to_download) > 1:
1939 self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
1940 for format in formats_to_download:
1941 new_info = dict(info_dict)
1942 new_info.update(format)
1943 self.process_info(new_info)
1944 # We update the info dict with the best quality format (backwards compatibility)
1945 info_dict.update(formats_to_download[-1])
1946 return info_dict
1947
1948 def process_subtitles(self, video_id, normal_subtitles, automatic_captions):
1949 """Select the requested subtitles and their format"""
1950 available_subs = {}
1951 if normal_subtitles and self.params.get('writesubtitles'):
1952 available_subs.update(normal_subtitles)
1953 if automatic_captions and self.params.get('writeautomaticsub'):
1954 for lang, cap_info in automatic_captions.items():
1955 if lang not in available_subs:
1956 available_subs[lang] = cap_info
1957
1958 if (not self.params.get('writesubtitles') and not
1959 self.params.get('writeautomaticsub') or not
1960 available_subs):
1961 return None
1962
1963 if self.params.get('allsubtitles', False):
1964 requested_langs = available_subs.keys()
1965 else:
1966 if self.params.get('subtitleslangs', False):
1967 requested_langs = self.params.get('subtitleslangs')
1968 elif 'en' in available_subs:
1969 requested_langs = ['en']
1970 else:
1971 requested_langs = [list(available_subs.keys())[0]]
1972
1973 formats_query = self.params.get('subtitlesformat', 'best')
1974 formats_preference = formats_query.split('/') if formats_query else []
1975 subs = {}
1976 for lang in requested_langs:
1977 formats = available_subs.get(lang)
1978 if formats is None:
1979 self.report_warning('%s subtitles not available for %s' % (lang, video_id))
1980 continue
1981 for ext in formats_preference:
1982 if ext == 'best':
1983 f = formats[-1]
1984 break
1985 matches = list(filter(lambda f: f['ext'] == ext, formats))
1986 if matches:
1987 f = matches[-1]
1988 break
1989 else:
1990 f = formats[-1]
1991 self.report_warning(
1992 'No subtitle format found matching "%s" for language %s, '
1993 'using %s' % (formats_query, lang, f['ext']))
1994 subs[lang] = f
1995 return subs
1996
1997 def __forced_printings(self, info_dict, filename, incomplete):
1998 def print_mandatory(field):
1999 if (self.params.get('force%s' % field, False)
2000 and (not incomplete or info_dict.get(field) is not None)):
2001 self.to_stdout(info_dict[field])
2002
2003 def print_optional(field):
2004 if (self.params.get('force%s' % field, False)
2005 and info_dict.get(field) is not None):
2006 self.to_stdout(info_dict[field])
2007
2008 print_mandatory('title')
2009 print_mandatory('id')
2010 if self.params.get('forceurl', False) and not incomplete:
2011 if info_dict.get('requested_formats') is not None:
2012 for f in info_dict['requested_formats']:
2013 self.to_stdout(f['url'] + f.get('play_path', ''))
2014 else:
2015 # For RTMP URLs, also include the playpath
2016 self.to_stdout(info_dict['url'] + info_dict.get('play_path', ''))
2017 print_optional('thumbnail')
2018 print_optional('description')
2019 if self.params.get('forcefilename', False) and filename is not None:
2020 self.to_stdout(filename)
2021 if self.params.get('forceduration', False) and info_dict.get('duration') is not None:
2022 self.to_stdout(formatSeconds(info_dict['duration']))
2023 print_mandatory('format')
2024 if self.params.get('forcejson', False):
2025 self.to_stdout(json.dumps(info_dict))
2026
2027 def process_info(self, info_dict):
2028 """Process a single resolved IE result."""
2029
2030 assert info_dict.get('_type', 'video') == 'video'
2031
2032 info_dict.setdefault('__postprocessors', [])
2033
2034 max_downloads = self.params.get('max_downloads')
2035 if max_downloads is not None:
2036 if self._num_downloads >= int(max_downloads):
2037 raise MaxDownloadsReached()
2038
2039 # TODO: backward compatibility, to be removed
2040 info_dict['fulltitle'] = info_dict['title']
2041
2042 if 'format' not in info_dict:
2043 info_dict['format'] = info_dict['ext']
2044
2045 if self._match_entry(info_dict, incomplete=False) is not None:
2046 return
2047
2048 self._num_downloads += 1
2049
2050 info_dict = self.pre_process(info_dict)
2051
2052 info_dict['_filename'] = full_filename = self.prepare_filename(info_dict, warn=True)
2053 temp_filename = self.prepare_filename(info_dict, 'temp')
2054 files_to_move = {}
2055 skip_dl = self.params.get('skip_download', False)
2056
2057 # Forced printings
2058 self.__forced_printings(info_dict, full_filename, incomplete=False)
2059
2060 if self.params.get('simulate', False):
2061 if self.params.get('force_write_download_archive', False):
2062 self.record_download_archive(info_dict)
2063
2064 # Do nothing else if in simulate mode
2065 return
2066
2067 if full_filename is None:
2068 return
2069
2070 def ensure_dir_exists(path):
2071 return make_dir(path, self.report_error)
2072
2073 if not ensure_dir_exists(encodeFilename(full_filename)):
2074 return
2075 if not ensure_dir_exists(encodeFilename(temp_filename)):
2076 return
2077
2078 if self.params.get('writedescription', False):
2079 descfn = self.prepare_filename(info_dict, 'description')
2080 if not ensure_dir_exists(encodeFilename(descfn)):
2081 return
2082 if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
2083 self.to_screen('[info] Video description is already present')
2084 elif info_dict.get('description') is None:
2085 self.report_warning('There\'s no description to write.')
2086 else:
2087 try:
2088 self.to_screen('[info] Writing video description to: ' + descfn)
2089 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
2090 descfile.write(info_dict['description'])
2091 except (OSError, IOError):
2092 self.report_error('Cannot write description file ' + descfn)
2093 return
2094
2095 if self.params.get('writeannotations', False):
2096 annofn = self.prepare_filename(info_dict, 'annotation')
2097 if not ensure_dir_exists(encodeFilename(annofn)):
2098 return
2099 if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
2100 self.to_screen('[info] Video annotations are already present')
2101 elif not info_dict.get('annotations'):
2102 self.report_warning('There are no annotations to write.')
2103 else:
2104 try:
2105 self.to_screen('[info] Writing video annotations to: ' + annofn)
2106 with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
2107 annofile.write(info_dict['annotations'])
2108 except (KeyError, TypeError):
2109 self.report_warning('There are no annotations to write.')
2110 except (OSError, IOError):
2111 self.report_error('Cannot write annotations file: ' + annofn)
2112 return
2113
2114 def dl(name, info, subtitle=False):
2115 fd = get_suitable_downloader(info, self.params)(self, self.params)
2116 for ph in self._progress_hooks:
2117 fd.add_progress_hook(ph)
2118 if self.params.get('verbose'):
2119 self.to_screen('[debug] Invoking downloader on %r' % info.get('url'))
2120 return fd.download(name, info, subtitle)
2121
2122 subtitles_are_requested = any([self.params.get('writesubtitles', False),
2123 self.params.get('writeautomaticsub')])
2124
2125 if subtitles_are_requested and info_dict.get('requested_subtitles'):
2126 # subtitles download errors are already managed as troubles in relevant IE
2127 # that way it will silently go on when used with unsupporting IE
2128 subtitles = info_dict['requested_subtitles']
2129 # ie = self.get_info_extractor(info_dict['extractor_key'])
2130 for sub_lang, sub_info in subtitles.items():
2131 sub_format = sub_info['ext']
2132 sub_fn = self.prepare_filename(info_dict, 'subtitle')
2133 sub_filename = subtitles_filename(
2134 temp_filename if not skip_dl else sub_fn,
2135 sub_lang, sub_format, info_dict.get('ext'))
2136 sub_filename_final = subtitles_filename(sub_fn, sub_lang, sub_format, info_dict.get('ext'))
2137 if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
2138 self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
2139 files_to_move[sub_filename] = sub_filename_final
2140 else:
2141 self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
2142 if sub_info.get('data') is not None:
2143 try:
2144 # Use newline='' to prevent conversion of newline characters
2145 # See https://github.com/ytdl-org/youtube-dl/issues/10268
2146 with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8', newline='') as subfile:
2147 subfile.write(sub_info['data'])
2148 files_to_move[sub_filename] = sub_filename_final
2149 except (OSError, IOError):
2150 self.report_error('Cannot write subtitles file ' + sub_filename)
2151 return
2152 else:
2153 try:
2154 dl(sub_filename, sub_info, subtitle=True)
2155 '''
2156 if self.params.get('sleep_interval_subtitles', False):
2157 dl(sub_filename, sub_info)
2158 else:
2159 sub_data = ie._request_webpage(
2160 sub_info['url'], info_dict['id'], note=False).read()
2161 with io.open(encodeFilename(sub_filename), 'wb') as subfile:
2162 subfile.write(sub_data)
2163 '''
2164 files_to_move[sub_filename] = sub_filename_final
2165 except (ExtractorError, IOError, OSError, ValueError, compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
2166 self.report_warning('Unable to download subtitle for "%s": %s' %
2167 (sub_lang, error_to_compat_str(err)))
2168 continue
2169
2170 if skip_dl:
2171 if self.params.get('convertsubtitles', False):
2172 # subconv = FFmpegSubtitlesConvertorPP(self, format=self.params.get('convertsubtitles'))
2173 filename_real_ext = os.path.splitext(full_filename)[1][1:]
2174 filename_wo_ext = (
2175 os.path.splitext(full_filename)[0]
2176 if filename_real_ext == info_dict['ext']
2177 else full_filename)
2178 afilename = '%s.%s' % (filename_wo_ext, self.params.get('convertsubtitles'))
2179 # if subconv.available:
2180 # info_dict['__postprocessors'].append(subconv)
2181 if os.path.exists(encodeFilename(afilename)):
2182 self.to_screen(
2183 '[download] %s has already been downloaded and '
2184 'converted' % afilename)
2185 else:
2186 try:
2187 self.post_process(full_filename, info_dict, files_to_move)
2188 except PostProcessingError as err:
2189 self.report_error('Postprocessing: %s' % str(err))
2190 return
2191
2192 if self.params.get('writeinfojson', False):
2193 infofn = self.prepare_filename(info_dict, 'infojson')
2194 if not ensure_dir_exists(encodeFilename(infofn)):
2195 return
2196 if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
2197 self.to_screen('[info] Video metadata is already present')
2198 else:
2199 self.to_screen('[info] Writing video metadata as JSON to: ' + infofn)
2200 try:
2201 write_json_file(self.filter_requested_info(info_dict), infofn)
2202 except (OSError, IOError):
2203 self.report_error('Cannot write video metadata to JSON file ' + infofn)
2204 return
2205 info_dict['__infojson_filename'] = infofn
2206
2207 thumbfn = self.prepare_filename(info_dict, 'thumbnail')
2208 thumb_fn_temp = temp_filename if not skip_dl else thumbfn
2209 for thumb_ext in self._write_thumbnails(info_dict, thumb_fn_temp):
2210 thumb_filename_temp = replace_extension(thumb_fn_temp, thumb_ext, info_dict.get('ext'))
2211 thumb_filename = replace_extension(thumbfn, thumb_ext, info_dict.get('ext'))
2212 files_to_move[thumb_filename_temp] = info_dict['__thumbnail_filename'] = thumb_filename
2213
2214 # Write internet shortcut files
2215 url_link = webloc_link = desktop_link = False
2216 if self.params.get('writelink', False):
2217 if sys.platform == "darwin": # macOS.
2218 webloc_link = True
2219 elif sys.platform.startswith("linux"):
2220 desktop_link = True
2221 else: # if sys.platform in ['win32', 'cygwin']:
2222 url_link = True
2223 if self.params.get('writeurllink', False):
2224 url_link = True
2225 if self.params.get('writewebloclink', False):
2226 webloc_link = True
2227 if self.params.get('writedesktoplink', False):
2228 desktop_link = True
2229
2230 if url_link or webloc_link or desktop_link:
2231 if 'webpage_url' not in info_dict:
2232 self.report_error('Cannot write internet shortcut file because the "webpage_url" field is missing in the media information')
2233 return
2234 ascii_url = iri_to_uri(info_dict['webpage_url'])
2235
2236 def _write_link_file(extension, template, newline, embed_filename):
2237 linkfn = replace_extension(full_filename, extension, info_dict.get('ext'))
2238 if self.params.get('overwrites', True) and os.path.exists(encodeFilename(linkfn)):
2239 self.to_screen('[info] Internet shortcut is already present')
2240 else:
2241 try:
2242 self.to_screen('[info] Writing internet shortcut to: ' + linkfn)
2243 with io.open(encodeFilename(to_high_limit_path(linkfn)), 'w', encoding='utf-8', newline=newline) as linkfile:
2244 template_vars = {'url': ascii_url}
2245 if embed_filename:
2246 template_vars['filename'] = linkfn[:-(len(extension) + 1)]
2247 linkfile.write(template % template_vars)
2248 except (OSError, IOError):
2249 self.report_error('Cannot write internet shortcut ' + linkfn)
2250 return False
2251 return True
2252
2253 if url_link:
2254 if not _write_link_file('url', DOT_URL_LINK_TEMPLATE, '\r\n', embed_filename=False):
2255 return
2256 if webloc_link:
2257 if not _write_link_file('webloc', DOT_WEBLOC_LINK_TEMPLATE, '\n', embed_filename=False):
2258 return
2259 if desktop_link:
2260 if not _write_link_file('desktop', DOT_DESKTOP_LINK_TEMPLATE, '\n', embed_filename=True):
2261 return
2262
2263 # Download
2264 must_record_download_archive = False
2265 if not skip_dl:
2266 try:
2267
2268 def existing_file(*filepaths):
2269 ext = info_dict.get('ext')
2270 final_ext = self.params.get('final_ext', ext)
2271 existing_files = []
2272 for file in orderedSet(filepaths):
2273 if final_ext != ext:
2274 converted = replace_extension(file, final_ext, ext)
2275 if os.path.exists(encodeFilename(converted)):
2276 existing_files.append(converted)
2277 if os.path.exists(encodeFilename(file)):
2278 existing_files.append(file)
2279
2280 if not existing_files or self.params.get('overwrites', False):
2281 for file in orderedSet(existing_files):
2282 self.report_file_delete(file)
2283 os.remove(encodeFilename(file))
2284 return None
2285
2286 self.report_file_already_downloaded(existing_files[0])
2287 info_dict['ext'] = os.path.splitext(existing_files[0])[1][1:]
2288 return existing_files[0]
2289
2290 success = True
2291 if info_dict.get('requested_formats') is not None:
2292 downloaded = []
2293 merger = FFmpegMergerPP(self)
2294 if not merger.available:
2295 postprocessors = []
2296 self.report_warning('You have requested multiple '
2297 'formats but ffmpeg is not installed.'
2298 ' The formats won\'t be merged.')
2299 else:
2300 postprocessors = [merger]
2301
2302 def compatible_formats(formats):
2303 # TODO: some formats actually allow this (mkv, webm, ogg, mp4), but not all of them.
2304 video_formats = [format for format in formats if format.get('vcodec') != 'none']
2305 audio_formats = [format for format in formats if format.get('acodec') != 'none']
2306 if len(video_formats) > 2 or len(audio_formats) > 2:
2307 return False
2308
2309 # Check extension
2310 exts = set(format.get('ext') for format in formats)
2311 COMPATIBLE_EXTS = (
2312 set(('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'ismv', 'isma')),
2313 set(('webm',)),
2314 )
2315 for ext_sets in COMPATIBLE_EXTS:
2316 if ext_sets.issuperset(exts):
2317 return True
2318 # TODO: Check acodec/vcodec
2319 return False
2320
2321 requested_formats = info_dict['requested_formats']
2322 old_ext = info_dict['ext']
2323 if self.params.get('merge_output_format') is None and not compatible_formats(requested_formats):
2324 info_dict['ext'] = 'mkv'
2325 self.report_warning(
2326 'Requested formats are incompatible for merge and will be merged into mkv.')
2327
2328 def correct_ext(filename):
2329 filename_real_ext = os.path.splitext(filename)[1][1:]
2330 filename_wo_ext = (
2331 os.path.splitext(filename)[0]
2332 if filename_real_ext == old_ext
2333 else filename)
2334 return '%s.%s' % (filename_wo_ext, info_dict['ext'])
2335
2336 # Ensure filename always has a correct extension for successful merge
2337 full_filename = correct_ext(full_filename)
2338 temp_filename = correct_ext(temp_filename)
2339 dl_filename = existing_file(full_filename, temp_filename)
2340 if dl_filename is None:
2341 for f in requested_formats:
2342 new_info = dict(info_dict)
2343 new_info.update(f)
2344 fname = prepend_extension(
2345 self.prepare_filename(new_info, 'temp'),
2346 'f%s' % f['format_id'], new_info['ext'])
2347 if not ensure_dir_exists(fname):
2348 return
2349 downloaded.append(fname)
2350 partial_success, real_download = dl(fname, new_info)
2351 success = success and partial_success
2352 info_dict['__postprocessors'] = postprocessors
2353 info_dict['__files_to_merge'] = downloaded
2354 # Even if there were no downloads, it is being merged only now
2355 info_dict['__real_download'] = True
2356 else:
2357 # Just a single file
2358 dl_filename = existing_file(full_filename, temp_filename)
2359 if dl_filename is None:
2360 success, real_download = dl(temp_filename, info_dict)
2361 info_dict['__real_download'] = real_download
2362
2363 dl_filename = dl_filename or temp_filename
2364 info_dict['__finaldir'] = os.path.dirname(os.path.abspath(encodeFilename(full_filename)))
2365
2366 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
2367 self.report_error('unable to download video data: %s' % error_to_compat_str(err))
2368 return
2369 except (OSError, IOError) as err:
2370 raise UnavailableVideoError(err)
2371 except (ContentTooShortError, ) as err:
2372 self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
2373 return
2374
2375 if success and full_filename != '-':
2376 # Fixup content
2377 fixup_policy = self.params.get('fixup')
2378 if fixup_policy is None:
2379 fixup_policy = 'detect_or_warn'
2380
2381 INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg to fix this automatically.'
2382
2383 stretched_ratio = info_dict.get('stretched_ratio')
2384 if stretched_ratio is not None and stretched_ratio != 1:
2385 if fixup_policy == 'warn':
2386 self.report_warning('%s: Non-uniform pixel ratio (%s)' % (
2387 info_dict['id'], stretched_ratio))
2388 elif fixup_policy == 'detect_or_warn':
2389 stretched_pp = FFmpegFixupStretchedPP(self)
2390 if stretched_pp.available:
2391 info_dict['__postprocessors'].append(stretched_pp)
2392 else:
2393 self.report_warning(
2394 '%s: Non-uniform pixel ratio (%s). %s'
2395 % (info_dict['id'], stretched_ratio, INSTALL_FFMPEG_MESSAGE))
2396 else:
2397 assert fixup_policy in ('ignore', 'never')
2398
2399 if (info_dict.get('requested_formats') is None
2400 and info_dict.get('container') == 'm4a_dash'
2401 and info_dict.get('ext') == 'm4a'):
2402 if fixup_policy == 'warn':
2403 self.report_warning(
2404 '%s: writing DASH m4a. '
2405 'Only some players support this container.'
2406 % info_dict['id'])
2407 elif fixup_policy == 'detect_or_warn':
2408 fixup_pp = FFmpegFixupM4aPP(self)
2409 if fixup_pp.available:
2410 info_dict['__postprocessors'].append(fixup_pp)
2411 else:
2412 self.report_warning(
2413 '%s: writing DASH m4a. '
2414 'Only some players support this container. %s'
2415 % (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
2416 else:
2417 assert fixup_policy in ('ignore', 'never')
2418
2419 if (info_dict.get('protocol') == 'm3u8_native'
2420 or info_dict.get('protocol') == 'm3u8'
2421 and self.params.get('hls_prefer_native')):
2422 if fixup_policy == 'warn':
2423 self.report_warning('%s: malformed AAC bitstream detected.' % (
2424 info_dict['id']))
2425 elif fixup_policy == 'detect_or_warn':
2426 fixup_pp = FFmpegFixupM3u8PP(self)
2427 if fixup_pp.available:
2428 info_dict['__postprocessors'].append(fixup_pp)
2429 else:
2430 self.report_warning(
2431 '%s: malformed AAC bitstream detected. %s'
2432 % (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
2433 else:
2434 assert fixup_policy in ('ignore', 'never')
2435
2436 try:
2437 self.post_process(dl_filename, info_dict, files_to_move)
2438 except PostProcessingError as err:
2439 self.report_error('Postprocessing: %s' % str(err))
2440 return
2441 try:
2442 for ph in self._post_hooks:
2443 ph(full_filename)
2444 except Exception as err:
2445 self.report_error('post hooks: %s' % str(err))
2446 return
2447 must_record_download_archive = True
2448
2449 if must_record_download_archive or self.params.get('force_write_download_archive', False):
2450 self.record_download_archive(info_dict)
2451 max_downloads = self.params.get('max_downloads')
2452 if max_downloads is not None and self._num_downloads >= int(max_downloads):
2453 raise MaxDownloadsReached()
2454
2455 def download(self, url_list):
2456 """Download a given list of URLs."""
2457 outtmpl = self.outtmpl_dict['default']
2458 if (len(url_list) > 1
2459 and outtmpl != '-'
2460 and '%' not in outtmpl
2461 and self.params.get('max_downloads') != 1):
2462 raise SameFileError(outtmpl)
2463
2464 for url in url_list:
2465 try:
2466 # It also downloads the videos
2467 res = self.extract_info(
2468 url, force_generic_extractor=self.params.get('force_generic_extractor', False))
2469 except UnavailableVideoError:
2470 self.report_error('unable to download video')
2471 except MaxDownloadsReached:
2472 self.to_screen('[info] Maximum number of downloaded files reached')
2473 raise
2474 except ExistingVideoReached:
2475 self.to_screen('[info] Encountered a file that is already in the archive, stopping due to --break-on-existing')
2476 raise
2477 except RejectedVideoReached:
2478 self.to_screen('[info] Encountered a file that did not match filter, stopping due to --break-on-reject')
2479 raise
2480 else:
2481 if self.params.get('dump_single_json', False):
2482 self.to_stdout(json.dumps(res))
2483
2484 return self._download_retcode
2485
2486 def download_with_info_file(self, info_filename):
2487 with contextlib.closing(fileinput.FileInput(
2488 [info_filename], mode='r',
2489 openhook=fileinput.hook_encoded('utf-8'))) as f:
2490 # FileInput doesn't have a read method, we can't call json.load
2491 info = self.filter_requested_info(json.loads('\n'.join(f)))
2492 try:
2493 self.process_ie_result(info, download=True)
2494 except DownloadError:
2495 webpage_url = info.get('webpage_url')
2496 if webpage_url is not None:
2497 self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
2498 return self.download([webpage_url])
2499 else:
2500 raise
2501 return self._download_retcode
2502
2503 @staticmethod
2504 def filter_requested_info(info_dict):
2505 fields_to_remove = ('requested_formats', 'requested_subtitles')
2506 return dict(
2507 (k, v) for k, v in info_dict.items()
2508 if (k[0] != '_' or k == '_type') and k not in fields_to_remove)
2509
2510 def run_pp(self, pp, infodict, files_to_move={}):
2511 files_to_delete = []
2512 files_to_delete, infodict = pp.run(infodict)
2513 if not files_to_delete:
2514 return files_to_move, infodict
2515
2516 if self.params.get('keepvideo', False):
2517 for f in files_to_delete:
2518 files_to_move.setdefault(f, '')
2519 else:
2520 for old_filename in set(files_to_delete):
2521 self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
2522 try:
2523 os.remove(encodeFilename(old_filename))
2524 except (IOError, OSError):
2525 self.report_warning('Unable to remove downloaded original file')
2526 if old_filename in files_to_move:
2527 del files_to_move[old_filename]
2528 return files_to_move, infodict
2529
2530 def pre_process(self, ie_info):
2531 info = dict(ie_info)
2532 for pp in self._pps['beforedl']:
2533 info = self.run_pp(pp, info)[1]
2534 return info
2535
2536 def post_process(self, filename, ie_info, files_to_move={}):
2537 """Run all the postprocessors on the given file."""
2538 info = dict(ie_info)
2539 info['filepath'] = filename
2540 info['__files_to_move'] = {}
2541
2542 for pp in ie_info.get('__postprocessors', []) + self._pps['normal']:
2543 files_to_move, info = self.run_pp(pp, info, files_to_move)
2544 info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info)[1]
2545 for pp in self._pps['aftermove']:
2546 info = self.run_pp(pp, info, {})[1]
2547
2548 def _make_archive_id(self, info_dict):
2549 video_id = info_dict.get('id')
2550 if not video_id:
2551 return
2552 # Future-proof against any change in case
2553 # and backwards compatibility with prior versions
2554 extractor = info_dict.get('extractor_key') or info_dict.get('ie_key') # key in a playlist
2555 if extractor is None:
2556 url = str_or_none(info_dict.get('url'))
2557 if not url:
2558 return
2559 # Try to find matching extractor for the URL and take its ie_key
2560 for ie in self._ies:
2561 if ie.suitable(url):
2562 extractor = ie.ie_key()
2563 break
2564 else:
2565 return
2566 return '%s %s' % (extractor.lower(), video_id)
2567
2568 def in_download_archive(self, info_dict):
2569 fn = self.params.get('download_archive')
2570 if fn is None:
2571 return False
2572
2573 vid_id = self._make_archive_id(info_dict)
2574 if not vid_id:
2575 return False # Incomplete video information
2576
2577 return vid_id in self.archive
2578
2579 def record_download_archive(self, info_dict):
2580 fn = self.params.get('download_archive')
2581 if fn is None:
2582 return
2583 vid_id = self._make_archive_id(info_dict)
2584 assert vid_id
2585 with locked_file(fn, 'a', encoding='utf-8') as archive_file:
2586 archive_file.write(vid_id + '\n')
2587 self.archive.add(vid_id)
2588
2589 @staticmethod
2590 def format_resolution(format, default='unknown'):
2591 if format.get('vcodec') == 'none':
2592 return 'audio only'
2593 if format.get('resolution') is not None:
2594 return format['resolution']
2595 if format.get('height') is not None:
2596 if format.get('width') is not None:
2597 res = '%sx%s' % (format['width'], format['height'])
2598 else:
2599 res = '%sp' % format['height']
2600 elif format.get('width') is not None:
2601 res = '%dx?' % format['width']
2602 else:
2603 res = default
2604 return res
2605
2606 def _format_note(self, fdict):
2607 res = ''
2608 if fdict.get('ext') in ['f4f', 'f4m']:
2609 res += '(unsupported) '
2610 if fdict.get('language'):
2611 if res:
2612 res += ' '
2613 res += '[%s] ' % fdict['language']
2614 if fdict.get('format_note') is not None:
2615 res += fdict['format_note'] + ' '
2616 if fdict.get('tbr') is not None:
2617 res += '%4dk ' % fdict['tbr']
2618 if fdict.get('container') is not None:
2619 if res:
2620 res += ', '
2621 res += '%s container' % fdict['container']
2622 if (fdict.get('vcodec') is not None
2623 and fdict.get('vcodec') != 'none'):
2624 if res:
2625 res += ', '
2626 res += fdict['vcodec']
2627 if fdict.get('vbr') is not None:
2628 res += '@'
2629 elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
2630 res += 'video@'
2631 if fdict.get('vbr') is not None:
2632 res += '%4dk' % fdict['vbr']
2633 if fdict.get('fps') is not None:
2634 if res:
2635 res += ', '
2636 res += '%sfps' % fdict['fps']
2637 if fdict.get('acodec') is not None:
2638 if res:
2639 res += ', '
2640 if fdict['acodec'] == 'none':
2641 res += 'video only'
2642 else:
2643 res += '%-5s' % fdict['acodec']
2644 elif fdict.get('abr') is not None:
2645 if res:
2646 res += ', '
2647 res += 'audio'
2648 if fdict.get('abr') is not None:
2649 res += '@%3dk' % fdict['abr']
2650 if fdict.get('asr') is not None:
2651 res += ' (%5dHz)' % fdict['asr']
2652 if fdict.get('filesize') is not None:
2653 if res:
2654 res += ', '
2655 res += format_bytes(fdict['filesize'])
2656 elif fdict.get('filesize_approx') is not None:
2657 if res:
2658 res += ', '
2659 res += '~' + format_bytes(fdict['filesize_approx'])
2660 return res
2661
2662 def _format_note_table(self, f):
2663 def join_fields(*vargs):
2664 return ', '.join((val for val in vargs if val != ''))
2665
2666 return join_fields(
2667 'UNSUPPORTED' if f.get('ext') in ('f4f', 'f4m') else '',
2668 format_field(f, 'language', '[%s]'),
2669 format_field(f, 'format_note'),
2670 format_field(f, 'container', ignore=(None, f.get('ext'))),
2671 format_field(f, 'asr', '%5dHz'))
2672
2673 def list_formats(self, info_dict):
2674 formats = info_dict.get('formats', [info_dict])
2675 new_format = self.params.get('listformats_table', False)
2676 if new_format:
2677 table = [
2678 [
2679 format_field(f, 'format_id'),
2680 format_field(f, 'ext'),
2681 self.format_resolution(f),
2682 format_field(f, 'fps', '%d'),
2683 '|',
2684 format_field(f, 'filesize', ' %s', func=format_bytes) + format_field(f, 'filesize_approx', '~%s', func=format_bytes),
2685 format_field(f, 'tbr', '%4dk'),
2686 f.get('protocol').replace('http_dash_segments', 'dash').replace("native", "n"),
2687 '|',
2688 format_field(f, 'vcodec', default='unknown').replace('none', ''),
2689 format_field(f, 'vbr', '%4dk'),
2690 format_field(f, 'acodec', default='unknown').replace('none', ''),
2691 format_field(f, 'abr', '%3dk'),
2692 format_field(f, 'asr', '%5dHz'),
2693 self._format_note_table(f)]
2694 for f in formats
2695 if f.get('preference') is None or f['preference'] >= -1000]
2696 header_line = ['ID', 'EXT', 'RESOLUTION', 'FPS', '|', ' FILESIZE', ' TBR', 'PROTO',
2697 '|', 'VCODEC', ' VBR', 'ACODEC', ' ABR', ' ASR', 'NOTE']
2698 else:
2699 table = [
2700 [
2701 format_field(f, 'format_id'),
2702 format_field(f, 'ext'),
2703 self.format_resolution(f),
2704 self._format_note(f)]
2705 for f in formats
2706 if f.get('preference') is None or f['preference'] >= -1000]
2707 header_line = ['format code', 'extension', 'resolution', 'note']
2708
2709 # if len(formats) > 1:
2710 # table[-1][-1] += (' ' if table[-1][-1] else '') + '(best)'
2711 self.to_screen(
2712 '[info] Available formats for %s:\n%s' % (info_dict['id'], render_table(
2713 header_line,
2714 table,
2715 delim=new_format,
2716 extraGap=(0 if new_format else 1),
2717 hideEmpty=new_format)))
2718
2719 def list_thumbnails(self, info_dict):
2720 thumbnails = info_dict.get('thumbnails')
2721 if not thumbnails:
2722 self.to_screen('[info] No thumbnails present for %s' % info_dict['id'])
2723 return
2724
2725 self.to_screen(
2726 '[info] Thumbnails for %s:' % info_dict['id'])
2727 self.to_screen(render_table(
2728 ['ID', 'width', 'height', 'URL'],
2729 [[t['id'], t.get('width', 'unknown'), t.get('height', 'unknown'), t['url']] for t in thumbnails]))
2730
2731 def list_subtitles(self, video_id, subtitles, name='subtitles'):
2732 if not subtitles:
2733 self.to_screen('%s has no %s' % (video_id, name))
2734 return
2735 self.to_screen(
2736 'Available %s for %s:' % (name, video_id))
2737 self.to_screen(render_table(
2738 ['Language', 'formats'],
2739 [[lang, ', '.join(f['ext'] for f in reversed(formats))]
2740 for lang, formats in subtitles.items()]))
2741
2742 def urlopen(self, req):
2743 """ Start an HTTP download """
2744 if isinstance(req, compat_basestring):
2745 req = sanitized_Request(req)
2746 return self._opener.open(req, timeout=self._socket_timeout)
2747
2748 def print_debug_header(self):
2749 if not self.params.get('verbose'):
2750 return
2751
2752 if type('') is not compat_str:
2753 # Python 2.6 on SLES11 SP1 (https://github.com/ytdl-org/youtube-dl/issues/3326)
2754 self.report_warning(
2755 'Your Python is broken! Update to a newer and supported version')
2756
2757 stdout_encoding = getattr(
2758 sys.stdout, 'encoding', 'missing (%s)' % type(sys.stdout).__name__)
2759 encoding_str = (
2760 '[debug] Encodings: locale %s, fs %s, out %s, pref %s\n' % (
2761 locale.getpreferredencoding(),
2762 sys.getfilesystemencoding(),
2763 stdout_encoding,
2764 self.get_encoding()))
2765 write_string(encoding_str, encoding=None)
2766
2767 self._write_string('[debug] yt-dlp version %s\n' % __version__)
2768 if _LAZY_LOADER:
2769 self._write_string('[debug] Lazy loading extractors enabled\n')
2770 if _PLUGIN_CLASSES:
2771 self._write_string(
2772 '[debug] Plugin Extractors: %s\n' % [ie.ie_key() for ie in _PLUGIN_CLASSES])
2773 try:
2774 sp = subprocess.Popen(
2775 ['git', 'rev-parse', '--short', 'HEAD'],
2776 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2777 cwd=os.path.dirname(os.path.abspath(__file__)))
2778 out, err = process_communicate_or_kill(sp)
2779 out = out.decode().strip()
2780 if re.match('[0-9a-f]+', out):
2781 self._write_string('[debug] Git HEAD: %s\n' % out)
2782 except Exception:
2783 try:
2784 sys.exc_clear()
2785 except Exception:
2786 pass
2787
2788 def python_implementation():
2789 impl_name = platform.python_implementation()
2790 if impl_name == 'PyPy' and hasattr(sys, 'pypy_version_info'):
2791 return impl_name + ' version %d.%d.%d' % sys.pypy_version_info[:3]
2792 return impl_name
2793
2794 self._write_string('[debug] Python version %s (%s) - %s\n' % (
2795 platform.python_version(), python_implementation(),
2796 platform_name()))
2797
2798 exe_versions = FFmpegPostProcessor.get_versions(self)
2799 exe_versions['rtmpdump'] = rtmpdump_version()
2800 exe_versions['phantomjs'] = PhantomJSwrapper._version()
2801 exe_str = ', '.join(
2802 '%s %s' % (exe, v)
2803 for exe, v in sorted(exe_versions.items())
2804 if v
2805 )
2806 if not exe_str:
2807 exe_str = 'none'
2808 self._write_string('[debug] exe versions: %s\n' % exe_str)
2809
2810 proxy_map = {}
2811 for handler in self._opener.handlers:
2812 if hasattr(handler, 'proxies'):
2813 proxy_map.update(handler.proxies)
2814 self._write_string('[debug] Proxy map: ' + compat_str(proxy_map) + '\n')
2815
2816 if self.params.get('call_home', False):
2817 ipaddr = self.urlopen('https://yt-dl.org/ip').read().decode('utf-8')
2818 self._write_string('[debug] Public IP address: %s\n' % ipaddr)
2819 return
2820 latest_version = self.urlopen(
2821 'https://yt-dl.org/latest/version').read().decode('utf-8')
2822 if version_tuple(latest_version) > version_tuple(__version__):
2823 self.report_warning(
2824 'You are using an outdated version (newest version: %s)! '
2825 'See https://yt-dl.org/update if you need help updating.' %
2826 latest_version)
2827
2828 def _setup_opener(self):
2829 timeout_val = self.params.get('socket_timeout')
2830 self._socket_timeout = 600 if timeout_val is None else float(timeout_val)
2831
2832 opts_cookiefile = self.params.get('cookiefile')
2833 opts_proxy = self.params.get('proxy')
2834
2835 if opts_cookiefile is None:
2836 self.cookiejar = compat_cookiejar.CookieJar()
2837 else:
2838 opts_cookiefile = expand_path(opts_cookiefile)
2839 self.cookiejar = YoutubeDLCookieJar(opts_cookiefile)
2840 if os.access(opts_cookiefile, os.R_OK):
2841 self.cookiejar.load(ignore_discard=True, ignore_expires=True)
2842
2843 cookie_processor = YoutubeDLCookieProcessor(self.cookiejar)
2844 if opts_proxy is not None:
2845 if opts_proxy == '':
2846 proxies = {}
2847 else:
2848 proxies = {'http': opts_proxy, 'https': opts_proxy}
2849 else:
2850 proxies = compat_urllib_request.getproxies()
2851 # Set HTTPS proxy to HTTP one if given (https://github.com/ytdl-org/youtube-dl/issues/805)
2852 if 'http' in proxies and 'https' not in proxies:
2853 proxies['https'] = proxies['http']
2854 proxy_handler = PerRequestProxyHandler(proxies)
2855
2856 debuglevel = 1 if self.params.get('debug_printtraffic') else 0
2857 https_handler = make_HTTPS_handler(self.params, debuglevel=debuglevel)
2858 ydlh = YoutubeDLHandler(self.params, debuglevel=debuglevel)
2859 redirect_handler = YoutubeDLRedirectHandler()
2860 data_handler = compat_urllib_request_DataHandler()
2861
2862 # When passing our own FileHandler instance, build_opener won't add the
2863 # default FileHandler and allows us to disable the file protocol, which
2864 # can be used for malicious purposes (see
2865 # https://github.com/ytdl-org/youtube-dl/issues/8227)
2866 file_handler = compat_urllib_request.FileHandler()
2867
2868 def file_open(*args, **kwargs):
2869 raise compat_urllib_error.URLError('file:// scheme is explicitly disabled in youtube-dlc for security reasons')
2870 file_handler.file_open = file_open
2871
2872 opener = compat_urllib_request.build_opener(
2873 proxy_handler, https_handler, cookie_processor, ydlh, redirect_handler, data_handler, file_handler)
2874
2875 # Delete the default user-agent header, which would otherwise apply in
2876 # cases where our custom HTTP handler doesn't come into play
2877 # (See https://github.com/ytdl-org/youtube-dl/issues/1309 for details)
2878 opener.addheaders = []
2879 self._opener = opener
2880
2881 def encode(self, s):
2882 if isinstance(s, bytes):
2883 return s # Already encoded
2884
2885 try:
2886 return s.encode(self.get_encoding())
2887 except UnicodeEncodeError as err:
2888 err.reason = err.reason + '. Check your system encoding configuration or use the --encoding option.'
2889 raise
2890
2891 def get_encoding(self):
2892 encoding = self.params.get('encoding')
2893 if encoding is None:
2894 encoding = preferredencoding()
2895 return encoding
2896
2897 def _write_thumbnails(self, info_dict, filename): # return the extensions
2898 if self.params.get('writethumbnail', False):
2899 thumbnails = info_dict.get('thumbnails')
2900 if thumbnails:
2901 thumbnails = [thumbnails[-1]]
2902 elif self.params.get('write_all_thumbnails', False):
2903 thumbnails = info_dict.get('thumbnails') or []
2904 else:
2905 thumbnails = []
2906
2907 ret = []
2908 for t in thumbnails:
2909 thumb_ext = determine_ext(t['url'], 'jpg')
2910 suffix = '%s.' % t['id'] if len(thumbnails) > 1 else ''
2911 thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
2912 t['filename'] = thumb_filename = replace_extension(filename, suffix + thumb_ext, info_dict.get('ext'))
2913
2914 if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
2915 ret.append(suffix + thumb_ext)
2916 self.to_screen('[%s] %s: Thumbnail %sis already present' %
2917 (info_dict['extractor'], info_dict['id'], thumb_display_id))
2918 else:
2919 self.to_screen('[%s] %s: Downloading thumbnail %s...' %
2920 (info_dict['extractor'], info_dict['id'], thumb_display_id))
2921 try:
2922 uf = self.urlopen(t['url'])
2923 with open(encodeFilename(thumb_filename), 'wb') as thumbf:
2924 shutil.copyfileobj(uf, thumbf)
2925 ret.append(suffix + thumb_ext)
2926 self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
2927 (info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
2928 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
2929 self.report_warning('Unable to download thumbnail "%s": %s' %
2930 (t['url'], error_to_compat_str(err)))
2931 return ret