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