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