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