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