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