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