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