]> jfr.im git - yt-dlp.git/blame - youtube_dl/YoutubeDL.py
Merge remote-tracking branch 'anovicecodemonkey/generic-data-video-url'
[yt-dlp.git] / youtube_dl / YoutubeDL.py
CommitLineData
8222d8de
JMF
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
6febd1c1 4from __future__ import absolute_import, unicode_literals
8222d8de 5
26e63931 6import collections
9d2ecdbc 7import datetime
c1c9a79c 8import errno
8222d8de 9import io
8694c600 10import json
62fec3b2 11import locale
8222d8de 12import os
dca08720 13import platform
8222d8de
JMF
14import re
15import shutil
dca08720 16import subprocess
8222d8de
JMF
17import socket
18import sys
19import time
20import traceback
21
1e5b9a95
PH
22if os.name == 'nt':
23 import ctypes
24
ce02ed60 25from .utils import (
dca08720 26 compat_cookiejar,
ce02ed60 27 compat_http_client,
ce02ed60
PH
28 compat_str,
29 compat_urllib_error,
30 compat_urllib_request,
31 ContentTooShortError,
32 date_from_str,
33 DateRange,
acd69589 34 DEFAULT_OUTTMPL,
ce02ed60
PH
35 determine_ext,
36 DownloadError,
37 encodeFilename,
38 ExtractorError,
02dbf93f 39 format_bytes,
525ef922 40 formatSeconds,
1c088fa8 41 get_term_width,
ce02ed60 42 locked_file,
dca08720 43 make_HTTPS_handler,
ce02ed60 44 MaxDownloadsReached,
b7ab0590 45 PagedList,
ce02ed60 46 PostProcessingError,
dca08720 47 platform_name,
ce02ed60
PH
48 preferredencoding,
49 SameFileError,
50 sanitize_filename,
51 subtitles_filename,
52 takewhile_inclusive,
53 UnavailableVideoError,
29eb5174 54 url_basename,
ce02ed60
PH
55 write_json_file,
56 write_string,
dca08720 57 YoutubeDLHandler,
6350728b 58 prepend_extension,
ce02ed60 59)
023fa8c4 60from .extractor import get_info_extractor, gen_extractors
3bc2ddcc 61from .downloader import get_suitable_downloader
56327689 62from .postprocessor import FFmpegMergerPP
dca08720 63from .version import __version__
8222d8de
JMF
64
65
66class YoutubeDL(object):
67 """YoutubeDL class.
68
69 YoutubeDL objects are the ones responsible of downloading the
70 actual video file and writing it to disk if the user has requested
71 it, among some other tasks. In most cases there should be one per
72 program. As, given a video URL, the downloader doesn't know how to
73 extract all the needed information, task that InfoExtractors do, it
74 has to pass the URL to one of them.
75
76 For this, YoutubeDL objects have a method that allows
77 InfoExtractors to be registered in a given order. When it is passed
78 a URL, the YoutubeDL object handles it to the first InfoExtractor it
79 finds that reports being able to handle it. The InfoExtractor extracts
80 all the information about the video or videos the URL refers to, and
81 YoutubeDL process the extracted information, possibly using a File
82 Downloader to download the video.
83
84 YoutubeDL objects accept a lot of parameters. In order not to saturate
85 the object constructor with arguments, it receives a dictionary of
86 options instead. These options are available through the params
87 attribute for the InfoExtractors to use. The YoutubeDL also
88 registers itself as the downloader in charge for the InfoExtractors
89 that are added to it, so this is a "mutual registration".
90
91 Available options:
92
93 username: Username for authentication purposes.
94 password: Password for authentication purposes.
c6c19746 95 videopassword: Password for acces a video.
8222d8de
JMF
96 usenetrc: Use netrc for authentication instead.
97 verbose: Print additional info to stdout.
98 quiet: Do not print messages to stdout.
ad8915b7 99 no_warnings: Do not print out anything for warnings.
8222d8de
JMF
100 forceurl: Force printing final URL.
101 forcetitle: Force printing title.
102 forceid: Force printing ID.
103 forcethumbnail: Force printing thumbnail URL.
104 forcedescription: Force printing description.
105 forcefilename: Force printing final filename.
525ef922 106 forceduration: Force printing duration.
8694c600 107 forcejson: Force printing info_dict as JSON.
8222d8de
JMF
108 simulate: Do not download the video files.
109 format: Video format code.
110 format_limit: Highest quality format to try.
111 outtmpl: Template for output names.
112 restrictfilenames: Do not allow "&" and spaces in file names
113 ignoreerrors: Do not stop on download errors.
114 nooverwrites: Prevent overwriting files.
115 playliststart: Playlist item to start at.
116 playlistend: Playlist item to end at.
117 matchtitle: Download only matching titles.
118 rejecttitle: Reject downloads for matching titles.
8bf9319e 119 logger: Log messages to a logging.Logger instance.
8222d8de
JMF
120 logtostderr: Log messages to stderr instead of stdout.
121 writedescription: Write the video description to a .description file
122 writeinfojson: Write the video description to a .info.json file
1fb07d10 123 writeannotations: Write the video annotations to a .annotations.xml file
8222d8de
JMF
124 writethumbnail: Write the thumbnail image to a file
125 writesubtitles: Write the video subtitles to a file
b004821f 126 writeautomaticsub: Write the automatic subtitles to a file
8222d8de 127 allsubtitles: Downloads all the subtitles of the video
0b7f3118 128 (requires writesubtitles or writeautomaticsub)
8222d8de 129 listsubtitles: Lists all available subtitles for the video
b98a6b2f 130 subtitlesformat: Subtitle format [srt/sbv/vtt] (default=srt)
aa6a10c4 131 subtitleslangs: List of languages of the subtitles to download
8222d8de
JMF
132 keepvideo: Keep the video file after post-processing
133 daterange: A DateRange object, download only if the upload_date is in the range.
134 skip_download: Skip the actual download of the video file
c35f9e72 135 cachedir: Location of the cache files in the filesystem.
c3c88a26 136 None to disable filesystem cache.
47192f92 137 noplaylist: Download single video instead of a playlist if in doubt.
8dbe9899
PH
138 age_limit: An integer representing the user's age in years.
139 Unsuitable videos for the given age are skipped.
5fe18bdb
PH
140 min_views: An integer representing the minimum view count the video
141 must have in order to not be skipped.
142 Videos without view count information are always
143 downloaded. None for no limit.
144 max_views: An integer representing the maximum view count.
145 Videos that are more popular than that are not
146 downloaded.
147 Videos without view count information are always
148 downloaded. None for no limit.
149 download_archive: File name of a file where all downloads are recorded.
c1c9a79c
PH
150 Videos already present in the file are not downloaded
151 again.
dca08720 152 cookiefile: File name where cookies should be read from and dumped to.
a1ee09e8 153 nocheckcertificate:Do not verify SSL certificates
7e8c0af0
PH
154 prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
155 At the moment, this is only supported by YouTube.
a1ee09e8 156 proxy: URL of the proxy server to use
e344693b 157 socket_timeout: Time to wait for unresponsive hosts, in seconds
0783b09b
PH
158 bidi_workaround: Work around buggy terminals without bidirectional text
159 support, using fridibi
a0ddb8a2 160 debug_printtraffic:Print out sent and received HTTP traffic
7b0817e8 161 include_ads: Download ads as well
04b4d394
PH
162 default_search: Prepend this string if an input url is not valid.
163 'auto' for elaborate guessing
62fec3b2 164 encoding: Use this encoding instead of the system-specified.
e8ee972c 165 extract_flat: Do not resolve URLs, return the immediate result.
fe7e0c98 166
8222d8de
JMF
167 The following parameters are not used by YoutubeDL itself, they are used by
168 the FileDownloader:
169 nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
170 noresizebuffer, retries, continuedl, noprogress, consoletitle
76b1bd67
JMF
171
172 The following options are used by the post processors:
173 prefer_ffmpeg: If True, use ffmpeg instead of avconv if both are available,
174 otherwise prefer avconv.
8222d8de
JMF
175 """
176
177 params = None
178 _ies = []
179 _pps = []
180 _download_retcode = None
181 _num_downloads = None
182 _screen_file = None
183
a3fb4675 184 def __init__(self, params=None):
8222d8de 185 """Create a FileDownloader object with the given options."""
e9f9a10f
JMF
186 if params is None:
187 params = {}
8222d8de 188 self._ies = []
56c73665 189 self._ies_instances = {}
8222d8de 190 self._pps = []
933605d7 191 self._progress_hooks = []
8222d8de
JMF
192 self._download_retcode = 0
193 self._num_downloads = 0
194 self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
0783b09b 195 self._err_file = sys.stderr
e9f9a10f 196 self.params = params
34308b30 197
0783b09b 198 if params.get('bidi_workaround', False):
1c088fa8
PH
199 try:
200 import pty
201 master, slave = pty.openpty()
202 width = get_term_width()
203 if width is None:
204 width_args = []
205 else:
206 width_args = ['-w', str(width)]
5d681e96 207 sp_kwargs = dict(
1c088fa8
PH
208 stdin=subprocess.PIPE,
209 stdout=slave,
210 stderr=self._err_file)
5d681e96
PH
211 try:
212 self._output_process = subprocess.Popen(
213 ['bidiv'] + width_args, **sp_kwargs
214 )
215 except OSError:
5d681e96
PH
216 self._output_process = subprocess.Popen(
217 ['fribidi', '-c', 'UTF-8'] + width_args, **sp_kwargs)
218 self._output_channel = os.fdopen(master, 'rb')
1c088fa8
PH
219 except OSError as ose:
220 if ose.errno == 2:
6febd1c1 221 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.')
1c088fa8
PH
222 else:
223 raise
0783b09b 224
34308b30
PH
225 if (sys.version_info >= (3,) and sys.platform != 'win32' and
226 sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
227 and not params['restrictfilenames']):
228 # On Python 3, the Unicode filesystem API will throw errors (#1474)
229 self.report_warning(
6febd1c1
PH
230 'Assuming --restrict-filenames since file system encoding '
231 'cannot encode all charactes. '
232 'Set the LC_ALL environment variable to fix this.')
4a98cdbf 233 self.params['restrictfilenames'] = True
34308b30 234
a3927cf7 235 if '%(stitle)s' in self.params.get('outtmpl', ''):
6febd1c1 236 self.report_warning('%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
8222d8de 237
dca08720
PH
238 self._setup_opener()
239
8222d8de
JMF
240 def add_info_extractor(self, ie):
241 """Add an InfoExtractor object to the end of the list."""
242 self._ies.append(ie)
56c73665 243 self._ies_instances[ie.ie_key()] = ie
8222d8de
JMF
244 ie.set_downloader(self)
245
56c73665
JMF
246 def get_info_extractor(self, ie_key):
247 """
248 Get an instance of an IE with name ie_key, it will try to get one from
249 the _ies list, if there's no instance it will create a new one and add
250 it to the extractor list.
251 """
252 ie = self._ies_instances.get(ie_key)
253 if ie is None:
254 ie = get_info_extractor(ie_key)()
255 self.add_info_extractor(ie)
256 return ie
257
023fa8c4
JMF
258 def add_default_info_extractors(self):
259 """
260 Add the InfoExtractors returned by gen_extractors to the end of the list
261 """
262 for ie in gen_extractors():
263 self.add_info_extractor(ie)
264
8222d8de
JMF
265 def add_post_processor(self, pp):
266 """Add a PostProcessor object to the end of the chain."""
267 self._pps.append(pp)
268 pp.set_downloader(self)
269
933605d7
JMF
270 def add_progress_hook(self, ph):
271 """Add the progress hook (currently only for the file downloader)"""
272 self._progress_hooks.append(ph)
8ab470f1 273
1c088fa8 274 def _bidi_workaround(self, message):
5d681e96 275 if not hasattr(self, '_output_channel'):
1c088fa8
PH
276 return message
277
5d681e96 278 assert hasattr(self, '_output_process')
11b85ce6 279 assert isinstance(message, compat_str)
6febd1c1
PH
280 line_count = message.count('\n') + 1
281 self._output_process.stdin.write((message + '\n').encode('utf-8'))
5d681e96 282 self._output_process.stdin.flush()
6febd1c1 283 res = ''.join(self._output_channel.readline().decode('utf-8')
1c088fa8 284 for _ in range(line_count))
6febd1c1 285 return res[:-len('\n')]
1c088fa8 286
8222d8de 287 def to_screen(self, message, skip_eol=False):
0783b09b
PH
288 """Print message to stdout if not in quiet mode."""
289 return self.to_stdout(message, skip_eol, check_quiet=True)
290
734f90bb 291 def _write_string(self, s, out=None):
b58ddb32 292 write_string(s, out=out, encoding=self.params.get('encoding'))
734f90bb 293
0783b09b 294 def to_stdout(self, message, skip_eol=False, check_quiet=False):
8222d8de 295 """Print message to stdout if not in quiet mode."""
8bf9319e 296 if self.params.get('logger'):
43afe285 297 self.params['logger'].debug(message)
0783b09b 298 elif not check_quiet or not self.params.get('quiet', False):
1c088fa8 299 message = self._bidi_workaround(message)
6febd1c1 300 terminator = ['\n', ''][skip_eol]
8222d8de 301 output = message + terminator
1c088fa8 302
734f90bb 303 self._write_string(output, self._screen_file)
8222d8de
JMF
304
305 def to_stderr(self, message):
306 """Print message to stderr."""
11b85ce6 307 assert isinstance(message, compat_str)
8bf9319e 308 if self.params.get('logger'):
43afe285
IB
309 self.params['logger'].error(message)
310 else:
1c088fa8 311 message = self._bidi_workaround(message)
6febd1c1 312 output = message + '\n'
734f90bb 313 self._write_string(output, self._err_file)
8222d8de 314
1e5b9a95
PH
315 def to_console_title(self, message):
316 if not self.params.get('consoletitle', False):
317 return
318 if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
319 # c_wchar_p() might not be necessary if `message` is
320 # already of type unicode()
321 ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
322 elif 'TERM' in os.environ:
734f90bb 323 self._write_string('\033]0;%s\007' % message, self._screen_file)
1e5b9a95 324
bdde425c
PH
325 def save_console_title(self):
326 if not self.params.get('consoletitle', False):
327 return
328 if 'TERM' in os.environ:
efd6c574 329 # Save the title on stack
734f90bb 330 self._write_string('\033[22;0t', self._screen_file)
bdde425c
PH
331
332 def restore_console_title(self):
333 if not self.params.get('consoletitle', False):
334 return
335 if 'TERM' in os.environ:
efd6c574 336 # Restore the title from stack
734f90bb 337 self._write_string('\033[23;0t', self._screen_file)
bdde425c
PH
338
339 def __enter__(self):
340 self.save_console_title()
341 return self
342
343 def __exit__(self, *args):
344 self.restore_console_title()
f89197d7 345
dca08720
PH
346 if self.params.get('cookiefile') is not None:
347 self.cookiejar.save()
bdde425c 348
8222d8de
JMF
349 def trouble(self, message=None, tb=None):
350 """Determine action to take when a download problem appears.
351
352 Depending on if the downloader has been configured to ignore
353 download errors or not, this method may throw an exception or
354 not when errors are found, after printing the message.
355
356 tb, if given, is additional traceback information.
357 """
358 if message is not None:
359 self.to_stderr(message)
360 if self.params.get('verbose'):
361 if tb is None:
362 if sys.exc_info()[0]: # if .trouble has been called from an except block
6febd1c1 363 tb = ''
8222d8de 364 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
6febd1c1 365 tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
8222d8de
JMF
366 tb += compat_str(traceback.format_exc())
367 else:
368 tb_data = traceback.format_list(traceback.extract_stack())
6febd1c1 369 tb = ''.join(tb_data)
8222d8de
JMF
370 self.to_stderr(tb)
371 if not self.params.get('ignoreerrors', False):
372 if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
373 exc_info = sys.exc_info()[1].exc_info
374 else:
375 exc_info = sys.exc_info()
376 raise DownloadError(message, exc_info)
377 self._download_retcode = 1
378
379 def report_warning(self, message):
380 '''
381 Print the message to stderr, it will be prefixed with 'WARNING:'
382 If stderr is a tty file the 'WARNING:' will be colored
383 '''
6d07ce01
JMF
384 if self.params.get('logger') is not None:
385 self.params['logger'].warning(message)
8222d8de 386 else:
ad8915b7
PH
387 if self.params.get('no_warnings'):
388 return
6d07ce01
JMF
389 if self._err_file.isatty() and os.name != 'nt':
390 _msg_header = '\033[0;33mWARNING:\033[0m'
391 else:
392 _msg_header = 'WARNING:'
393 warning_message = '%s %s' % (_msg_header, message)
394 self.to_stderr(warning_message)
8222d8de
JMF
395
396 def report_error(self, message, tb=None):
397 '''
398 Do the same as trouble, but prefixes the message with 'ERROR:', colored
399 in red if stderr is a tty file.
400 '''
0783b09b 401 if self._err_file.isatty() and os.name != 'nt':
6febd1c1 402 _msg_header = '\033[0;31mERROR:\033[0m'
8222d8de 403 else:
6febd1c1
PH
404 _msg_header = 'ERROR:'
405 error_message = '%s %s' % (_msg_header, message)
8222d8de
JMF
406 self.trouble(error_message, tb)
407
8222d8de
JMF
408 def report_file_already_downloaded(self, file_name):
409 """Report file has already been fully downloaded."""
410 try:
6febd1c1 411 self.to_screen('[download] %s has already been downloaded' % file_name)
ce02ed60 412 except UnicodeEncodeError:
6febd1c1 413 self.to_screen('[download] The file has already been downloaded')
8222d8de 414
8222d8de
JMF
415 def prepare_filename(self, info_dict):
416 """Generate the output filename."""
417 try:
418 template_dict = dict(info_dict)
419
420 template_dict['epoch'] = int(time.time())
421 autonumber_size = self.params.get('autonumber_size')
422 if autonumber_size is None:
423 autonumber_size = 5
6febd1c1 424 autonumber_templ = '%0' + str(autonumber_size) + 'd'
8222d8de 425 template_dict['autonumber'] = autonumber_templ % self._num_downloads
702665c0 426 if template_dict.get('playlist_index') is not None:
6febd1c1 427 template_dict['playlist_index'] = '%05d' % template_dict['playlist_index']
17b75c0d
PH
428 if template_dict.get('resolution') is None:
429 if template_dict.get('width') and template_dict.get('height'):
430 template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
431 elif template_dict.get('height'):
805ef3c6 432 template_dict['resolution'] = '%sp' % template_dict['height']
17b75c0d 433 elif template_dict.get('width'):
805ef3c6 434 template_dict['resolution'] = '?x%d' % template_dict['width']
8222d8de 435
586a91b6 436 sanitize = lambda k, v: sanitize_filename(
45598aab 437 compat_str(v),
8222d8de 438 restricted=self.params.get('restrictfilenames'),
6febd1c1 439 is_id=(k == 'id'))
586a91b6 440 template_dict = dict((k, sanitize(k, v))
45598aab
PH
441 for k, v in template_dict.items()
442 if v is not None)
6febd1c1 443 template_dict = collections.defaultdict(lambda: 'NA', template_dict)
8222d8de 444
acd69589
PH
445 outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
446 tmpl = os.path.expanduser(outtmpl)
586a91b6 447 filename = tmpl % template_dict
8222d8de 448 return filename
8222d8de 449 except ValueError as err:
6febd1c1 450 self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
8222d8de
JMF
451 return None
452
453 def _match_entry(self, info_dict):
454 """ Returns None iff the file should be downloaded """
455
6febd1c1 456 video_title = info_dict.get('title', info_dict.get('id', 'video'))
7012b23c
PH
457 if 'title' in info_dict:
458 # This can happen when we're just evaluating the playlist
459 title = info_dict['title']
460 matchtitle = self.params.get('matchtitle', False)
461 if matchtitle:
462 if not re.search(matchtitle, title, re.IGNORECASE):
6febd1c1 463 return '"' + title + '" title did not match pattern "' + matchtitle + '"'
7012b23c
PH
464 rejecttitle = self.params.get('rejecttitle', False)
465 if rejecttitle:
466 if re.search(rejecttitle, title, re.IGNORECASE):
6febd1c1 467 return '"' + title + '" title matched reject pattern "' + rejecttitle + '"'
8222d8de
JMF
468 date = info_dict.get('upload_date', None)
469 if date is not None:
470 dateRange = self.params.get('daterange', DateRange())
471 if date not in dateRange:
6febd1c1 472 return '%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
5fe18bdb
PH
473 view_count = info_dict.get('view_count', None)
474 if view_count is not None:
475 min_views = self.params.get('min_views')
476 if min_views is not None and view_count < min_views:
6febd1c1 477 return 'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
5fe18bdb
PH
478 max_views = self.params.get('max_views')
479 if max_views is not None and view_count > max_views:
6febd1c1 480 return 'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
8dbe9899
PH
481 age_limit = self.params.get('age_limit')
482 if age_limit is not None:
cfadd183 483 if age_limit < info_dict.get('age_limit', 0):
6febd1c1 484 return 'Skipping "' + title + '" because it is age restricted'
c1c9a79c 485 if self.in_download_archive(info_dict):
6febd1c1 486 return '%s has already been recorded in archive' % video_title
8222d8de 487 return None
fe7e0c98 488
b6c45014
JMF
489 @staticmethod
490 def add_extra_info(info_dict, extra_info):
491 '''Set the keys from extra_info in info dict if they are missing'''
492 for key, value in extra_info.items():
493 info_dict.setdefault(key, value)
494
7fc3fa05
PH
495 def extract_info(self, url, download=True, ie_key=None, extra_info={},
496 process=True):
8222d8de
JMF
497 '''
498 Returns a list with a dictionary for each video we find.
499 If 'download', also downloads the videos.
500 extra_info is a dict containing the extra values to add to each result
501 '''
fe7e0c98 502
8222d8de 503 if ie_key:
56c73665 504 ies = [self.get_info_extractor(ie_key)]
8222d8de
JMF
505 else:
506 ies = self._ies
507
508 for ie in ies:
509 if not ie.suitable(url):
510 continue
511
512 if not ie.working():
6febd1c1
PH
513 self.report_warning('The program functionality for this site has been marked as broken, '
514 'and will probably not work.')
8222d8de
JMF
515
516 try:
517 ie_result = ie.extract(url)
518 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
519 break
520 if isinstance(ie_result, list):
521 # Backwards compatibility: old IE result format
8222d8de
JMF
522 ie_result = {
523 '_type': 'compat_list',
524 'entries': ie_result,
525 }
ea38e55f 526 self.add_default_extra_info(ie_result, ie, url)
7fc3fa05
PH
527 if process:
528 return self.process_ie_result(ie_result, download, extra_info)
529 else:
530 return ie_result
8222d8de
JMF
531 except ExtractorError as de: # An error we somewhat expected
532 self.report_error(compat_str(de), de.format_traceback())
533 break
d3e5bbf4
PH
534 except MaxDownloadsReached:
535 raise
8222d8de
JMF
536 except Exception as e:
537 if self.params.get('ignoreerrors', False):
538 self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
539 break
540 else:
541 raise
542 else:
1a489545 543 self.report_error('no suitable InfoExtractor for URL %s' % url)
fe7e0c98 544
ea38e55f
PH
545 def add_default_extra_info(self, ie_result, ie, url):
546 self.add_extra_info(ie_result, {
547 'extractor': ie.IE_NAME,
548 'webpage_url': url,
549 'webpage_url_basename': url_basename(url),
550 'extractor_key': ie.ie_key(),
551 })
552
8222d8de
JMF
553 def process_ie_result(self, ie_result, download=True, extra_info={}):
554 """
555 Take the result of the ie(may be modified) and resolve all unresolved
556 references (URLs, playlist items).
557
558 It will also download the videos if 'download'.
559 Returns the resolved ie_result.
560 """
561
e8ee972c
PH
562 result_type = ie_result.get('_type', 'video')
563
564 if self.params.get('extract_flat', False):
565 if result_type in ('url', 'url_transparent'):
566 return ie_result
567
8222d8de 568 if result_type == 'video':
b6c45014 569 self.add_extra_info(ie_result, extra_info)
feee2ecf 570 return self.process_video_result(ie_result, download=download)
8222d8de
JMF
571 elif result_type == 'url':
572 # We have to add extra_info to the results because it may be
573 # contained in a playlist
574 return self.extract_info(ie_result['url'],
575 download,
576 ie_key=ie_result.get('ie_key'),
577 extra_info=extra_info)
7fc3fa05
PH
578 elif result_type == 'url_transparent':
579 # Use the information from the embedding page
580 info = self.extract_info(
581 ie_result['url'], ie_key=ie_result.get('ie_key'),
582 extra_info=extra_info, download=False, process=False)
583
584 def make_result(embedded_info):
585 new_result = ie_result.copy()
586 for f in ('_type', 'url', 'ext', 'player_url', 'formats',
1538eff6 587 'entries', 'ie_key', 'duration',
ef4fd848
PH
588 'subtitles', 'annotations', 'format',
589 'thumbnail', 'thumbnails'):
7fc3fa05
PH
590 if f in new_result:
591 del new_result[f]
592 if f in embedded_info:
593 new_result[f] = embedded_info[f]
594 return new_result
595 new_result = make_result(info)
596
597 assert new_result.get('_type') != 'url_transparent'
598 if new_result.get('_type') == 'compat_list':
599 new_result['entries'] = [
600 make_result(e) for e in new_result['entries']]
601
602 return self.process_ie_result(
603 new_result, download=download, extra_info=extra_info)
8222d8de
JMF
604 elif result_type == 'playlist':
605 # We process each entry in the playlist
606 playlist = ie_result.get('title', None) or ie_result.get('id', None)
6febd1c1 607 self.to_screen('[download] Downloading playlist: %s' % playlist)
8222d8de
JMF
608
609 playlist_results = []
610
8222d8de 611 playliststart = self.params.get('playliststart', 1) - 1
a19fd00c
PH
612 playlistend = self.params.get('playlistend', None)
613 # For backwards compatibility, interpret -1 as whole list
8222d8de 614 if playlistend == -1:
a19fd00c 615 playlistend = None
8222d8de 616
b7ab0590
PH
617 if isinstance(ie_result['entries'], list):
618 n_all_entries = len(ie_result['entries'])
619 entries = ie_result['entries'][playliststart:playlistend]
620 n_entries = len(entries)
621 self.to_screen(
622 "[%s] playlist %s: Collected %d video ids (downloading %d of them)" %
623 (ie_result['extractor'], playlist, n_all_entries, n_entries))
624 else:
625 assert isinstance(ie_result['entries'], PagedList)
626 entries = ie_result['entries'].getslice(
627 playliststart, playlistend)
628 n_entries = len(entries)
629 self.to_screen(
630 "[%s] playlist %s: Downloading %d videos" %
631 (ie_result['extractor'], playlist, n_entries))
8222d8de 632
fe7e0c98 633 for i, entry in enumerate(entries, 1):
6febd1c1 634 self.to_screen('[download] Downloading video #%s of %s' % (i, n_entries))
8222d8de 635 extra = {
fe7e0c98
JMF
636 'playlist': playlist,
637 'playlist_index': i + playliststart,
b6c45014 638 'extractor': ie_result['extractor'],
9103bbc5 639 'webpage_url': ie_result['webpage_url'],
29eb5174 640 'webpage_url_basename': url_basename(ie_result['webpage_url']),
be97abc2 641 'extractor_key': ie_result['extractor_key'],
fe7e0c98 642 }
7012b23c
PH
643
644 reason = self._match_entry(entry)
645 if reason is not None:
6febd1c1 646 self.to_screen('[download] ' + reason)
7012b23c
PH
647 continue
648
8222d8de
JMF
649 entry_result = self.process_ie_result(entry,
650 download=download,
651 extra_info=extra)
652 playlist_results.append(entry_result)
653 ie_result['entries'] = playlist_results
654 return ie_result
655 elif result_type == 'compat_list':
656 def _fixup(r):
b6c45014 657 self.add_extra_info(r,
9103bbc5
JMF
658 {
659 'extractor': ie_result['extractor'],
660 'webpage_url': ie_result['webpage_url'],
29eb5174 661 'webpage_url_basename': url_basename(ie_result['webpage_url']),
be97abc2 662 'extractor_key': ie_result['extractor_key'],
9103bbc5 663 })
8222d8de
JMF
664 return r
665 ie_result['entries'] = [
b6c45014 666 self.process_ie_result(_fixup(r), download, extra_info)
8222d8de
JMF
667 for r in ie_result['entries']
668 ]
669 return ie_result
670 else:
671 raise Exception('Invalid result type: %s' % result_type)
672
a9c58ad9
JMF
673 def select_format(self, format_spec, available_formats):
674 if format_spec == 'best' or format_spec is None:
675 return available_formats[-1]
676 elif format_spec == 'worst':
677 return available_formats[0]
ba7678f9
PH
678 elif format_spec == 'bestaudio':
679 audio_formats = [
680 f for f in available_formats
681 if f.get('vcodec') == 'none']
682 if audio_formats:
683 return audio_formats[-1]
684 elif format_spec == 'worstaudio':
685 audio_formats = [
686 f for f in available_formats
687 if f.get('vcodec') == 'none']
688 if audio_formats:
689 return audio_formats[0]
bc6d5978
JMF
690 elif format_spec == 'bestvideo':
691 video_formats = [
692 f for f in available_formats
693 if f.get('acodec') == 'none']
694 if video_formats:
695 return video_formats[-1]
696 elif format_spec == 'worstvideo':
697 video_formats = [
698 f for f in available_formats
699 if f.get('acodec') == 'none']
700 if video_formats:
701 return video_formats[0]
a9c58ad9 702 else:
6febd1c1 703 extensions = ['mp4', 'flv', 'webm', '3gp']
49e86983
JMF
704 if format_spec in extensions:
705 filter_f = lambda f: f['ext'] == format_spec
706 else:
707 filter_f = lambda f: f['format_id'] == format_spec
fe7e0c98 708 matches = list(filter(filter_f, available_formats))
a9c58ad9
JMF
709 if matches:
710 return matches[-1]
711 return None
712
dd82ffea
JMF
713 def process_video_result(self, info_dict, download=True):
714 assert info_dict.get('_type', 'video') == 'video'
715
bec1fad2
PH
716 if 'id' not in info_dict:
717 raise ExtractorError('Missing "id" field in extractor result')
718 if 'title' not in info_dict:
719 raise ExtractorError('Missing "title" field in extractor result')
720
dd82ffea
JMF
721 if 'playlist' not in info_dict:
722 # It isn't part of a playlist
723 info_dict['playlist'] = None
724 info_dict['playlist_index'] = None
725
d5519808
PH
726 thumbnails = info_dict.get('thumbnails')
727 if thumbnails:
be6d7229
PH
728 thumbnails.sort(key=lambda t: (
729 t.get('width'), t.get('height'), t.get('url')))
d5519808
PH
730 for t in thumbnails:
731 if 'width' in t and 'height' in t:
732 t['resolution'] = '%dx%d' % (t['width'], t['height'])
733
734 if thumbnails and 'thumbnail' not in info_dict:
735 info_dict['thumbnail'] = thumbnails[-1]['url']
736
c9ae7b95 737 if 'display_id' not in info_dict and 'id' in info_dict:
0afef30b
PH
738 info_dict['display_id'] = info_dict['id']
739
955c4514 740 if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
9d2ecdbc 741 upload_date = datetime.datetime.utcfromtimestamp(
955c4514 742 info_dict['timestamp'])
9d2ecdbc
PH
743 info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
744
6ff000b8 745 # This extractors handle format selection themselves
6febd1c1 746 if info_dict['extractor'] in ['Youku']:
12893efe
JMF
747 if download:
748 self.process_info(info_dict)
6ff000b8
JMF
749 return info_dict
750
dd82ffea
JMF
751 # We now pick which formats have to be downloaded
752 if info_dict.get('formats') is None:
753 # There's only one format available
754 formats = [info_dict]
755 else:
756 formats = info_dict['formats']
757
db95dc13
PH
758 if not formats:
759 raise ExtractorError('No video formats found!')
760
dd82ffea 761 # We check that all the formats have the format and format_id fields
db95dc13 762 for i, format in enumerate(formats):
bec1fad2
PH
763 if 'url' not in format:
764 raise ExtractorError('Missing "url" key in result (index %d)' % i)
765
dd82ffea 766 if format.get('format_id') is None:
8016c922 767 format['format_id'] = compat_str(i)
8c51aa65 768 if format.get('format') is None:
6febd1c1 769 format['format'] = '{id} - {res}{note}'.format(
8c51aa65
JMF
770 id=format['format_id'],
771 res=self.format_resolution(format),
6febd1c1 772 note=' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
8c51aa65 773 )
c1002e96
PH
774 # Automatically determine file extension if missing
775 if 'ext' not in format:
cce929ea 776 format['ext'] = determine_ext(format['url']).lower()
dd82ffea 777
99e206d5
JMF
778 format_limit = self.params.get('format_limit', None)
779 if format_limit:
f4d96df0
PH
780 formats = list(takewhile_inclusive(
781 lambda f: f['format_id'] != format_limit, formats
782 ))
4bcc7bd1
PH
783
784 # TODO Central sorting goes here
99e206d5 785
f89197d7 786 if formats[0] is not info_dict:
b3d9ef88
JMF
787 # only set the 'formats' fields if the original info_dict list them
788 # otherwise we end up with a circular reference, the first (and unique)
f89197d7 789 # element in the 'formats' field in info_dict is info_dict itself,
b3d9ef88
JMF
790 # wich can't be exported to json
791 info_dict['formats'] = formats
bfaae0a7 792 if self.params.get('listformats', None):
793 self.list_formats(info_dict)
794 return
795
de3ef3ed 796 req_format = self.params.get('format')
a9c58ad9
JMF
797 if req_format is None:
798 req_format = 'best'
dd82ffea 799 formats_to_download = []
dd82ffea 800 # The -1 is for supporting YoutubeIE
a9c58ad9 801 if req_format in ('-1', 'all'):
dd82ffea
JMF
802 formats_to_download = formats
803 else:
a9c5e5ca 804 # We can accept formats requested in the format: 34/5/best, we pick
416a5efc 805 # the first that is available, starting from left
dd82ffea
JMF
806 req_formats = req_format.split('/')
807 for rf in req_formats:
6350728b
JMF
808 if re.match(r'.+?\+.+?', rf) is not None:
809 # Two formats have been requested like '137+139'
810 format_1, format_2 = rf.split('+')
811 formats_info = (self.select_format(format_1, formats),
812 self.select_format(format_2, formats))
813 if all(formats_info):
a9c5e5ca
PH
814 selected_format = {
815 'requested_formats': formats_info,
816 'format': rf,
817 'ext': formats_info[0]['ext'],
818 }
6350728b
JMF
819 else:
820 selected_format = None
821 else:
822 selected_format = self.select_format(rf, formats)
a9c58ad9
JMF
823 if selected_format is not None:
824 formats_to_download = [selected_format]
dd82ffea
JMF
825 break
826 if not formats_to_download:
6febd1c1 827 raise ExtractorError('requested format not available',
78a3a9f8 828 expected=True)
dd82ffea
JMF
829
830 if download:
831 if len(formats_to_download) > 1:
6febd1c1 832 self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
dd82ffea
JMF
833 for format in formats_to_download:
834 new_info = dict(info_dict)
835 new_info.update(format)
836 self.process_info(new_info)
837 # We update the info dict with the best quality format (backwards compatibility)
838 info_dict.update(formats_to_download[-1])
839 return info_dict
840
8222d8de
JMF
841 def process_info(self, info_dict):
842 """Process a single resolved IE result."""
843
844 assert info_dict.get('_type', 'video') == 'video'
fd288278
PH
845
846 max_downloads = self.params.get('max_downloads')
847 if max_downloads is not None:
848 if self._num_downloads >= int(max_downloads):
849 raise MaxDownloadsReached()
8222d8de
JMF
850
851 info_dict['fulltitle'] = info_dict['title']
852 if len(info_dict['title']) > 200:
6febd1c1 853 info_dict['title'] = info_dict['title'][:197] + '...'
8222d8de
JMF
854
855 # Keep for backwards compatibility
856 info_dict['stitle'] = info_dict['title']
857
11b85ce6 858 if 'format' not in info_dict:
8222d8de
JMF
859 info_dict['format'] = info_dict['ext']
860
861 reason = self._match_entry(info_dict)
862 if reason is not None:
6febd1c1 863 self.to_screen('[download] ' + reason)
8222d8de
JMF
864 return
865
fd288278 866 self._num_downloads += 1
8222d8de
JMF
867
868 filename = self.prepare_filename(info_dict)
869
870 # Forced printings
871 if self.params.get('forcetitle', False):
0783b09b 872 self.to_stdout(info_dict['fulltitle'])
8222d8de 873 if self.params.get('forceid', False):
0783b09b 874 self.to_stdout(info_dict['id'])
8222d8de 875 if self.params.get('forceurl', False):
edde6c56 876 # For RTMP URLs, also include the playpath
6febd1c1 877 self.to_stdout(info_dict['url'] + info_dict.get('play_path', ''))
216d71d0 878 if self.params.get('forcethumbnail', False) and info_dict.get('thumbnail') is not None:
0783b09b 879 self.to_stdout(info_dict['thumbnail'])
216d71d0 880 if self.params.get('forcedescription', False) and info_dict.get('description') is not None:
0783b09b 881 self.to_stdout(info_dict['description'])
8222d8de 882 if self.params.get('forcefilename', False) and filename is not None:
0783b09b 883 self.to_stdout(filename)
525ef922
PH
884 if self.params.get('forceduration', False) and info_dict.get('duration') is not None:
885 self.to_stdout(formatSeconds(info_dict['duration']))
8222d8de 886 if self.params.get('forceformat', False):
0783b09b 887 self.to_stdout(info_dict['format'])
9d153818 888 if self.params.get('forcejson', False):
a0d96c98 889 info_dict['_filename'] = filename
0783b09b 890 self.to_stdout(json.dumps(info_dict))
8222d8de
JMF
891
892 # Do nothing else if in simulate mode
893 if self.params.get('simulate', False):
894 return
895
896 if filename is None:
897 return
898
899 try:
900 dn = os.path.dirname(encodeFilename(filename))
d26e981d 901 if dn and not os.path.exists(dn):
8222d8de
JMF
902 os.makedirs(dn)
903 except (OSError, IOError) as err:
6febd1c1 904 self.report_error('unable to create directory ' + compat_str(err))
8222d8de
JMF
905 return
906
907 if self.params.get('writedescription', False):
6febd1c1 908 descfn = filename + '.description'
7b6fefc9 909 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
6febd1c1 910 self.to_screen('[info] Video description is already present')
7b6fefc9
PH
911 else:
912 try:
6febd1c1 913 self.to_screen('[info] Writing video description to: ' + descfn)
7b6fefc9
PH
914 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
915 descfile.write(info_dict['description'])
916 except (KeyError, TypeError):
6febd1c1 917 self.report_warning('There\'s no description to write.')
7b6fefc9 918 except (OSError, IOError):
6febd1c1 919 self.report_error('Cannot write description file ' + descfn)
7b6fefc9 920 return
8222d8de 921
1fb07d10 922 if self.params.get('writeannotations', False):
6febd1c1 923 annofn = filename + '.annotations.xml'
7b6fefc9 924 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
6febd1c1 925 self.to_screen('[info] Video annotations are already present')
7b6fefc9
PH
926 else:
927 try:
6febd1c1 928 self.to_screen('[info] Writing video annotations to: ' + annofn)
7b6fefc9
PH
929 with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
930 annofile.write(info_dict['annotations'])
931 except (KeyError, TypeError):
6febd1c1 932 self.report_warning('There are no annotations to write.')
7b6fefc9 933 except (OSError, IOError):
6febd1c1 934 self.report_error('Cannot write annotations file: ' + annofn)
7b6fefc9 935 return
1fb07d10 936
c4a91be7 937 subtitles_are_requested = any([self.params.get('writesubtitles', False),
0b7f3118 938 self.params.get('writeautomaticsub')])
c4a91be7 939
fe7e0c98 940 if subtitles_are_requested and 'subtitles' in info_dict and info_dict['subtitles']:
8222d8de
JMF
941 # subtitles download errors are already managed as troubles in relevant IE
942 # that way it will silently go on when used with unsupporting IE
8222d8de 943 subtitles = info_dict['subtitles']
ca715127 944 sub_format = self.params.get('subtitlesformat', 'srt')
5d51a883
JMF
945 for sub_lang in subtitles.keys():
946 sub = subtitles[sub_lang]
6804038d
JMF
947 if sub is None:
948 continue
8222d8de 949 try:
d4051a8e 950 sub_filename = subtitles_filename(filename, sub_lang, sub_format)
7b6fefc9 951 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(sub_filename)):
6febd1c1 952 self.to_screen('[info] Video subtitle %s.%s is already_present' % (sub_lang, sub_format))
7b6fefc9 953 else:
6febd1c1 954 self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
7b6fefc9
PH
955 with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
956 subfile.write(sub)
8222d8de 957 except (OSError, IOError):
e4db1951 958 self.report_error('Cannot write subtitles file ' + sub_filename)
8222d8de
JMF
959 return
960
8222d8de 961 if self.params.get('writeinfojson', False):
6febd1c1 962 infofn = os.path.splitext(filename)[0] + '.info.json'
7b6fefc9 963 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
6febd1c1 964 self.to_screen('[info] Video description metadata is already present')
7b6fefc9 965 else:
6febd1c1 966 self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
7b6fefc9 967 try:
1538eff6 968 write_json_file(info_dict, encodeFilename(infofn))
7b6fefc9 969 except (OSError, IOError):
6febd1c1 970 self.report_error('Cannot write metadata to JSON file ' + infofn)
7b6fefc9 971 return
8222d8de
JMF
972
973 if self.params.get('writethumbnail', False):
d8269e1d 974 if info_dict.get('thumbnail') is not None:
6febd1c1
PH
975 thumb_format = determine_ext(info_dict['thumbnail'], 'jpg')
976 thumb_filename = os.path.splitext(filename)[0] + '.' + thumb_format
0a9ce268 977 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
6febd1c1 978 self.to_screen('[%s] %s: Thumbnail is already present' %
7b6fefc9
PH
979 (info_dict['extractor'], info_dict['id']))
980 else:
6febd1c1 981 self.to_screen('[%s] %s: Downloading thumbnail ...' %
7b6fefc9
PH
982 (info_dict['extractor'], info_dict['id']))
983 try:
e9c092f1 984 uf = self.urlopen(info_dict['thumbnail'])
7b6fefc9
PH
985 with open(thumb_filename, 'wb') as thumbf:
986 shutil.copyfileobj(uf, thumbf)
6febd1c1 987 self.to_screen('[%s] %s: Writing thumbnail to: %s' %
7b6fefc9
PH
988 (info_dict['extractor'], info_dict['id'], thumb_filename))
989 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
6febd1c1 990 self.report_warning('Unable to download thumbnail "%s": %s' %
7b6fefc9 991 (info_dict['thumbnail'], compat_str(err)))
8222d8de
JMF
992
993 if not self.params.get('skip_download', False):
994 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
995 success = True
996 else:
997 try:
6350728b
JMF
998 def dl(name, info):
999 fd = get_suitable_downloader(info)(self, self.params)
1000 for ph in self._progress_hooks:
1001 fd.add_progress_hook(ph)
8d5797b0
PH
1002 if self.params.get('verbose'):
1003 self.to_stdout('[debug] Invoking downloader on %r' % info.get('url'))
6350728b
JMF
1004 return fd.download(name, info)
1005 if info_dict.get('requested_formats') is not None:
1006 downloaded = []
1007 success = True
b7f81164 1008 merger = FFmpegMergerPP(self, not self.params.get('keepvideo'))
58c3c7ae
JMF
1009 if not merger._get_executable():
1010 postprocessors = []
1011 self.report_warning('You have requested multiple '
1012 'formats but ffmpeg or avconv are not installed.'
1013 ' The formats won\'t be merged')
1014 else:
1015 postprocessors = [merger]
6350728b
JMF
1016 for f in info_dict['requested_formats']:
1017 new_info = dict(info_dict)
1018 new_info.update(f)
1019 fname = self.prepare_filename(new_info)
1020 fname = prepend_extension(fname, 'f%s' % f['format_id'])
1021 downloaded.append(fname)
1022 partial_success = dl(fname, new_info)
1023 success = success and partial_success
58c3c7ae 1024 info_dict['__postprocessors'] = postprocessors
6350728b
JMF
1025 info_dict['__files_to_merge'] = downloaded
1026 else:
1027 # Just a single file
1028 success = dl(filename, info_dict)
8222d8de 1029 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
6febd1c1 1030 self.report_error('unable to download video data: %s' % str(err))
8222d8de 1031 return
c40c6aaa
JMF
1032 except (OSError, IOError) as err:
1033 raise UnavailableVideoError(err)
8222d8de 1034 except (ContentTooShortError, ) as err:
6febd1c1 1035 self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
8222d8de
JMF
1036 return
1037
1038 if success:
1039 try:
1040 self.post_process(filename, info_dict)
1041 except (PostProcessingError) as err:
6febd1c1 1042 self.report_error('postprocessing: %s' % str(err))
8222d8de
JMF
1043 return
1044
c1c9a79c
PH
1045 self.record_download_archive(info_dict)
1046
8222d8de
JMF
1047 def download(self, url_list):
1048 """Download a given list of URLs."""
acd69589 1049 outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
0c75c3fa 1050 if (len(url_list) > 1 and
acd69589 1051 '%' not in outtmpl
0c75c3fa 1052 and self.params.get('max_downloads') != 1):
acd69589 1053 raise SameFileError(outtmpl)
8222d8de
JMF
1054
1055 for url in url_list:
1056 try:
1057 #It also downloads the videos
dca08720 1058 self.extract_info(url)
8222d8de 1059 except UnavailableVideoError:
6febd1c1 1060 self.report_error('unable to download video')
8222d8de 1061 except MaxDownloadsReached:
6febd1c1 1062 self.to_screen('[info] Maximum number of downloaded files reached.')
8222d8de
JMF
1063 raise
1064
1065 return self._download_retcode
1066
1dcc4c0c 1067 def download_with_info_file(self, info_filename):
395293a8 1068 with io.open(info_filename, 'r', encoding='utf-8') as f:
1dcc4c0c 1069 info = json.load(f)
d4943898
JMF
1070 try:
1071 self.process_ie_result(info, download=True)
1072 except DownloadError:
1073 webpage_url = info.get('webpage_url')
1074 if webpage_url is not None:
6febd1c1 1075 self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
d4943898
JMF
1076 return self.download([webpage_url])
1077 else:
1078 raise
1079 return self._download_retcode
1dcc4c0c 1080
8222d8de
JMF
1081 def post_process(self, filename, ie_info):
1082 """Run all the postprocessors on the given file."""
1083 info = dict(ie_info)
1084 info['filepath'] = filename
1085 keep_video = None
6350728b
JMF
1086 pps_chain = []
1087 if ie_info.get('__postprocessors') is not None:
1088 pps_chain.extend(ie_info['__postprocessors'])
1089 pps_chain.extend(self._pps)
1090 for pp in pps_chain:
8222d8de 1091 try:
fe7e0c98 1092 keep_video_wish, new_info = pp.run(info)
8222d8de
JMF
1093 if keep_video_wish is not None:
1094 if keep_video_wish:
1095 keep_video = keep_video_wish
1096 elif keep_video is None:
1097 # No clear decision yet, let IE decide
1098 keep_video = keep_video_wish
1099 except PostProcessingError as e:
bbcbf4d4 1100 self.report_error(e.msg)
8222d8de
JMF
1101 if keep_video is False and not self.params.get('keepvideo', False):
1102 try:
6febd1c1 1103 self.to_screen('Deleting original file %s (pass -k to keep)' % filename)
8222d8de
JMF
1104 os.remove(encodeFilename(filename))
1105 except (IOError, OSError):
6febd1c1 1106 self.report_warning('Unable to remove downloaded video file')
c1c9a79c 1107
5db07df6
PH
1108 def _make_archive_id(self, info_dict):
1109 # Future-proof against any change in case
1110 # and backwards compatibility with prior versions
d31209a1 1111 extractor = info_dict.get('extractor_key')
7012b23c
PH
1112 if extractor is None:
1113 if 'id' in info_dict:
1114 extractor = info_dict.get('ie_key') # key in a playlist
1115 if extractor is None:
5db07df6 1116 return None # Incomplete video information
6febd1c1 1117 return extractor.lower() + ' ' + info_dict['id']
5db07df6
PH
1118
1119 def in_download_archive(self, info_dict):
1120 fn = self.params.get('download_archive')
1121 if fn is None:
1122 return False
1123
1124 vid_id = self._make_archive_id(info_dict)
1125 if vid_id is None:
7012b23c 1126 return False # Incomplete video information
5db07df6 1127
c1c9a79c
PH
1128 try:
1129 with locked_file(fn, 'r', encoding='utf-8') as archive_file:
1130 for line in archive_file:
1131 if line.strip() == vid_id:
1132 return True
1133 except IOError as ioe:
1134 if ioe.errno != errno.ENOENT:
1135 raise
1136 return False
1137
1138 def record_download_archive(self, info_dict):
1139 fn = self.params.get('download_archive')
1140 if fn is None:
1141 return
5db07df6
PH
1142 vid_id = self._make_archive_id(info_dict)
1143 assert vid_id
c1c9a79c 1144 with locked_file(fn, 'a', encoding='utf-8') as archive_file:
6febd1c1 1145 archive_file.write(vid_id + '\n')
dd82ffea 1146
8c51aa65 1147 @staticmethod
8abeeb94 1148 def format_resolution(format, default='unknown'):
fb04e403
PH
1149 if format.get('vcodec') == 'none':
1150 return 'audio only'
f49d89ee
PH
1151 if format.get('resolution') is not None:
1152 return format['resolution']
8c51aa65
JMF
1153 if format.get('height') is not None:
1154 if format.get('width') is not None:
6febd1c1 1155 res = '%sx%s' % (format['width'], format['height'])
8c51aa65 1156 else:
6febd1c1 1157 res = '%sp' % format['height']
f49d89ee 1158 elif format.get('width') is not None:
6febd1c1 1159 res = '?x%d' % format['width']
8c51aa65 1160 else:
8abeeb94 1161 res = default
8c51aa65
JMF
1162 return res
1163
c57f7757
PH
1164 def _format_note(self, fdict):
1165 res = ''
1166 if fdict.get('ext') in ['f4f', 'f4m']:
1167 res += '(unsupported) '
1168 if fdict.get('format_note') is not None:
1169 res += fdict['format_note'] + ' '
1170 if fdict.get('tbr') is not None:
1171 res += '%4dk ' % fdict['tbr']
1172 if fdict.get('container') is not None:
1173 if res:
1174 res += ', '
1175 res += '%s container' % fdict['container']
1176 if (fdict.get('vcodec') is not None and
1177 fdict.get('vcodec') != 'none'):
1178 if res:
1179 res += ', '
1180 res += fdict['vcodec']
91c7271a 1181 if fdict.get('vbr') is not None:
c57f7757
PH
1182 res += '@'
1183 elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
1184 res += 'video@'
1185 if fdict.get('vbr') is not None:
1186 res += '%4dk' % fdict['vbr']
1187 if fdict.get('acodec') is not None:
1188 if res:
1189 res += ', '
1190 if fdict['acodec'] == 'none':
1191 res += 'video only'
1192 else:
1193 res += '%-5s' % fdict['acodec']
1194 elif fdict.get('abr') is not None:
1195 if res:
1196 res += ', '
1197 res += 'audio'
1198 if fdict.get('abr') is not None:
1199 res += '@%3dk' % fdict['abr']
1200 if fdict.get('asr') is not None:
1201 res += ' (%5dHz)' % fdict['asr']
1202 if fdict.get('filesize') is not None:
1203 if res:
1204 res += ', '
1205 res += format_bytes(fdict['filesize'])
9732d77e
PH
1206 elif fdict.get('filesize_approx') is not None:
1207 if res:
1208 res += ', '
1209 res += '~' + format_bytes(fdict['filesize_approx'])
c57f7757 1210 return res
91c7271a 1211
c57f7757 1212 def list_formats(self, info_dict):
02dbf93f 1213 def line(format, idlen=20):
6febd1c1 1214 return (('%-' + compat_str(idlen + 1) + 's%-10s%-12s%s') % (
8c51aa65
JMF
1215 format['format_id'],
1216 format['ext'],
8c51aa65 1217 self.format_resolution(format),
c57f7757 1218 self._format_note(format),
02dbf93f 1219 ))
57dd9a8f 1220
94badb25 1221 formats = info_dict.get('formats', [info_dict])
6febd1c1 1222 idlen = max(len('format code'),
02dbf93f
PH
1223 max(len(f['format_id']) for f in formats))
1224 formats_s = [line(f, idlen) for f in formats]
94badb25 1225 if len(formats) > 1:
c57f7757
PH
1226 formats_s[0] += (' ' if self._format_note(formats[0]) else '') + '(worst)'
1227 formats_s[-1] += (' ' if self._format_note(formats[-1]) else '') + '(best)'
57dd9a8f
PH
1228
1229 header_line = line({
6febd1c1
PH
1230 'format_id': 'format code', 'ext': 'extension',
1231 'resolution': 'resolution', 'format_note': 'note'}, idlen=idlen)
1232 self.to_screen('[info] Available formats for %s:\n%s\n%s' %
1233 (info_dict['id'], header_line, '\n'.join(formats_s)))
dca08720
PH
1234
1235 def urlopen(self, req):
1236 """ Start an HTTP download """
19a41fc6 1237 return self._opener.open(req, timeout=self._socket_timeout)
dca08720
PH
1238
1239 def print_debug_header(self):
1240 if not self.params.get('verbose'):
1241 return
62fec3b2 1242
4192b51c
PH
1243 if type('') is not compat_str:
1244 # Python 2.6 on SLES11 SP1 (https://github.com/rg3/youtube-dl/issues/3326)
1245 self.report_warning(
1246 'Your Python is broken! Update to a newer and supported version')
1247
b0472057 1248 encoding_str = (
734f90bb
PH
1249 '[debug] Encodings: locale %s, fs %s, out %s, pref %s\n' % (
1250 locale.getpreferredencoding(),
1251 sys.getfilesystemencoding(),
1252 sys.stdout.encoding,
b0472057 1253 self.get_encoding()))
4192b51c 1254 write_string(encoding_str, encoding=None)
734f90bb
PH
1255
1256 self._write_string('[debug] youtube-dl version ' + __version__ + '\n')
dca08720
PH
1257 try:
1258 sp = subprocess.Popen(
1259 ['git', 'rev-parse', '--short', 'HEAD'],
1260 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1261 cwd=os.path.dirname(os.path.abspath(__file__)))
1262 out, err = sp.communicate()
1263 out = out.decode().strip()
1264 if re.match('[0-9a-f]+', out):
734f90bb 1265 self._write_string('[debug] Git HEAD: ' + out + '\n')
dca08720
PH
1266 except:
1267 try:
1268 sys.exc_clear()
1269 except:
1270 pass
734f90bb 1271 self._write_string('[debug] Python version %s - %s' %
6febd1c1 1272 (platform.python_version(), platform_name()) + '\n')
dca08720
PH
1273
1274 proxy_map = {}
1275 for handler in self._opener.handlers:
1276 if hasattr(handler, 'proxies'):
1277 proxy_map.update(handler.proxies)
734f90bb 1278 self._write_string('[debug] Proxy map: ' + compat_str(proxy_map) + '\n')
dca08720 1279
e344693b 1280 def _setup_opener(self):
6ad14cab 1281 timeout_val = self.params.get('socket_timeout')
19a41fc6 1282 self._socket_timeout = 600 if timeout_val is None else float(timeout_val)
6ad14cab 1283
dca08720
PH
1284 opts_cookiefile = self.params.get('cookiefile')
1285 opts_proxy = self.params.get('proxy')
1286
1287 if opts_cookiefile is None:
1288 self.cookiejar = compat_cookiejar.CookieJar()
1289 else:
1290 self.cookiejar = compat_cookiejar.MozillaCookieJar(
1291 opts_cookiefile)
1292 if os.access(opts_cookiefile, os.R_OK):
1293 self.cookiejar.load()
1294
1295 cookie_processor = compat_urllib_request.HTTPCookieProcessor(
1296 self.cookiejar)
1297 if opts_proxy is not None:
1298 if opts_proxy == '':
1299 proxies = {}
1300 else:
1301 proxies = {'http': opts_proxy, 'https': opts_proxy}
1302 else:
1303 proxies = compat_urllib_request.getproxies()
1304 # Set HTTPS proxy to HTTP one if given (https://github.com/rg3/youtube-dl/issues/805)
1305 if 'http' in proxies and 'https' not in proxies:
1306 proxies['https'] = proxies['http']
1307 proxy_handler = compat_urllib_request.ProxyHandler(proxies)
a0ddb8a2
PH
1308
1309 debuglevel = 1 if self.params.get('debug_printtraffic') else 0
dca08720 1310 https_handler = make_HTTPS_handler(
a0ddb8a2
PH
1311 self.params.get('nocheckcertificate', False), debuglevel=debuglevel)
1312 ydlh = YoutubeDLHandler(debuglevel=debuglevel)
dca08720 1313 opener = compat_urllib_request.build_opener(
a0ddb8a2 1314 https_handler, proxy_handler, cookie_processor, ydlh)
dca08720
PH
1315 # Delete the default user-agent header, which would otherwise apply in
1316 # cases where our custom HTTP handler doesn't come into play
1317 # (See https://github.com/rg3/youtube-dl/issues/1309 for details)
1318 opener.addheaders = []
1319 self._opener = opener
62fec3b2
PH
1320
1321 def encode(self, s):
1322 if isinstance(s, bytes):
1323 return s # Already encoded
1324
1325 try:
1326 return s.encode(self.get_encoding())
1327 except UnicodeEncodeError as err:
1328 err.reason = err.reason + '. Check your system encoding configuration or use the --encoding option.'
1329 raise
1330
1331 def get_encoding(self):
1332 encoding = self.params.get('encoding')
1333 if encoding is None:
1334 encoding = preferredencoding()
1335 return encoding