]> Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/YoutubeDL.py
New upstream version 2016.12.01
[youtubedl] / youtube_dl / 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
28 from .compat import (
29 compat_basestring,
30 compat_cookiejar,
31 compat_expanduser,
32 compat_get_terminal_size,
33 compat_http_client,
34 compat_kwargs,
35 compat_os_name,
36 compat_str,
37 compat_tokenize_tokenize,
38 compat_urllib_error,
39 compat_urllib_request,
40 compat_urllib_request_DataHandler,
41 )
42 from .utils import (
43 age_restricted,
44 args_to_str,
45 ContentTooShortError,
46 date_from_str,
47 DateRange,
48 DEFAULT_OUTTMPL,
49 determine_ext,
50 determine_protocol,
51 DownloadError,
52 encode_compat_str,
53 encodeFilename,
54 error_to_compat_str,
55 ExtractorError,
56 format_bytes,
57 formatSeconds,
58 locked_file,
59 make_HTTPS_handler,
60 MaxDownloadsReached,
61 PagedList,
62 parse_filesize,
63 PerRequestProxyHandler,
64 platform_name,
65 PostProcessingError,
66 preferredencoding,
67 prepend_extension,
68 register_socks_protocols,
69 render_table,
70 replace_extension,
71 SameFileError,
72 sanitize_filename,
73 sanitize_path,
74 sanitize_url,
75 sanitized_Request,
76 std_headers,
77 subtitles_filename,
78 UnavailableVideoError,
79 url_basename,
80 version_tuple,
81 write_json_file,
82 write_string,
83 YoutubeDLCookieProcessor,
84 YoutubeDLHandler,
85 )
86 from .cache import Cache
87 from .extractor import get_info_extractor, gen_extractor_classes, _LAZY_LOADER
88 from .downloader import get_suitable_downloader
89 from .downloader.rtmp import rtmpdump_version
90 from .postprocessor import (
91 FFmpegFixupM3u8PP,
92 FFmpegFixupM4aPP,
93 FFmpegFixupStretchedPP,
94 FFmpegMergerPP,
95 FFmpegPostProcessor,
96 get_postprocessor,
97 )
98 from .version import __version__
99
100 if compat_os_name == 'nt':
101 import ctypes
102
103
104 class YoutubeDL(object):
105 """YoutubeDL class.
106
107 YoutubeDL objects are the ones responsible of downloading the
108 actual video file and writing it to disk if the user has requested
109 it, among some other tasks. In most cases there should be one per
110 program. As, given a video URL, the downloader doesn't know how to
111 extract all the needed information, task that InfoExtractors do, it
112 has to pass the URL to one of them.
113
114 For this, YoutubeDL objects have a method that allows
115 InfoExtractors to be registered in a given order. When it is passed
116 a URL, the YoutubeDL object handles it to the first InfoExtractor it
117 finds that reports being able to handle it. The InfoExtractor extracts
118 all the information about the video or videos the URL refers to, and
119 YoutubeDL process the extracted information, possibly using a File
120 Downloader to download the video.
121
122 YoutubeDL objects accept a lot of parameters. In order not to saturate
123 the object constructor with arguments, it receives a dictionary of
124 options instead. These options are available through the params
125 attribute for the InfoExtractors to use. The YoutubeDL also
126 registers itself as the downloader in charge for the InfoExtractors
127 that are added to it, so this is a "mutual registration".
128
129 Available options:
130
131 username: Username for authentication purposes.
132 password: Password for authentication purposes.
133 videopassword: Password for accessing a video.
134 ap_mso: Adobe Pass multiple-system operator identifier.
135 ap_username: Multiple-system operator account username.
136 ap_password: Multiple-system operator account password.
137 usenetrc: Use netrc for authentication instead.
138 verbose: Print additional info to stdout.
139 quiet: Do not print messages to stdout.
140 no_warnings: Do not print out anything for warnings.
141 forceurl: Force printing final URL.
142 forcetitle: Force printing title.
143 forceid: Force printing ID.
144 forcethumbnail: Force printing thumbnail URL.
145 forcedescription: Force printing description.
146 forcefilename: Force printing final filename.
147 forceduration: Force printing duration.
148 forcejson: Force printing info_dict as JSON.
149 dump_single_json: Force printing the info_dict of the whole playlist
150 (or video) as a single JSON line.
151 simulate: Do not download the video files.
152 format: Video format code. See options.py for more information.
153 outtmpl: Template for output names.
154 restrictfilenames: Do not allow "&" and spaces in file names
155 ignoreerrors: Do not stop on download errors.
156 force_generic_extractor: Force downloader to use the generic extractor
157 nooverwrites: Prevent overwriting files.
158 playliststart: Playlist item to start at.
159 playlistend: Playlist item to end at.
160 playlist_items: Specific indices of playlist to download.
161 playlistreverse: Download playlist items in reverse order.
162 matchtitle: Download only matching titles.
163 rejecttitle: Reject downloads for matching titles.
164 logger: Log messages to a logging.Logger instance.
165 logtostderr: Log messages to stderr instead of stdout.
166 writedescription: Write the video description to a .description file
167 writeinfojson: Write the video description to a .info.json file
168 writeannotations: Write the video annotations to a .annotations.xml file
169 writethumbnail: Write the thumbnail image to a file
170 write_all_thumbnails: Write all thumbnail formats to files
171 writesubtitles: Write the video subtitles to a file
172 writeautomaticsub: Write the automatically generated subtitles to a file
173 allsubtitles: Downloads all the subtitles of the video
174 (requires writesubtitles or writeautomaticsub)
175 listsubtitles: Lists all available subtitles for the video
176 subtitlesformat: The format code for subtitles
177 subtitleslangs: List of languages of the subtitles to download
178 keepvideo: Keep the video file after post-processing
179 daterange: A DateRange object, download only if the upload_date is in the range.
180 skip_download: Skip the actual download of the video file
181 cachedir: Location of the cache files in the filesystem.
182 False to disable filesystem cache.
183 noplaylist: Download single video instead of a playlist if in doubt.
184 age_limit: An integer representing the user's age in years.
185 Unsuitable videos for the given age are skipped.
186 min_views: An integer representing the minimum view count the video
187 must have in order to not be skipped.
188 Videos without view count information are always
189 downloaded. None for no limit.
190 max_views: An integer representing the maximum view count.
191 Videos that are more popular than that are not
192 downloaded.
193 Videos without view count information are always
194 downloaded. None for no limit.
195 download_archive: File name of a file where all downloads are recorded.
196 Videos already present in the file are not downloaded
197 again.
198 cookiefile: File name where cookies should be read from and dumped to.
199 nocheckcertificate:Do not verify SSL certificates
200 prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
201 At the moment, this is only supported by YouTube.
202 proxy: URL of the proxy server to use
203 geo_verification_proxy: URL of the proxy to use for IP address verification
204 on geo-restricted sites. (Experimental)
205 socket_timeout: Time to wait for unresponsive hosts, in seconds
206 bidi_workaround: Work around buggy terminals without bidirectional text
207 support, using fridibi
208 debug_printtraffic:Print out sent and received HTTP traffic
209 include_ads: Download ads as well
210 default_search: Prepend this string if an input url is not valid.
211 'auto' for elaborate guessing
212 encoding: Use this encoding instead of the system-specified.
213 extract_flat: Do not resolve URLs, return the immediate result.
214 Pass in 'in_playlist' to only show this behavior for
215 playlist items.
216 postprocessors: A list of dictionaries, each with an entry
217 * key: The name of the postprocessor. See
218 youtube_dl/postprocessor/__init__.py for a list.
219 as well as any further keyword arguments for the
220 postprocessor.
221 progress_hooks: A list of functions that get called on download
222 progress, with a dictionary with the entries
223 * status: One of "downloading", "error", or "finished".
224 Check this first and ignore unknown values.
225
226 If status is one of "downloading", or "finished", the
227 following properties may also be present:
228 * filename: The final filename (always present)
229 * tmpfilename: The filename we're currently writing to
230 * downloaded_bytes: Bytes on disk
231 * total_bytes: Size of the whole file, None if unknown
232 * total_bytes_estimate: Guess of the eventual file size,
233 None if unavailable.
234 * elapsed: The number of seconds since download started.
235 * eta: The estimated time in seconds, None if unknown
236 * speed: The download speed in bytes/second, None if
237 unknown
238 * fragment_index: The counter of the currently
239 downloaded video fragment.
240 * fragment_count: The number of fragments (= individual
241 files that will be merged)
242
243 Progress hooks are guaranteed to be called at least once
244 (with status "finished") if the download is successful.
245 merge_output_format: Extension to use when merging formats.
246 fixup: Automatically correct known faults of the file.
247 One of:
248 - "never": do nothing
249 - "warn": only emit a warning
250 - "detect_or_warn": check whether we can do anything
251 about it, warn otherwise (default)
252 source_address: (Experimental) Client-side IP address to bind to.
253 call_home: Boolean, true iff we are allowed to contact the
254 youtube-dl servers for debugging.
255 sleep_interval: Number of seconds to sleep before each download when
256 used alone or a lower bound of a range for randomized
257 sleep before each download (minimum possible number
258 of seconds to sleep) when used along with
259 max_sleep_interval.
260 max_sleep_interval:Upper bound of a range for randomized sleep before each
261 download (maximum possible number of seconds to sleep).
262 Must only be used along with sleep_interval.
263 Actual sleep time will be a random float from range
264 [sleep_interval; max_sleep_interval].
265 listformats: Print an overview of available video formats and exit.
266 list_thumbnails: Print a table of all thumbnails and exit.
267 match_filter: A function that gets called with the info_dict of
268 every video.
269 If it returns a message, the video is ignored.
270 If it returns None, the video is downloaded.
271 match_filter_func in utils.py is one example for this.
272 no_color: Do not emit color codes in output.
273
274 The following options determine which downloader is picked:
275 external_downloader: Executable of the external downloader to call.
276 None or unset for standard (built-in) downloader.
277 hls_prefer_native: Use the native HLS downloader instead of ffmpeg/avconv
278 if True, otherwise use ffmpeg/avconv if False, otherwise
279 use downloader suggested by extractor if None.
280
281 The following parameters are not used by YoutubeDL itself, they are used by
282 the downloader (see youtube_dl/downloader/common.py):
283 nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
284 noresizebuffer, retries, continuedl, noprogress, consoletitle,
285 xattr_set_filesize, external_downloader_args, hls_use_mpegts.
286
287 The following options are used by the post processors:
288 prefer_ffmpeg: If True, use ffmpeg instead of avconv if both are available,
289 otherwise prefer avconv.
290 postprocessor_args: A list of additional command-line arguments for the
291 postprocessor.
292 """
293
294 params = None
295 _ies = []
296 _pps = []
297 _download_retcode = None
298 _num_downloads = None
299 _screen_file = None
300
301 def __init__(self, params=None, auto_init=True):
302 """Create a FileDownloader object with the given options."""
303 if params is None:
304 params = {}
305 self._ies = []
306 self._ies_instances = {}
307 self._pps = []
308 self._progress_hooks = []
309 self._download_retcode = 0
310 self._num_downloads = 0
311 self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
312 self._err_file = sys.stderr
313 self.params = {
314 # Default parameters
315 'nocheckcertificate': False,
316 }
317 self.params.update(params)
318 self.cache = Cache(self)
319
320 if self.params.get('cn_verification_proxy') is not None:
321 self.report_warning('--cn-verification-proxy is deprecated. Use --geo-verification-proxy instead.')
322 if self.params.get('geo_verification_proxy') is None:
323 self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
324
325 if params.get('bidi_workaround', False):
326 try:
327 import pty
328 master, slave = pty.openpty()
329 width = compat_get_terminal_size().columns
330 if width is None:
331 width_args = []
332 else:
333 width_args = ['-w', str(width)]
334 sp_kwargs = dict(
335 stdin=subprocess.PIPE,
336 stdout=slave,
337 stderr=self._err_file)
338 try:
339 self._output_process = subprocess.Popen(
340 ['bidiv'] + width_args, **sp_kwargs
341 )
342 except OSError:
343 self._output_process = subprocess.Popen(
344 ['fribidi', '-c', 'UTF-8'] + width_args, **sp_kwargs)
345 self._output_channel = os.fdopen(master, 'rb')
346 except OSError as ose:
347 if ose.errno == errno.ENOENT:
348 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.')
349 else:
350 raise
351
352 if (sys.version_info >= (3,) and sys.platform != 'win32' and
353 sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968'] and
354 not params.get('restrictfilenames', False)):
355 # On Python 3, the Unicode filesystem API will throw errors (#1474)
356 self.report_warning(
357 'Assuming --restrict-filenames since file system encoding '
358 'cannot encode all characters. '
359 'Set the LC_ALL environment variable to fix this.')
360 self.params['restrictfilenames'] = True
361
362 if isinstance(params.get('outtmpl'), bytes):
363 self.report_warning(
364 'Parameter outtmpl is bytes, but should be a unicode string. '
365 'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
366
367 self._setup_opener()
368
369 if auto_init:
370 self.print_debug_header()
371 self.add_default_info_extractors()
372
373 for pp_def_raw in self.params.get('postprocessors', []):
374 pp_class = get_postprocessor(pp_def_raw['key'])
375 pp_def = dict(pp_def_raw)
376 del pp_def['key']
377 pp = pp_class(self, **compat_kwargs(pp_def))
378 self.add_post_processor(pp)
379
380 for ph in self.params.get('progress_hooks', []):
381 self.add_progress_hook(ph)
382
383 register_socks_protocols()
384
385 def warn_if_short_id(self, argv):
386 # short YouTube ID starting with dash?
387 idxs = [
388 i for i, a in enumerate(argv)
389 if re.match(r'^-[0-9A-Za-z_-]{10}$', a)]
390 if idxs:
391 correct_argv = (
392 ['youtube-dl'] +
393 [a for i, a in enumerate(argv) if i not in idxs] +
394 ['--'] + [argv[i] for i in idxs]
395 )
396 self.report_warning(
397 'Long argument string detected. '
398 'Use -- to separate parameters and URLs, like this:\n%s\n' %
399 args_to_str(correct_argv))
400
401 def add_info_extractor(self, ie):
402 """Add an InfoExtractor object to the end of the list."""
403 self._ies.append(ie)
404 if not isinstance(ie, type):
405 self._ies_instances[ie.ie_key()] = ie
406 ie.set_downloader(self)
407
408 def get_info_extractor(self, ie_key):
409 """
410 Get an instance of an IE with name ie_key, it will try to get one from
411 the _ies list, if there's no instance it will create a new one and add
412 it to the extractor list.
413 """
414 ie = self._ies_instances.get(ie_key)
415 if ie is None:
416 ie = get_info_extractor(ie_key)()
417 self.add_info_extractor(ie)
418 return ie
419
420 def add_default_info_extractors(self):
421 """
422 Add the InfoExtractors returned by gen_extractors to the end of the list
423 """
424 for ie in gen_extractor_classes():
425 self.add_info_extractor(ie)
426
427 def add_post_processor(self, pp):
428 """Add a PostProcessor object to the end of the chain."""
429 self._pps.append(pp)
430 pp.set_downloader(self)
431
432 def add_progress_hook(self, ph):
433 """Add the progress hook (currently only for the file downloader)"""
434 self._progress_hooks.append(ph)
435
436 def _bidi_workaround(self, message):
437 if not hasattr(self, '_output_channel'):
438 return message
439
440 assert hasattr(self, '_output_process')
441 assert isinstance(message, compat_str)
442 line_count = message.count('\n') + 1
443 self._output_process.stdin.write((message + '\n').encode('utf-8'))
444 self._output_process.stdin.flush()
445 res = ''.join(self._output_channel.readline().decode('utf-8')
446 for _ in range(line_count))
447 return res[:-len('\n')]
448
449 def to_screen(self, message, skip_eol=False):
450 """Print message to stdout if not in quiet mode."""
451 return self.to_stdout(message, skip_eol, check_quiet=True)
452
453 def _write_string(self, s, out=None):
454 write_string(s, out=out, encoding=self.params.get('encoding'))
455
456 def to_stdout(self, message, skip_eol=False, check_quiet=False):
457 """Print message to stdout if not in quiet mode."""
458 if self.params.get('logger'):
459 self.params['logger'].debug(message)
460 elif not check_quiet or not self.params.get('quiet', False):
461 message = self._bidi_workaround(message)
462 terminator = ['\n', ''][skip_eol]
463 output = message + terminator
464
465 self._write_string(output, self._screen_file)
466
467 def to_stderr(self, message):
468 """Print message to stderr."""
469 assert isinstance(message, compat_str)
470 if self.params.get('logger'):
471 self.params['logger'].error(message)
472 else:
473 message = self._bidi_workaround(message)
474 output = message + '\n'
475 self._write_string(output, self._err_file)
476
477 def to_console_title(self, message):
478 if not self.params.get('consoletitle', False):
479 return
480 if compat_os_name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
481 # c_wchar_p() might not be necessary if `message` is
482 # already of type unicode()
483 ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
484 elif 'TERM' in os.environ:
485 self._write_string('\033]0;%s\007' % message, self._screen_file)
486
487 def save_console_title(self):
488 if not self.params.get('consoletitle', False):
489 return
490 if 'TERM' in os.environ:
491 # Save the title on stack
492 self._write_string('\033[22;0t', self._screen_file)
493
494 def restore_console_title(self):
495 if not self.params.get('consoletitle', False):
496 return
497 if 'TERM' in os.environ:
498 # Restore the title from stack
499 self._write_string('\033[23;0t', self._screen_file)
500
501 def __enter__(self):
502 self.save_console_title()
503 return self
504
505 def __exit__(self, *args):
506 self.restore_console_title()
507
508 if self.params.get('cookiefile') is not None:
509 self.cookiejar.save()
510
511 def trouble(self, message=None, tb=None):
512 """Determine action to take when a download problem appears.
513
514 Depending on if the downloader has been configured to ignore
515 download errors or not, this method may throw an exception or
516 not when errors are found, after printing the message.
517
518 tb, if given, is additional traceback information.
519 """
520 if message is not None:
521 self.to_stderr(message)
522 if self.params.get('verbose'):
523 if tb is None:
524 if sys.exc_info()[0]: # if .trouble has been called from an except block
525 tb = ''
526 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
527 tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
528 tb += encode_compat_str(traceback.format_exc())
529 else:
530 tb_data = traceback.format_list(traceback.extract_stack())
531 tb = ''.join(tb_data)
532 self.to_stderr(tb)
533 if not self.params.get('ignoreerrors', False):
534 if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
535 exc_info = sys.exc_info()[1].exc_info
536 else:
537 exc_info = sys.exc_info()
538 raise DownloadError(message, exc_info)
539 self._download_retcode = 1
540
541 def report_warning(self, message):
542 '''
543 Print the message to stderr, it will be prefixed with 'WARNING:'
544 If stderr is a tty file the 'WARNING:' will be colored
545 '''
546 if self.params.get('logger') is not None:
547 self.params['logger'].warning(message)
548 else:
549 if self.params.get('no_warnings'):
550 return
551 if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
552 _msg_header = '\033[0;33mWARNING:\033[0m'
553 else:
554 _msg_header = 'WARNING:'
555 warning_message = '%s %s' % (_msg_header, message)
556 self.to_stderr(warning_message)
557
558 def report_error(self, message, tb=None):
559 '''
560 Do the same as trouble, but prefixes the message with 'ERROR:', colored
561 in red if stderr is a tty file.
562 '''
563 if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
564 _msg_header = '\033[0;31mERROR:\033[0m'
565 else:
566 _msg_header = 'ERROR:'
567 error_message = '%s %s' % (_msg_header, message)
568 self.trouble(error_message, tb)
569
570 def report_file_already_downloaded(self, file_name):
571 """Report file has already been fully downloaded."""
572 try:
573 self.to_screen('[download] %s has already been downloaded' % file_name)
574 except UnicodeEncodeError:
575 self.to_screen('[download] The file has already been downloaded')
576
577 def prepare_filename(self, info_dict):
578 """Generate the output filename."""
579 try:
580 template_dict = dict(info_dict)
581
582 template_dict['epoch'] = int(time.time())
583 autonumber_size = self.params.get('autonumber_size')
584 if autonumber_size is None:
585 autonumber_size = 5
586 autonumber_templ = '%0' + str(autonumber_size) + 'd'
587 template_dict['autonumber'] = autonumber_templ % self._num_downloads
588 if template_dict.get('playlist_index') is not None:
589 template_dict['playlist_index'] = '%0*d' % (len(str(template_dict['n_entries'])), template_dict['playlist_index'])
590 if template_dict.get('resolution') is None:
591 if template_dict.get('width') and template_dict.get('height'):
592 template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
593 elif template_dict.get('height'):
594 template_dict['resolution'] = '%sp' % template_dict['height']
595 elif template_dict.get('width'):
596 template_dict['resolution'] = '%dx?' % template_dict['width']
597
598 sanitize = lambda k, v: sanitize_filename(
599 compat_str(v),
600 restricted=self.params.get('restrictfilenames'),
601 is_id=(k == 'id'))
602 template_dict = dict((k, sanitize(k, v))
603 for k, v in template_dict.items()
604 if v is not None and not isinstance(v, (list, tuple, dict)))
605 template_dict = collections.defaultdict(lambda: 'NA', template_dict)
606
607 outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
608 tmpl = compat_expanduser(outtmpl)
609 filename = tmpl % template_dict
610 # Temporary fix for #4787
611 # 'Treat' all problem characters by passing filename through preferredencoding
612 # to workaround encoding issues with subprocess on python2 @ Windows
613 if sys.version_info < (3, 0) and sys.platform == 'win32':
614 filename = encodeFilename(filename, True).decode(preferredencoding())
615 return sanitize_path(filename)
616 except ValueError as err:
617 self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
618 return None
619
620 def _match_entry(self, info_dict, incomplete):
621 """ Returns None iff the file should be downloaded """
622
623 video_title = info_dict.get('title', info_dict.get('id', 'video'))
624 if 'title' in info_dict:
625 # This can happen when we're just evaluating the playlist
626 title = info_dict['title']
627 matchtitle = self.params.get('matchtitle', False)
628 if matchtitle:
629 if not re.search(matchtitle, title, re.IGNORECASE):
630 return '"' + title + '" title did not match pattern "' + matchtitle + '"'
631 rejecttitle = self.params.get('rejecttitle', False)
632 if rejecttitle:
633 if re.search(rejecttitle, title, re.IGNORECASE):
634 return '"' + title + '" title matched reject pattern "' + rejecttitle + '"'
635 date = info_dict.get('upload_date')
636 if date is not None:
637 dateRange = self.params.get('daterange', DateRange())
638 if date not in dateRange:
639 return '%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
640 view_count = info_dict.get('view_count')
641 if view_count is not None:
642 min_views = self.params.get('min_views')
643 if min_views is not None and view_count < min_views:
644 return 'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
645 max_views = self.params.get('max_views')
646 if max_views is not None and view_count > max_views:
647 return 'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
648 if age_restricted(info_dict.get('age_limit'), self.params.get('age_limit')):
649 return 'Skipping "%s" because it is age restricted' % video_title
650 if self.in_download_archive(info_dict):
651 return '%s has already been recorded in archive' % video_title
652
653 if not incomplete:
654 match_filter = self.params.get('match_filter')
655 if match_filter is not None:
656 ret = match_filter(info_dict)
657 if ret is not None:
658 return ret
659
660 return None
661
662 @staticmethod
663 def add_extra_info(info_dict, extra_info):
664 '''Set the keys from extra_info in info dict if they are missing'''
665 for key, value in extra_info.items():
666 info_dict.setdefault(key, value)
667
668 def extract_info(self, url, download=True, ie_key=None, extra_info={},
669 process=True, force_generic_extractor=False):
670 '''
671 Returns a list with a dictionary for each video we find.
672 If 'download', also downloads the videos.
673 extra_info is a dict containing the extra values to add to each result
674 '''
675
676 if not ie_key and force_generic_extractor:
677 ie_key = 'Generic'
678
679 if ie_key:
680 ies = [self.get_info_extractor(ie_key)]
681 else:
682 ies = self._ies
683
684 for ie in ies:
685 if not ie.suitable(url):
686 continue
687
688 ie = self.get_info_extractor(ie.ie_key())
689 if not ie.working():
690 self.report_warning('The program functionality for this site has been marked as broken, '
691 'and will probably not work.')
692
693 try:
694 ie_result = ie.extract(url)
695 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
696 break
697 if isinstance(ie_result, list):
698 # Backwards compatibility: old IE result format
699 ie_result = {
700 '_type': 'compat_list',
701 'entries': ie_result,
702 }
703 self.add_default_extra_info(ie_result, ie, url)
704 if process:
705 return self.process_ie_result(ie_result, download, extra_info)
706 else:
707 return ie_result
708 except ExtractorError as e: # An error we somewhat expected
709 self.report_error(compat_str(e), e.format_traceback())
710 break
711 except MaxDownloadsReached:
712 raise
713 except Exception as e:
714 if self.params.get('ignoreerrors', False):
715 self.report_error(error_to_compat_str(e), tb=encode_compat_str(traceback.format_exc()))
716 break
717 else:
718 raise
719 else:
720 self.report_error('no suitable InfoExtractor for URL %s' % url)
721
722 def add_default_extra_info(self, ie_result, ie, url):
723 self.add_extra_info(ie_result, {
724 'extractor': ie.IE_NAME,
725 'webpage_url': url,
726 'webpage_url_basename': url_basename(url),
727 'extractor_key': ie.ie_key(),
728 })
729
730 def process_ie_result(self, ie_result, download=True, extra_info={}):
731 """
732 Take the result of the ie(may be modified) and resolve all unresolved
733 references (URLs, playlist items).
734
735 It will also download the videos if 'download'.
736 Returns the resolved ie_result.
737 """
738 result_type = ie_result.get('_type', 'video')
739
740 if result_type in ('url', 'url_transparent'):
741 ie_result['url'] = sanitize_url(ie_result['url'])
742 extract_flat = self.params.get('extract_flat', False)
743 if ((extract_flat == 'in_playlist' and 'playlist' in extra_info) or
744 extract_flat is True):
745 if self.params.get('forcejson', False):
746 self.to_stdout(json.dumps(ie_result))
747 return ie_result
748
749 if result_type == 'video':
750 self.add_extra_info(ie_result, extra_info)
751 return self.process_video_result(ie_result, download=download)
752 elif result_type == 'url':
753 # We have to add extra_info to the results because it may be
754 # contained in a playlist
755 return self.extract_info(ie_result['url'],
756 download,
757 ie_key=ie_result.get('ie_key'),
758 extra_info=extra_info)
759 elif result_type == 'url_transparent':
760 # Use the information from the embedding page
761 info = self.extract_info(
762 ie_result['url'], ie_key=ie_result.get('ie_key'),
763 extra_info=extra_info, download=False, process=False)
764
765 force_properties = dict(
766 (k, v) for k, v in ie_result.items() if v is not None)
767 for f in ('_type', 'url', 'ie_key'):
768 if f in force_properties:
769 del force_properties[f]
770 new_result = info.copy()
771 new_result.update(force_properties)
772
773 assert new_result.get('_type') != 'url_transparent'
774
775 return self.process_ie_result(
776 new_result, download=download, extra_info=extra_info)
777 elif result_type == 'playlist' or result_type == 'multi_video':
778 # We process each entry in the playlist
779 playlist = ie_result.get('title') or ie_result.get('id')
780 self.to_screen('[download] Downloading playlist: %s' % playlist)
781
782 playlist_results = []
783
784 playliststart = self.params.get('playliststart', 1) - 1
785 playlistend = self.params.get('playlistend')
786 # For backwards compatibility, interpret -1 as whole list
787 if playlistend == -1:
788 playlistend = None
789
790 playlistitems_str = self.params.get('playlist_items')
791 playlistitems = None
792 if playlistitems_str is not None:
793 def iter_playlistitems(format):
794 for string_segment in format.split(','):
795 if '-' in string_segment:
796 start, end = string_segment.split('-')
797 for item in range(int(start), int(end) + 1):
798 yield int(item)
799 else:
800 yield int(string_segment)
801 playlistitems = iter_playlistitems(playlistitems_str)
802
803 ie_entries = ie_result['entries']
804 if isinstance(ie_entries, list):
805 n_all_entries = len(ie_entries)
806 if playlistitems:
807 entries = [
808 ie_entries[i - 1] for i in playlistitems
809 if -n_all_entries <= i - 1 < n_all_entries]
810 else:
811 entries = ie_entries[playliststart:playlistend]
812 n_entries = len(entries)
813 self.to_screen(
814 '[%s] playlist %s: Collected %d video ids (downloading %d of them)' %
815 (ie_result['extractor'], playlist, n_all_entries, n_entries))
816 elif isinstance(ie_entries, PagedList):
817 if playlistitems:
818 entries = []
819 for item in playlistitems:
820 entries.extend(ie_entries.getslice(
821 item - 1, item
822 ))
823 else:
824 entries = ie_entries.getslice(
825 playliststart, playlistend)
826 n_entries = len(entries)
827 self.to_screen(
828 '[%s] playlist %s: Downloading %d videos' %
829 (ie_result['extractor'], playlist, n_entries))
830 else: # iterable
831 if playlistitems:
832 entry_list = list(ie_entries)
833 entries = [entry_list[i - 1] for i in playlistitems]
834 else:
835 entries = list(itertools.islice(
836 ie_entries, playliststart, playlistend))
837 n_entries = len(entries)
838 self.to_screen(
839 '[%s] playlist %s: Downloading %d videos' %
840 (ie_result['extractor'], playlist, n_entries))
841
842 if self.params.get('playlistreverse', False):
843 entries = entries[::-1]
844
845 for i, entry in enumerate(entries, 1):
846 self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
847 extra = {
848 'n_entries': n_entries,
849 'playlist': playlist,
850 'playlist_id': ie_result.get('id'),
851 'playlist_title': ie_result.get('title'),
852 'playlist_index': i + playliststart,
853 'extractor': ie_result['extractor'],
854 'webpage_url': ie_result['webpage_url'],
855 'webpage_url_basename': url_basename(ie_result['webpage_url']),
856 'extractor_key': ie_result['extractor_key'],
857 }
858
859 reason = self._match_entry(entry, incomplete=True)
860 if reason is not None:
861 self.to_screen('[download] ' + reason)
862 continue
863
864 entry_result = self.process_ie_result(entry,
865 download=download,
866 extra_info=extra)
867 playlist_results.append(entry_result)
868 ie_result['entries'] = playlist_results
869 self.to_screen('[download] Finished downloading playlist: %s' % playlist)
870 return ie_result
871 elif result_type == 'compat_list':
872 self.report_warning(
873 'Extractor %s returned a compat_list result. '
874 'It needs to be updated.' % ie_result.get('extractor'))
875
876 def _fixup(r):
877 self.add_extra_info(
878 r,
879 {
880 'extractor': ie_result['extractor'],
881 'webpage_url': ie_result['webpage_url'],
882 'webpage_url_basename': url_basename(ie_result['webpage_url']),
883 'extractor_key': ie_result['extractor_key'],
884 }
885 )
886 return r
887 ie_result['entries'] = [
888 self.process_ie_result(_fixup(r), download, extra_info)
889 for r in ie_result['entries']
890 ]
891 return ie_result
892 else:
893 raise Exception('Invalid result type: %s' % result_type)
894
895 def _build_format_filter(self, filter_spec):
896 " Returns a function to filter the formats according to the filter_spec "
897
898 OPERATORS = {
899 '<': operator.lt,
900 '<=': operator.le,
901 '>': operator.gt,
902 '>=': operator.ge,
903 '=': operator.eq,
904 '!=': operator.ne,
905 }
906 operator_rex = re.compile(r'''(?x)\s*
907 (?P<key>width|height|tbr|abr|vbr|asr|filesize|fps)
908 \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
909 (?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
910 $
911 ''' % '|'.join(map(re.escape, OPERATORS.keys())))
912 m = operator_rex.search(filter_spec)
913 if m:
914 try:
915 comparison_value = int(m.group('value'))
916 except ValueError:
917 comparison_value = parse_filesize(m.group('value'))
918 if comparison_value is None:
919 comparison_value = parse_filesize(m.group('value') + 'B')
920 if comparison_value is None:
921 raise ValueError(
922 'Invalid value %r in format specification %r' % (
923 m.group('value'), filter_spec))
924 op = OPERATORS[m.group('op')]
925
926 if not m:
927 STR_OPERATORS = {
928 '=': operator.eq,
929 '!=': operator.ne,
930 '^=': lambda attr, value: attr.startswith(value),
931 '$=': lambda attr, value: attr.endswith(value),
932 '*=': lambda attr, value: value in attr,
933 }
934 str_operator_rex = re.compile(r'''(?x)
935 \s*(?P<key>ext|acodec|vcodec|container|protocol|format_id)
936 \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?
937 \s*(?P<value>[a-zA-Z0-9._-]+)
938 \s*$
939 ''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
940 m = str_operator_rex.search(filter_spec)
941 if m:
942 comparison_value = m.group('value')
943 op = STR_OPERATORS[m.group('op')]
944
945 if not m:
946 raise ValueError('Invalid filter specification %r' % filter_spec)
947
948 def _filter(f):
949 actual_value = f.get(m.group('key'))
950 if actual_value is None:
951 return m.group('none_inclusive')
952 return op(actual_value, comparison_value)
953 return _filter
954
955 def build_format_selector(self, format_spec):
956 def syntax_error(note, start):
957 message = (
958 'Invalid format specification: '
959 '{0}\n\t{1}\n\t{2}^'.format(note, format_spec, ' ' * start[1]))
960 return SyntaxError(message)
961
962 PICKFIRST = 'PICKFIRST'
963 MERGE = 'MERGE'
964 SINGLE = 'SINGLE'
965 GROUP = 'GROUP'
966 FormatSelector = collections.namedtuple('FormatSelector', ['type', 'selector', 'filters'])
967
968 def _parse_filter(tokens):
969 filter_parts = []
970 for type, string, start, _, _ in tokens:
971 if type == tokenize.OP and string == ']':
972 return ''.join(filter_parts)
973 else:
974 filter_parts.append(string)
975
976 def _remove_unused_ops(tokens):
977 # Remove operators that we don't use and join them with the surrounding strings
978 # for example: 'mp4' '-' 'baseline' '-' '16x9' is converted to 'mp4-baseline-16x9'
979 ALLOWED_OPS = ('/', '+', ',', '(', ')')
980 last_string, last_start, last_end, last_line = None, None, None, None
981 for type, string, start, end, line in tokens:
982 if type == tokenize.OP and string == '[':
983 if last_string:
984 yield tokenize.NAME, last_string, last_start, last_end, last_line
985 last_string = None
986 yield type, string, start, end, line
987 # everything inside brackets will be handled by _parse_filter
988 for type, string, start, end, line in tokens:
989 yield type, string, start, end, line
990 if type == tokenize.OP and string == ']':
991 break
992 elif type == tokenize.OP and string in ALLOWED_OPS:
993 if last_string:
994 yield tokenize.NAME, last_string, last_start, last_end, last_line
995 last_string = None
996 yield type, string, start, end, line
997 elif type in [tokenize.NAME, tokenize.NUMBER, tokenize.OP]:
998 if not last_string:
999 last_string = string
1000 last_start = start
1001 last_end = end
1002 else:
1003 last_string += string
1004 if last_string:
1005 yield tokenize.NAME, last_string, last_start, last_end, last_line
1006
1007 def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, inside_group=False):
1008 selectors = []
1009 current_selector = None
1010 for type, string, start, _, _ in tokens:
1011 # ENCODING is only defined in python 3.x
1012 if type == getattr(tokenize, 'ENCODING', None):
1013 continue
1014 elif type in [tokenize.NAME, tokenize.NUMBER]:
1015 current_selector = FormatSelector(SINGLE, string, [])
1016 elif type == tokenize.OP:
1017 if string == ')':
1018 if not inside_group:
1019 # ')' will be handled by the parentheses group
1020 tokens.restore_last_token()
1021 break
1022 elif inside_merge and string in ['/', ',']:
1023 tokens.restore_last_token()
1024 break
1025 elif inside_choice and string == ',':
1026 tokens.restore_last_token()
1027 break
1028 elif string == ',':
1029 if not current_selector:
1030 raise syntax_error('"," must follow a format selector', start)
1031 selectors.append(current_selector)
1032 current_selector = None
1033 elif string == '/':
1034 if not current_selector:
1035 raise syntax_error('"/" must follow a format selector', start)
1036 first_choice = current_selector
1037 second_choice = _parse_format_selection(tokens, inside_choice=True)
1038 current_selector = FormatSelector(PICKFIRST, (first_choice, second_choice), [])
1039 elif string == '[':
1040 if not current_selector:
1041 current_selector = FormatSelector(SINGLE, 'best', [])
1042 format_filter = _parse_filter(tokens)
1043 current_selector.filters.append(format_filter)
1044 elif string == '(':
1045 if current_selector:
1046 raise syntax_error('Unexpected "("', start)
1047 group = _parse_format_selection(tokens, inside_group=True)
1048 current_selector = FormatSelector(GROUP, group, [])
1049 elif string == '+':
1050 video_selector = current_selector
1051 audio_selector = _parse_format_selection(tokens, inside_merge=True)
1052 if not video_selector or not audio_selector:
1053 raise syntax_error('"+" must be between two format selectors', start)
1054 current_selector = FormatSelector(MERGE, (video_selector, audio_selector), [])
1055 else:
1056 raise syntax_error('Operator not recognized: "{0}"'.format(string), start)
1057 elif type == tokenize.ENDMARKER:
1058 break
1059 if current_selector:
1060 selectors.append(current_selector)
1061 return selectors
1062
1063 def _build_selector_function(selector):
1064 if isinstance(selector, list):
1065 fs = [_build_selector_function(s) for s in selector]
1066
1067 def selector_function(ctx):
1068 for f in fs:
1069 for format in f(ctx):
1070 yield format
1071 return selector_function
1072 elif selector.type == GROUP:
1073 selector_function = _build_selector_function(selector.selector)
1074 elif selector.type == PICKFIRST:
1075 fs = [_build_selector_function(s) for s in selector.selector]
1076
1077 def selector_function(ctx):
1078 for f in fs:
1079 picked_formats = list(f(ctx))
1080 if picked_formats:
1081 return picked_formats
1082 return []
1083 elif selector.type == SINGLE:
1084 format_spec = selector.selector
1085
1086 def selector_function(ctx):
1087 formats = list(ctx['formats'])
1088 if not formats:
1089 return
1090 if format_spec == 'all':
1091 for f in formats:
1092 yield f
1093 elif format_spec in ['best', 'worst', None]:
1094 format_idx = 0 if format_spec == 'worst' else -1
1095 audiovideo_formats = [
1096 f for f in formats
1097 if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
1098 if audiovideo_formats:
1099 yield audiovideo_formats[format_idx]
1100 # for extractors with incomplete formats (audio only (soundcloud)
1101 # or video only (imgur)) we will fallback to best/worst
1102 # {video,audio}-only format
1103 elif ctx['incomplete_formats']:
1104 yield formats[format_idx]
1105 elif format_spec == 'bestaudio':
1106 audio_formats = [
1107 f for f in formats
1108 if f.get('vcodec') == 'none']
1109 if audio_formats:
1110 yield audio_formats[-1]
1111 elif format_spec == 'worstaudio':
1112 audio_formats = [
1113 f for f in formats
1114 if f.get('vcodec') == 'none']
1115 if audio_formats:
1116 yield audio_formats[0]
1117 elif format_spec == 'bestvideo':
1118 video_formats = [
1119 f for f in formats
1120 if f.get('acodec') == 'none']
1121 if video_formats:
1122 yield video_formats[-1]
1123 elif format_spec == 'worstvideo':
1124 video_formats = [
1125 f for f in formats
1126 if f.get('acodec') == 'none']
1127 if video_formats:
1128 yield video_formats[0]
1129 else:
1130 extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
1131 if format_spec in extensions:
1132 filter_f = lambda f: f['ext'] == format_spec
1133 else:
1134 filter_f = lambda f: f['format_id'] == format_spec
1135 matches = list(filter(filter_f, formats))
1136 if matches:
1137 yield matches[-1]
1138 elif selector.type == MERGE:
1139 def _merge(formats_info):
1140 format_1, format_2 = [f['format_id'] for f in formats_info]
1141 # The first format must contain the video and the
1142 # second the audio
1143 if formats_info[0].get('vcodec') == 'none':
1144 self.report_error('The first format must '
1145 'contain the video, try using '
1146 '"-f %s+%s"' % (format_2, format_1))
1147 return
1148 # Formats must be opposite (video+audio)
1149 if formats_info[0].get('acodec') == 'none' and formats_info[1].get('acodec') == 'none':
1150 self.report_error(
1151 'Both formats %s and %s are video-only, you must specify "-f video+audio"'
1152 % (format_1, format_2))
1153 return
1154 output_ext = (
1155 formats_info[0]['ext']
1156 if self.params.get('merge_output_format') is None
1157 else self.params['merge_output_format'])
1158 return {
1159 'requested_formats': formats_info,
1160 'format': '%s+%s' % (formats_info[0].get('format'),
1161 formats_info[1].get('format')),
1162 'format_id': '%s+%s' % (formats_info[0].get('format_id'),
1163 formats_info[1].get('format_id')),
1164 'width': formats_info[0].get('width'),
1165 'height': formats_info[0].get('height'),
1166 'resolution': formats_info[0].get('resolution'),
1167 'fps': formats_info[0].get('fps'),
1168 'vcodec': formats_info[0].get('vcodec'),
1169 'vbr': formats_info[0].get('vbr'),
1170 'stretched_ratio': formats_info[0].get('stretched_ratio'),
1171 'acodec': formats_info[1].get('acodec'),
1172 'abr': formats_info[1].get('abr'),
1173 'ext': output_ext,
1174 }
1175 video_selector, audio_selector = map(_build_selector_function, selector.selector)
1176
1177 def selector_function(ctx):
1178 for pair in itertools.product(
1179 video_selector(copy.deepcopy(ctx)), audio_selector(copy.deepcopy(ctx))):
1180 yield _merge(pair)
1181
1182 filters = [self._build_format_filter(f) for f in selector.filters]
1183
1184 def final_selector(ctx):
1185 ctx_copy = copy.deepcopy(ctx)
1186 for _filter in filters:
1187 ctx_copy['formats'] = list(filter(_filter, ctx_copy['formats']))
1188 return selector_function(ctx_copy)
1189 return final_selector
1190
1191 stream = io.BytesIO(format_spec.encode('utf-8'))
1192 try:
1193 tokens = list(_remove_unused_ops(compat_tokenize_tokenize(stream.readline)))
1194 except tokenize.TokenError:
1195 raise syntax_error('Missing closing/opening brackets or parenthesis', (0, len(format_spec)))
1196
1197 class TokenIterator(object):
1198 def __init__(self, tokens):
1199 self.tokens = tokens
1200 self.counter = 0
1201
1202 def __iter__(self):
1203 return self
1204
1205 def __next__(self):
1206 if self.counter >= len(self.tokens):
1207 raise StopIteration()
1208 value = self.tokens[self.counter]
1209 self.counter += 1
1210 return value
1211
1212 next = __next__
1213
1214 def restore_last_token(self):
1215 self.counter -= 1
1216
1217 parsed_selector = _parse_format_selection(iter(TokenIterator(tokens)))
1218 return _build_selector_function(parsed_selector)
1219
1220 def _calc_headers(self, info_dict):
1221 res = std_headers.copy()
1222
1223 add_headers = info_dict.get('http_headers')
1224 if add_headers:
1225 res.update(add_headers)
1226
1227 cookies = self._calc_cookies(info_dict)
1228 if cookies:
1229 res['Cookie'] = cookies
1230
1231 return res
1232
1233 def _calc_cookies(self, info_dict):
1234 pr = sanitized_Request(info_dict['url'])
1235 self.cookiejar.add_cookie_header(pr)
1236 return pr.get_header('Cookie')
1237
1238 def process_video_result(self, info_dict, download=True):
1239 assert info_dict.get('_type', 'video') == 'video'
1240
1241 if 'id' not in info_dict:
1242 raise ExtractorError('Missing "id" field in extractor result')
1243 if 'title' not in info_dict:
1244 raise ExtractorError('Missing "title" field in extractor result')
1245
1246 if not isinstance(info_dict['id'], compat_str):
1247 self.report_warning('"id" field is not a string - forcing string conversion')
1248 info_dict['id'] = compat_str(info_dict['id'])
1249
1250 if 'playlist' not in info_dict:
1251 # It isn't part of a playlist
1252 info_dict['playlist'] = None
1253 info_dict['playlist_index'] = None
1254
1255 thumbnails = info_dict.get('thumbnails')
1256 if thumbnails is None:
1257 thumbnail = info_dict.get('thumbnail')
1258 if thumbnail:
1259 info_dict['thumbnails'] = thumbnails = [{'url': thumbnail}]
1260 if thumbnails:
1261 thumbnails.sort(key=lambda t: (
1262 t.get('preference') if t.get('preference') is not None else -1,
1263 t.get('width') if t.get('width') is not None else -1,
1264 t.get('height') if t.get('height') is not None else -1,
1265 t.get('id') if t.get('id') is not None else '', t.get('url')))
1266 for i, t in enumerate(thumbnails):
1267 t['url'] = sanitize_url(t['url'])
1268 if t.get('width') and t.get('height'):
1269 t['resolution'] = '%dx%d' % (t['width'], t['height'])
1270 if t.get('id') is None:
1271 t['id'] = '%d' % i
1272
1273 if self.params.get('list_thumbnails'):
1274 self.list_thumbnails(info_dict)
1275 return
1276
1277 thumbnail = info_dict.get('thumbnail')
1278 if thumbnail:
1279 info_dict['thumbnail'] = sanitize_url(thumbnail)
1280 elif thumbnails:
1281 info_dict['thumbnail'] = thumbnails[-1]['url']
1282
1283 if 'display_id' not in info_dict and 'id' in info_dict:
1284 info_dict['display_id'] = info_dict['id']
1285
1286 if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
1287 # Working around out-of-range timestamp values (e.g. negative ones on Windows,
1288 # see http://bugs.python.org/issue1646728)
1289 try:
1290 upload_date = datetime.datetime.utcfromtimestamp(info_dict['timestamp'])
1291 info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
1292 except (ValueError, OverflowError, OSError):
1293 pass
1294
1295 # Auto generate title fields corresponding to the *_number fields when missing
1296 # in order to always have clean titles. This is very common for TV series.
1297 for field in ('chapter', 'season', 'episode'):
1298 if info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
1299 info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field])
1300
1301 subtitles = info_dict.get('subtitles')
1302 if subtitles:
1303 for _, subtitle in subtitles.items():
1304 for subtitle_format in subtitle:
1305 if subtitle_format.get('url'):
1306 subtitle_format['url'] = sanitize_url(subtitle_format['url'])
1307 if subtitle_format.get('ext') is None:
1308 subtitle_format['ext'] = determine_ext(subtitle_format['url']).lower()
1309
1310 if self.params.get('listsubtitles', False):
1311 if 'automatic_captions' in info_dict:
1312 self.list_subtitles(info_dict['id'], info_dict.get('automatic_captions'), 'automatic captions')
1313 self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
1314 return
1315 info_dict['requested_subtitles'] = self.process_subtitles(
1316 info_dict['id'], subtitles,
1317 info_dict.get('automatic_captions'))
1318
1319 # We now pick which formats have to be downloaded
1320 if info_dict.get('formats') is None:
1321 # There's only one format available
1322 formats = [info_dict]
1323 else:
1324 formats = info_dict['formats']
1325
1326 if not formats:
1327 raise ExtractorError('No video formats found!')
1328
1329 formats_dict = {}
1330
1331 # We check that all the formats have the format and format_id fields
1332 for i, format in enumerate(formats):
1333 if 'url' not in format:
1334 raise ExtractorError('Missing "url" key in result (index %d)' % i)
1335
1336 format['url'] = sanitize_url(format['url'])
1337
1338 if format.get('format_id') is None:
1339 format['format_id'] = compat_str(i)
1340 else:
1341 # Sanitize format_id from characters used in format selector expression
1342 format['format_id'] = re.sub('[\s,/+\[\]()]', '_', format['format_id'])
1343 format_id = format['format_id']
1344 if format_id not in formats_dict:
1345 formats_dict[format_id] = []
1346 formats_dict[format_id].append(format)
1347
1348 # Make sure all formats have unique format_id
1349 for format_id, ambiguous_formats in formats_dict.items():
1350 if len(ambiguous_formats) > 1:
1351 for i, format in enumerate(ambiguous_formats):
1352 format['format_id'] = '%s-%d' % (format_id, i)
1353
1354 for i, format in enumerate(formats):
1355 if format.get('format') is None:
1356 format['format'] = '{id} - {res}{note}'.format(
1357 id=format['format_id'],
1358 res=self.format_resolution(format),
1359 note=' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
1360 )
1361 # Automatically determine file extension if missing
1362 if format.get('ext') is None:
1363 format['ext'] = determine_ext(format['url']).lower()
1364 # Automatically determine protocol if missing (useful for format
1365 # selection purposes)
1366 if 'protocol' not in format:
1367 format['protocol'] = determine_protocol(format)
1368 # Add HTTP headers, so that external programs can use them from the
1369 # json output
1370 full_format_info = info_dict.copy()
1371 full_format_info.update(format)
1372 format['http_headers'] = self._calc_headers(full_format_info)
1373
1374 # TODO Central sorting goes here
1375
1376 if formats[0] is not info_dict:
1377 # only set the 'formats' fields if the original info_dict list them
1378 # otherwise we end up with a circular reference, the first (and unique)
1379 # element in the 'formats' field in info_dict is info_dict itself,
1380 # which can't be exported to json
1381 info_dict['formats'] = formats
1382 if self.params.get('listformats'):
1383 self.list_formats(info_dict)
1384 return
1385
1386 req_format = self.params.get('format')
1387 if req_format is None:
1388 req_format_list = []
1389 if (self.params.get('outtmpl', DEFAULT_OUTTMPL) != '-' and
1390 not info_dict.get('is_live')):
1391 merger = FFmpegMergerPP(self)
1392 if merger.available and merger.can_merge():
1393 req_format_list.append('bestvideo+bestaudio')
1394 req_format_list.append('best')
1395 req_format = '/'.join(req_format_list)
1396 format_selector = self.build_format_selector(req_format)
1397
1398 # While in format selection we may need to have an access to the original
1399 # format set in order to calculate some metrics or do some processing.
1400 # For now we need to be able to guess whether original formats provided
1401 # by extractor are incomplete or not (i.e. whether extractor provides only
1402 # video-only or audio-only formats) for proper formats selection for
1403 # extractors with such incomplete formats (see
1404 # https://github.com/rg3/youtube-dl/pull/5556).
1405 # Since formats may be filtered during format selection and may not match
1406 # the original formats the results may be incorrect. Thus original formats
1407 # or pre-calculated metrics should be passed to format selection routines
1408 # as well.
1409 # We will pass a context object containing all necessary additional data
1410 # instead of just formats.
1411 # This fixes incorrect format selection issue (see
1412 # https://github.com/rg3/youtube-dl/issues/10083).
1413 incomplete_formats = (
1414 # All formats are video-only or
1415 all(f.get('vcodec') != 'none' and f.get('acodec') == 'none' for f in formats) or
1416 # all formats are audio-only
1417 all(f.get('vcodec') == 'none' and f.get('acodec') != 'none' for f in formats))
1418
1419 ctx = {
1420 'formats': formats,
1421 'incomplete_formats': incomplete_formats,
1422 }
1423
1424 formats_to_download = list(format_selector(ctx))
1425 if not formats_to_download:
1426 raise ExtractorError('requested format not available',
1427 expected=True)
1428
1429 if download:
1430 if len(formats_to_download) > 1:
1431 self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
1432 for format in formats_to_download:
1433 new_info = dict(info_dict)
1434 new_info.update(format)
1435 self.process_info(new_info)
1436 # We update the info dict with the best quality format (backwards compatibility)
1437 info_dict.update(formats_to_download[-1])
1438 return info_dict
1439
1440 def process_subtitles(self, video_id, normal_subtitles, automatic_captions):
1441 """Select the requested subtitles and their format"""
1442 available_subs = {}
1443 if normal_subtitles and self.params.get('writesubtitles'):
1444 available_subs.update(normal_subtitles)
1445 if automatic_captions and self.params.get('writeautomaticsub'):
1446 for lang, cap_info in automatic_captions.items():
1447 if lang not in available_subs:
1448 available_subs[lang] = cap_info
1449
1450 if (not self.params.get('writesubtitles') and not
1451 self.params.get('writeautomaticsub') or not
1452 available_subs):
1453 return None
1454
1455 if self.params.get('allsubtitles', False):
1456 requested_langs = available_subs.keys()
1457 else:
1458 if self.params.get('subtitleslangs', False):
1459 requested_langs = self.params.get('subtitleslangs')
1460 elif 'en' in available_subs:
1461 requested_langs = ['en']
1462 else:
1463 requested_langs = [list(available_subs.keys())[0]]
1464
1465 formats_query = self.params.get('subtitlesformat', 'best')
1466 formats_preference = formats_query.split('/') if formats_query else []
1467 subs = {}
1468 for lang in requested_langs:
1469 formats = available_subs.get(lang)
1470 if formats is None:
1471 self.report_warning('%s subtitles not available for %s' % (lang, video_id))
1472 continue
1473 for ext in formats_preference:
1474 if ext == 'best':
1475 f = formats[-1]
1476 break
1477 matches = list(filter(lambda f: f['ext'] == ext, formats))
1478 if matches:
1479 f = matches[-1]
1480 break
1481 else:
1482 f = formats[-1]
1483 self.report_warning(
1484 'No subtitle format found matching "%s" for language %s, '
1485 'using %s' % (formats_query, lang, f['ext']))
1486 subs[lang] = f
1487 return subs
1488
1489 def process_info(self, info_dict):
1490 """Process a single resolved IE result."""
1491
1492 assert info_dict.get('_type', 'video') == 'video'
1493
1494 max_downloads = self.params.get('max_downloads')
1495 if max_downloads is not None:
1496 if self._num_downloads >= int(max_downloads):
1497 raise MaxDownloadsReached()
1498
1499 info_dict['fulltitle'] = info_dict['title']
1500 if len(info_dict['title']) > 200:
1501 info_dict['title'] = info_dict['title'][:197] + '...'
1502
1503 if 'format' not in info_dict:
1504 info_dict['format'] = info_dict['ext']
1505
1506 reason = self._match_entry(info_dict, incomplete=False)
1507 if reason is not None:
1508 self.to_screen('[download] ' + reason)
1509 return
1510
1511 self._num_downloads += 1
1512
1513 info_dict['_filename'] = filename = self.prepare_filename(info_dict)
1514
1515 # Forced printings
1516 if self.params.get('forcetitle', False):
1517 self.to_stdout(info_dict['fulltitle'])
1518 if self.params.get('forceid', False):
1519 self.to_stdout(info_dict['id'])
1520 if self.params.get('forceurl', False):
1521 if info_dict.get('requested_formats') is not None:
1522 for f in info_dict['requested_formats']:
1523 self.to_stdout(f['url'] + f.get('play_path', ''))
1524 else:
1525 # For RTMP URLs, also include the playpath
1526 self.to_stdout(info_dict['url'] + info_dict.get('play_path', ''))
1527 if self.params.get('forcethumbnail', False) and info_dict.get('thumbnail') is not None:
1528 self.to_stdout(info_dict['thumbnail'])
1529 if self.params.get('forcedescription', False) and info_dict.get('description') is not None:
1530 self.to_stdout(info_dict['description'])
1531 if self.params.get('forcefilename', False) and filename is not None:
1532 self.to_stdout(filename)
1533 if self.params.get('forceduration', False) and info_dict.get('duration') is not None:
1534 self.to_stdout(formatSeconds(info_dict['duration']))
1535 if self.params.get('forceformat', False):
1536 self.to_stdout(info_dict['format'])
1537 if self.params.get('forcejson', False):
1538 self.to_stdout(json.dumps(info_dict))
1539
1540 # Do nothing else if in simulate mode
1541 if self.params.get('simulate', False):
1542 return
1543
1544 if filename is None:
1545 return
1546
1547 try:
1548 dn = os.path.dirname(sanitize_path(encodeFilename(filename)))
1549 if dn and not os.path.exists(dn):
1550 os.makedirs(dn)
1551 except (OSError, IOError) as err:
1552 self.report_error('unable to create directory ' + error_to_compat_str(err))
1553 return
1554
1555 if self.params.get('writedescription', False):
1556 descfn = replace_extension(filename, 'description', info_dict.get('ext'))
1557 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
1558 self.to_screen('[info] Video description is already present')
1559 elif info_dict.get('description') is None:
1560 self.report_warning('There\'s no description to write.')
1561 else:
1562 try:
1563 self.to_screen('[info] Writing video description to: ' + descfn)
1564 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
1565 descfile.write(info_dict['description'])
1566 except (OSError, IOError):
1567 self.report_error('Cannot write description file ' + descfn)
1568 return
1569
1570 if self.params.get('writeannotations', False):
1571 annofn = replace_extension(filename, 'annotations.xml', info_dict.get('ext'))
1572 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
1573 self.to_screen('[info] Video annotations are already present')
1574 else:
1575 try:
1576 self.to_screen('[info] Writing video annotations to: ' + annofn)
1577 with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
1578 annofile.write(info_dict['annotations'])
1579 except (KeyError, TypeError):
1580 self.report_warning('There are no annotations to write.')
1581 except (OSError, IOError):
1582 self.report_error('Cannot write annotations file: ' + annofn)
1583 return
1584
1585 subtitles_are_requested = any([self.params.get('writesubtitles', False),
1586 self.params.get('writeautomaticsub')])
1587
1588 if subtitles_are_requested and info_dict.get('requested_subtitles'):
1589 # subtitles download errors are already managed as troubles in relevant IE
1590 # that way it will silently go on when used with unsupporting IE
1591 subtitles = info_dict['requested_subtitles']
1592 ie = self.get_info_extractor(info_dict['extractor_key'])
1593 for sub_lang, sub_info in subtitles.items():
1594 sub_format = sub_info['ext']
1595 if sub_info.get('data') is not None:
1596 sub_data = sub_info['data']
1597 else:
1598 try:
1599 sub_data = ie._download_webpage(
1600 sub_info['url'], info_dict['id'], note=False)
1601 except ExtractorError as err:
1602 self.report_warning('Unable to download subtitle for "%s": %s' %
1603 (sub_lang, error_to_compat_str(err.cause)))
1604 continue
1605 try:
1606 sub_filename = subtitles_filename(filename, sub_lang, sub_format)
1607 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(sub_filename)):
1608 self.to_screen('[info] Video subtitle %s.%s is already_present' % (sub_lang, sub_format))
1609 else:
1610 self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
1611 # Use newline='' to prevent conversion of newline characters
1612 # See https://github.com/rg3/youtube-dl/issues/10268
1613 with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8', newline='') as subfile:
1614 subfile.write(sub_data)
1615 except (OSError, IOError):
1616 self.report_error('Cannot write subtitles file ' + sub_filename)
1617 return
1618
1619 if self.params.get('writeinfojson', False):
1620 infofn = replace_extension(filename, 'info.json', info_dict.get('ext'))
1621 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
1622 self.to_screen('[info] Video description metadata is already present')
1623 else:
1624 self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
1625 try:
1626 write_json_file(self.filter_requested_info(info_dict), infofn)
1627 except (OSError, IOError):
1628 self.report_error('Cannot write metadata to JSON file ' + infofn)
1629 return
1630
1631 self._write_thumbnails(info_dict, filename)
1632
1633 if not self.params.get('skip_download', False):
1634 try:
1635 def dl(name, info):
1636 fd = get_suitable_downloader(info, self.params)(self, self.params)
1637 for ph in self._progress_hooks:
1638 fd.add_progress_hook(ph)
1639 if self.params.get('verbose'):
1640 self.to_stdout('[debug] Invoking downloader on %r' % info.get('url'))
1641 return fd.download(name, info)
1642
1643 if info_dict.get('requested_formats') is not None:
1644 downloaded = []
1645 success = True
1646 merger = FFmpegMergerPP(self)
1647 if not merger.available:
1648 postprocessors = []
1649 self.report_warning('You have requested multiple '
1650 'formats but ffmpeg or avconv are not installed.'
1651 ' The formats won\'t be merged.')
1652 else:
1653 postprocessors = [merger]
1654
1655 def compatible_formats(formats):
1656 video, audio = formats
1657 # Check extension
1658 video_ext, audio_ext = audio.get('ext'), video.get('ext')
1659 if video_ext and audio_ext:
1660 COMPATIBLE_EXTS = (
1661 ('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'ismv', 'isma'),
1662 ('webm')
1663 )
1664 for exts in COMPATIBLE_EXTS:
1665 if video_ext in exts and audio_ext in exts:
1666 return True
1667 # TODO: Check acodec/vcodec
1668 return False
1669
1670 filename_real_ext = os.path.splitext(filename)[1][1:]
1671 filename_wo_ext = (
1672 os.path.splitext(filename)[0]
1673 if filename_real_ext == info_dict['ext']
1674 else filename)
1675 requested_formats = info_dict['requested_formats']
1676 if self.params.get('merge_output_format') is None and not compatible_formats(requested_formats):
1677 info_dict['ext'] = 'mkv'
1678 self.report_warning(
1679 'Requested formats are incompatible for merge and will be merged into mkv.')
1680 # Ensure filename always has a correct extension for successful merge
1681 filename = '%s.%s' % (filename_wo_ext, info_dict['ext'])
1682 if os.path.exists(encodeFilename(filename)):
1683 self.to_screen(
1684 '[download] %s has already been downloaded and '
1685 'merged' % filename)
1686 else:
1687 for f in requested_formats:
1688 new_info = dict(info_dict)
1689 new_info.update(f)
1690 fname = self.prepare_filename(new_info)
1691 fname = prepend_extension(fname, 'f%s' % f['format_id'], new_info['ext'])
1692 downloaded.append(fname)
1693 partial_success = dl(fname, new_info)
1694 success = success and partial_success
1695 info_dict['__postprocessors'] = postprocessors
1696 info_dict['__files_to_merge'] = downloaded
1697 else:
1698 # Just a single file
1699 success = dl(filename, info_dict)
1700 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1701 self.report_error('unable to download video data: %s' % error_to_compat_str(err))
1702 return
1703 except (OSError, IOError) as err:
1704 raise UnavailableVideoError(err)
1705 except (ContentTooShortError, ) as err:
1706 self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
1707 return
1708
1709 if success and filename != '-':
1710 # Fixup content
1711 fixup_policy = self.params.get('fixup')
1712 if fixup_policy is None:
1713 fixup_policy = 'detect_or_warn'
1714
1715 INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg or avconv to fix this automatically.'
1716
1717 stretched_ratio = info_dict.get('stretched_ratio')
1718 if stretched_ratio is not None and stretched_ratio != 1:
1719 if fixup_policy == 'warn':
1720 self.report_warning('%s: Non-uniform pixel ratio (%s)' % (
1721 info_dict['id'], stretched_ratio))
1722 elif fixup_policy == 'detect_or_warn':
1723 stretched_pp = FFmpegFixupStretchedPP(self)
1724 if stretched_pp.available:
1725 info_dict.setdefault('__postprocessors', [])
1726 info_dict['__postprocessors'].append(stretched_pp)
1727 else:
1728 self.report_warning(
1729 '%s: Non-uniform pixel ratio (%s). %s'
1730 % (info_dict['id'], stretched_ratio, INSTALL_FFMPEG_MESSAGE))
1731 else:
1732 assert fixup_policy in ('ignore', 'never')
1733
1734 if (info_dict.get('requested_formats') is None and
1735 info_dict.get('container') == 'm4a_dash'):
1736 if fixup_policy == 'warn':
1737 self.report_warning(
1738 '%s: writing DASH m4a. '
1739 'Only some players support this container.'
1740 % info_dict['id'])
1741 elif fixup_policy == 'detect_or_warn':
1742 fixup_pp = FFmpegFixupM4aPP(self)
1743 if fixup_pp.available:
1744 info_dict.setdefault('__postprocessors', [])
1745 info_dict['__postprocessors'].append(fixup_pp)
1746 else:
1747 self.report_warning(
1748 '%s: writing DASH m4a. '
1749 'Only some players support this container. %s'
1750 % (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
1751 else:
1752 assert fixup_policy in ('ignore', 'never')
1753
1754 if (info_dict.get('protocol') == 'm3u8_native' or
1755 info_dict.get('protocol') == 'm3u8' and
1756 self.params.get('hls_prefer_native')):
1757 if fixup_policy == 'warn':
1758 self.report_warning('%s: malformated aac bitstream.' % (
1759 info_dict['id']))
1760 elif fixup_policy == 'detect_or_warn':
1761 fixup_pp = FFmpegFixupM3u8PP(self)
1762 if fixup_pp.available:
1763 info_dict.setdefault('__postprocessors', [])
1764 info_dict['__postprocessors'].append(fixup_pp)
1765 else:
1766 self.report_warning(
1767 '%s: malformated aac bitstream. %s'
1768 % (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
1769 else:
1770 assert fixup_policy in ('ignore', 'never')
1771
1772 try:
1773 self.post_process(filename, info_dict)
1774 except (PostProcessingError) as err:
1775 self.report_error('postprocessing: %s' % str(err))
1776 return
1777 self.record_download_archive(info_dict)
1778
1779 def download(self, url_list):
1780 """Download a given list of URLs."""
1781 outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
1782 if (len(url_list) > 1 and
1783 '%' not in outtmpl and
1784 self.params.get('max_downloads') != 1):
1785 raise SameFileError(outtmpl)
1786
1787 for url in url_list:
1788 try:
1789 # It also downloads the videos
1790 res = self.extract_info(
1791 url, force_generic_extractor=self.params.get('force_generic_extractor', False))
1792 except UnavailableVideoError:
1793 self.report_error('unable to download video')
1794 except MaxDownloadsReached:
1795 self.to_screen('[info] Maximum number of downloaded files reached.')
1796 raise
1797 else:
1798 if self.params.get('dump_single_json', False):
1799 self.to_stdout(json.dumps(res))
1800
1801 return self._download_retcode
1802
1803 def download_with_info_file(self, info_filename):
1804 with contextlib.closing(fileinput.FileInput(
1805 [info_filename], mode='r',
1806 openhook=fileinput.hook_encoded('utf-8'))) as f:
1807 # FileInput doesn't have a read method, we can't call json.load
1808 info = self.filter_requested_info(json.loads('\n'.join(f)))
1809 try:
1810 self.process_ie_result(info, download=True)
1811 except DownloadError:
1812 webpage_url = info.get('webpage_url')
1813 if webpage_url is not None:
1814 self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
1815 return self.download([webpage_url])
1816 else:
1817 raise
1818 return self._download_retcode
1819
1820 @staticmethod
1821 def filter_requested_info(info_dict):
1822 return dict(
1823 (k, v) for k, v in info_dict.items()
1824 if k not in ['requested_formats', 'requested_subtitles'])
1825
1826 def post_process(self, filename, ie_info):
1827 """Run all the postprocessors on the given file."""
1828 info = dict(ie_info)
1829 info['filepath'] = filename
1830 pps_chain = []
1831 if ie_info.get('__postprocessors') is not None:
1832 pps_chain.extend(ie_info['__postprocessors'])
1833 pps_chain.extend(self._pps)
1834 for pp in pps_chain:
1835 files_to_delete = []
1836 try:
1837 files_to_delete, info = pp.run(info)
1838 except PostProcessingError as e:
1839 self.report_error(e.msg)
1840 if files_to_delete and not self.params.get('keepvideo', False):
1841 for old_filename in files_to_delete:
1842 self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
1843 try:
1844 os.remove(encodeFilename(old_filename))
1845 except (IOError, OSError):
1846 self.report_warning('Unable to remove downloaded original file')
1847
1848 def _make_archive_id(self, info_dict):
1849 # Future-proof against any change in case
1850 # and backwards compatibility with prior versions
1851 extractor = info_dict.get('extractor_key')
1852 if extractor is None:
1853 if 'id' in info_dict:
1854 extractor = info_dict.get('ie_key') # key in a playlist
1855 if extractor is None:
1856 return None # Incomplete video information
1857 return extractor.lower() + ' ' + info_dict['id']
1858
1859 def in_download_archive(self, info_dict):
1860 fn = self.params.get('download_archive')
1861 if fn is None:
1862 return False
1863
1864 vid_id = self._make_archive_id(info_dict)
1865 if vid_id is None:
1866 return False # Incomplete video information
1867
1868 try:
1869 with locked_file(fn, 'r', encoding='utf-8') as archive_file:
1870 for line in archive_file:
1871 if line.strip() == vid_id:
1872 return True
1873 except IOError as ioe:
1874 if ioe.errno != errno.ENOENT:
1875 raise
1876 return False
1877
1878 def record_download_archive(self, info_dict):
1879 fn = self.params.get('download_archive')
1880 if fn is None:
1881 return
1882 vid_id = self._make_archive_id(info_dict)
1883 assert vid_id
1884 with locked_file(fn, 'a', encoding='utf-8') as archive_file:
1885 archive_file.write(vid_id + '\n')
1886
1887 @staticmethod
1888 def format_resolution(format, default='unknown'):
1889 if format.get('vcodec') == 'none':
1890 return 'audio only'
1891 if format.get('resolution') is not None:
1892 return format['resolution']
1893 if format.get('height') is not None:
1894 if format.get('width') is not None:
1895 res = '%sx%s' % (format['width'], format['height'])
1896 else:
1897 res = '%sp' % format['height']
1898 elif format.get('width') is not None:
1899 res = '%dx?' % format['width']
1900 else:
1901 res = default
1902 return res
1903
1904 def _format_note(self, fdict):
1905 res = ''
1906 if fdict.get('ext') in ['f4f', 'f4m']:
1907 res += '(unsupported) '
1908 if fdict.get('language'):
1909 if res:
1910 res += ' '
1911 res += '[%s] ' % fdict['language']
1912 if fdict.get('format_note') is not None:
1913 res += fdict['format_note'] + ' '
1914 if fdict.get('tbr') is not None:
1915 res += '%4dk ' % fdict['tbr']
1916 if fdict.get('container') is not None:
1917 if res:
1918 res += ', '
1919 res += '%s container' % fdict['container']
1920 if (fdict.get('vcodec') is not None and
1921 fdict.get('vcodec') != 'none'):
1922 if res:
1923 res += ', '
1924 res += fdict['vcodec']
1925 if fdict.get('vbr') is not None:
1926 res += '@'
1927 elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
1928 res += 'video@'
1929 if fdict.get('vbr') is not None:
1930 res += '%4dk' % fdict['vbr']
1931 if fdict.get('fps') is not None:
1932 if res:
1933 res += ', '
1934 res += '%sfps' % fdict['fps']
1935 if fdict.get('acodec') is not None:
1936 if res:
1937 res += ', '
1938 if fdict['acodec'] == 'none':
1939 res += 'video only'
1940 else:
1941 res += '%-5s' % fdict['acodec']
1942 elif fdict.get('abr') is not None:
1943 if res:
1944 res += ', '
1945 res += 'audio'
1946 if fdict.get('abr') is not None:
1947 res += '@%3dk' % fdict['abr']
1948 if fdict.get('asr') is not None:
1949 res += ' (%5dHz)' % fdict['asr']
1950 if fdict.get('filesize') is not None:
1951 if res:
1952 res += ', '
1953 res += format_bytes(fdict['filesize'])
1954 elif fdict.get('filesize_approx') is not None:
1955 if res:
1956 res += ', '
1957 res += '~' + format_bytes(fdict['filesize_approx'])
1958 return res
1959
1960 def list_formats(self, info_dict):
1961 formats = info_dict.get('formats', [info_dict])
1962 table = [
1963 [f['format_id'], f['ext'], self.format_resolution(f), self._format_note(f)]
1964 for f in formats
1965 if f.get('preference') is None or f['preference'] >= -1000]
1966 if len(formats) > 1:
1967 table[-1][-1] += (' ' if table[-1][-1] else '') + '(best)'
1968
1969 header_line = ['format code', 'extension', 'resolution', 'note']
1970 self.to_screen(
1971 '[info] Available formats for %s:\n%s' %
1972 (info_dict['id'], render_table(header_line, table)))
1973
1974 def list_thumbnails(self, info_dict):
1975 thumbnails = info_dict.get('thumbnails')
1976 if not thumbnails:
1977 self.to_screen('[info] No thumbnails present for %s' % info_dict['id'])
1978 return
1979
1980 self.to_screen(
1981 '[info] Thumbnails for %s:' % info_dict['id'])
1982 self.to_screen(render_table(
1983 ['ID', 'width', 'height', 'URL'],
1984 [[t['id'], t.get('width', 'unknown'), t.get('height', 'unknown'), t['url']] for t in thumbnails]))
1985
1986 def list_subtitles(self, video_id, subtitles, name='subtitles'):
1987 if not subtitles:
1988 self.to_screen('%s has no %s' % (video_id, name))
1989 return
1990 self.to_screen(
1991 'Available %s for %s:' % (name, video_id))
1992 self.to_screen(render_table(
1993 ['Language', 'formats'],
1994 [[lang, ', '.join(f['ext'] for f in reversed(formats))]
1995 for lang, formats in subtitles.items()]))
1996
1997 def urlopen(self, req):
1998 """ Start an HTTP download """
1999 if isinstance(req, compat_basestring):
2000 req = sanitized_Request(req)
2001 return self._opener.open(req, timeout=self._socket_timeout)
2002
2003 def print_debug_header(self):
2004 if not self.params.get('verbose'):
2005 return
2006
2007 if type('') is not compat_str:
2008 # Python 2.6 on SLES11 SP1 (https://github.com/rg3/youtube-dl/issues/3326)
2009 self.report_warning(
2010 'Your Python is broken! Update to a newer and supported version')
2011
2012 stdout_encoding = getattr(
2013 sys.stdout, 'encoding', 'missing (%s)' % type(sys.stdout).__name__)
2014 encoding_str = (
2015 '[debug] Encodings: locale %s, fs %s, out %s, pref %s\n' % (
2016 locale.getpreferredencoding(),
2017 sys.getfilesystemencoding(),
2018 stdout_encoding,
2019 self.get_encoding()))
2020 write_string(encoding_str, encoding=None)
2021
2022 self._write_string('[debug] youtube-dl version ' + __version__ + '\n')
2023 if _LAZY_LOADER:
2024 self._write_string('[debug] Lazy loading extractors enabled' + '\n')
2025 try:
2026 sp = subprocess.Popen(
2027 ['git', 'rev-parse', '--short', 'HEAD'],
2028 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2029 cwd=os.path.dirname(os.path.abspath(__file__)))
2030 out, err = sp.communicate()
2031 out = out.decode().strip()
2032 if re.match('[0-9a-f]+', out):
2033 self._write_string('[debug] Git HEAD: ' + out + '\n')
2034 except Exception:
2035 try:
2036 sys.exc_clear()
2037 except Exception:
2038 pass
2039 self._write_string('[debug] Python version %s - %s\n' % (
2040 platform.python_version(), platform_name()))
2041
2042 exe_versions = FFmpegPostProcessor.get_versions(self)
2043 exe_versions['rtmpdump'] = rtmpdump_version()
2044 exe_str = ', '.join(
2045 '%s %s' % (exe, v)
2046 for exe, v in sorted(exe_versions.items())
2047 if v
2048 )
2049 if not exe_str:
2050 exe_str = 'none'
2051 self._write_string('[debug] exe versions: %s\n' % exe_str)
2052
2053 proxy_map = {}
2054 for handler in self._opener.handlers:
2055 if hasattr(handler, 'proxies'):
2056 proxy_map.update(handler.proxies)
2057 self._write_string('[debug] Proxy map: ' + compat_str(proxy_map) + '\n')
2058
2059 if self.params.get('call_home', False):
2060 ipaddr = self.urlopen('https://yt-dl.org/ip').read().decode('utf-8')
2061 self._write_string('[debug] Public IP address: %s\n' % ipaddr)
2062 latest_version = self.urlopen(
2063 'https://yt-dl.org/latest/version').read().decode('utf-8')
2064 if version_tuple(latest_version) > version_tuple(__version__):
2065 self.report_warning(
2066 'You are using an outdated version (newest version: %s)! '
2067 'See https://yt-dl.org/update if you need help updating.' %
2068 latest_version)
2069
2070 def _setup_opener(self):
2071 timeout_val = self.params.get('socket_timeout')
2072 self._socket_timeout = 600 if timeout_val is None else float(timeout_val)
2073
2074 opts_cookiefile = self.params.get('cookiefile')
2075 opts_proxy = self.params.get('proxy')
2076
2077 if opts_cookiefile is None:
2078 self.cookiejar = compat_cookiejar.CookieJar()
2079 else:
2080 opts_cookiefile = compat_expanduser(opts_cookiefile)
2081 self.cookiejar = compat_cookiejar.MozillaCookieJar(
2082 opts_cookiefile)
2083 if os.access(opts_cookiefile, os.R_OK):
2084 self.cookiejar.load()
2085
2086 cookie_processor = YoutubeDLCookieProcessor(self.cookiejar)
2087 if opts_proxy is not None:
2088 if opts_proxy == '':
2089 proxies = {}
2090 else:
2091 proxies = {'http': opts_proxy, 'https': opts_proxy}
2092 else:
2093 proxies = compat_urllib_request.getproxies()
2094 # Set HTTPS proxy to HTTP one if given (https://github.com/rg3/youtube-dl/issues/805)
2095 if 'http' in proxies and 'https' not in proxies:
2096 proxies['https'] = proxies['http']
2097 proxy_handler = PerRequestProxyHandler(proxies)
2098
2099 debuglevel = 1 if self.params.get('debug_printtraffic') else 0
2100 https_handler = make_HTTPS_handler(self.params, debuglevel=debuglevel)
2101 ydlh = YoutubeDLHandler(self.params, debuglevel=debuglevel)
2102 data_handler = compat_urllib_request_DataHandler()
2103
2104 # When passing our own FileHandler instance, build_opener won't add the
2105 # default FileHandler and allows us to disable the file protocol, which
2106 # can be used for malicious purposes (see
2107 # https://github.com/rg3/youtube-dl/issues/8227)
2108 file_handler = compat_urllib_request.FileHandler()
2109
2110 def file_open(*args, **kwargs):
2111 raise compat_urllib_error.URLError('file:// scheme is explicitly disabled in youtube-dl for security reasons')
2112 file_handler.file_open = file_open
2113
2114 opener = compat_urllib_request.build_opener(
2115 proxy_handler, https_handler, cookie_processor, ydlh, data_handler, file_handler)
2116
2117 # Delete the default user-agent header, which would otherwise apply in
2118 # cases where our custom HTTP handler doesn't come into play
2119 # (See https://github.com/rg3/youtube-dl/issues/1309 for details)
2120 opener.addheaders = []
2121 self._opener = opener
2122
2123 def encode(self, s):
2124 if isinstance(s, bytes):
2125 return s # Already encoded
2126
2127 try:
2128 return s.encode(self.get_encoding())
2129 except UnicodeEncodeError as err:
2130 err.reason = err.reason + '. Check your system encoding configuration or use the --encoding option.'
2131 raise
2132
2133 def get_encoding(self):
2134 encoding = self.params.get('encoding')
2135 if encoding is None:
2136 encoding = preferredencoding()
2137 return encoding
2138
2139 def _write_thumbnails(self, info_dict, filename):
2140 if self.params.get('writethumbnail', False):
2141 thumbnails = info_dict.get('thumbnails')
2142 if thumbnails:
2143 thumbnails = [thumbnails[-1]]
2144 elif self.params.get('write_all_thumbnails', False):
2145 thumbnails = info_dict.get('thumbnails')
2146 else:
2147 return
2148
2149 if not thumbnails:
2150 # No thumbnails present, so return immediately
2151 return
2152
2153 for t in thumbnails:
2154 thumb_ext = determine_ext(t['url'], 'jpg')
2155 suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
2156 thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
2157 t['filename'] = thumb_filename = os.path.splitext(filename)[0] + suffix + '.' + thumb_ext
2158
2159 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
2160 self.to_screen('[%s] %s: Thumbnail %sis already present' %
2161 (info_dict['extractor'], info_dict['id'], thumb_display_id))
2162 else:
2163 self.to_screen('[%s] %s: Downloading thumbnail %s...' %
2164 (info_dict['extractor'], info_dict['id'], thumb_display_id))
2165 try:
2166 uf = self.urlopen(t['url'])
2167 with open(encodeFilename(thumb_filename), 'wb') as thumbf:
2168 shutil.copyfileobj(uf, thumbf)
2169 self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
2170 (info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
2171 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
2172 self.report_warning('Unable to download thumbnail "%s": %s' %
2173 (t['url'], error_to_compat_str(err)))