]> jfr.im git - yt-dlp.git/blame - youtube_dl/YoutubeDL.py
Move console_title to YoutubeDL
[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
c1c9a79c 6import errno
8222d8de
JMF
7import io
8import os
9import re
10import shutil
11import socket
12import sys
13import time
14import traceback
15
1e5b9a95
PH
16if os.name == 'nt':
17 import ctypes
18
8222d8de 19from .utils import *
023fa8c4 20from .extractor import get_info_extractor, gen_extractors
8222d8de
JMF
21from .FileDownloader import FileDownloader
22
23
24class YoutubeDL(object):
25 """YoutubeDL class.
26
27 YoutubeDL objects are the ones responsible of downloading the
28 actual video file and writing it to disk if the user has requested
29 it, among some other tasks. In most cases there should be one per
30 program. As, given a video URL, the downloader doesn't know how to
31 extract all the needed information, task that InfoExtractors do, it
32 has to pass the URL to one of them.
33
34 For this, YoutubeDL objects have a method that allows
35 InfoExtractors to be registered in a given order. When it is passed
36 a URL, the YoutubeDL object handles it to the first InfoExtractor it
37 finds that reports being able to handle it. The InfoExtractor extracts
38 all the information about the video or videos the URL refers to, and
39 YoutubeDL process the extracted information, possibly using a File
40 Downloader to download the video.
41
42 YoutubeDL objects accept a lot of parameters. In order not to saturate
43 the object constructor with arguments, it receives a dictionary of
44 options instead. These options are available through the params
45 attribute for the InfoExtractors to use. The YoutubeDL also
46 registers itself as the downloader in charge for the InfoExtractors
47 that are added to it, so this is a "mutual registration".
48
49 Available options:
50
51 username: Username for authentication purposes.
52 password: Password for authentication purposes.
c6c19746 53 videopassword: Password for acces a video.
8222d8de
JMF
54 usenetrc: Use netrc for authentication instead.
55 verbose: Print additional info to stdout.
56 quiet: Do not print messages to stdout.
57 forceurl: Force printing final URL.
58 forcetitle: Force printing title.
59 forceid: Force printing ID.
60 forcethumbnail: Force printing thumbnail URL.
61 forcedescription: Force printing description.
62 forcefilename: Force printing final filename.
63 simulate: Do not download the video files.
64 format: Video format code.
65 format_limit: Highest quality format to try.
66 outtmpl: Template for output names.
67 restrictfilenames: Do not allow "&" and spaces in file names
68 ignoreerrors: Do not stop on download errors.
69 nooverwrites: Prevent overwriting files.
70 playliststart: Playlist item to start at.
71 playlistend: Playlist item to end at.
72 matchtitle: Download only matching titles.
73 rejecttitle: Reject downloads for matching titles.
74 logtostderr: Log messages to stderr instead of stdout.
75 writedescription: Write the video description to a .description file
76 writeinfojson: Write the video description to a .info.json file
1fb07d10 77 writeannotations: Write the video annotations to a .annotations.xml file
8222d8de
JMF
78 writethumbnail: Write the thumbnail image to a file
79 writesubtitles: Write the video subtitles to a file
b004821f 80 writeautomaticsub: Write the automatic subtitles to a file
8222d8de 81 allsubtitles: Downloads all the subtitles of the video
0b7f3118 82 (requires writesubtitles or writeautomaticsub)
8222d8de 83 listsubtitles: Lists all available subtitles for the video
b98a6b2f 84 subtitlesformat: Subtitle format [srt/sbv/vtt] (default=srt)
aa6a10c4 85 subtitleslangs: List of languages of the subtitles to download
8222d8de
JMF
86 keepvideo: Keep the video file after post-processing
87 daterange: A DateRange object, download only if the upload_date is in the range.
88 skip_download: Skip the actual download of the video file
c35f9e72 89 cachedir: Location of the cache files in the filesystem.
c3c88a26 90 None to disable filesystem cache.
47192f92 91 noplaylist: Download single video instead of a playlist if in doubt.
8dbe9899
PH
92 age_limit: An integer representing the user's age in years.
93 Unsuitable videos for the given age are skipped.
c1c9a79c
PH
94 downloadarchive: File name of a file where all downloads are recorded.
95 Videos already present in the file are not downloaded
96 again.
fe7e0c98 97
8222d8de
JMF
98 The following parameters are not used by YoutubeDL itself, they are used by
99 the FileDownloader:
100 nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
101 noresizebuffer, retries, continuedl, noprogress, consoletitle
102 """
103
104 params = None
105 _ies = []
106 _pps = []
107 _download_retcode = None
108 _num_downloads = None
109 _screen_file = None
110
111 def __init__(self, params):
112 """Create a FileDownloader object with the given options."""
113 self._ies = []
56c73665 114 self._ies_instances = {}
8222d8de
JMF
115 self._pps = []
116 self._progress_hooks = []
117 self._download_retcode = 0
118 self._num_downloads = 0
119 self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
34308b30
PH
120
121 if (sys.version_info >= (3,) and sys.platform != 'win32' and
122 sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
123 and not params['restrictfilenames']):
124 # On Python 3, the Unicode filesystem API will throw errors (#1474)
125 self.report_warning(
1d368c75 126 u'Assuming --restrict-filenames since file system encoding '
34308b30
PH
127 u'cannot encode all charactes. '
128 u'Set the LC_ALL environment variable to fix this.')
129 params['restrictfilenames'] = True
130
8222d8de
JMF
131 self.params = params
132 self.fd = FileDownloader(self, self.params)
133
134 if '%(stitle)s' in self.params['outtmpl']:
135 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.')
136
137 def add_info_extractor(self, ie):
138 """Add an InfoExtractor object to the end of the list."""
139 self._ies.append(ie)
56c73665 140 self._ies_instances[ie.ie_key()] = ie
8222d8de
JMF
141 ie.set_downloader(self)
142
56c73665
JMF
143 def get_info_extractor(self, ie_key):
144 """
145 Get an instance of an IE with name ie_key, it will try to get one from
146 the _ies list, if there's no instance it will create a new one and add
147 it to the extractor list.
148 """
149 ie = self._ies_instances.get(ie_key)
150 if ie is None:
151 ie = get_info_extractor(ie_key)()
152 self.add_info_extractor(ie)
153 return ie
154
023fa8c4
JMF
155 def add_default_info_extractors(self):
156 """
157 Add the InfoExtractors returned by gen_extractors to the end of the list
158 """
159 for ie in gen_extractors():
160 self.add_info_extractor(ie)
161
8222d8de
JMF
162 def add_post_processor(self, pp):
163 """Add a PostProcessor object to the end of the chain."""
164 self._pps.append(pp)
165 pp.set_downloader(self)
166
167 def to_screen(self, message, skip_eol=False):
168 """Print message to stdout if not in quiet mode."""
8222d8de
JMF
169 if not self.params.get('quiet', False):
170 terminator = [u'\n', u''][skip_eol]
171 output = message + terminator
7459e3a2 172 write_string(output, self._screen_file)
8222d8de
JMF
173
174 def to_stderr(self, message):
175 """Print message to stderr."""
176 assert type(message) == type(u'')
177 output = message + u'\n'
178 if 'b' in getattr(self._screen_file, 'mode', '') or sys.version_info[0] < 3: # Python 2 lies about the mode of sys.stdout/sys.stderr
179 output = output.encode(preferredencoding())
180 sys.stderr.write(output)
181
1e5b9a95
PH
182 def to_console_title(self, message):
183 if not self.params.get('consoletitle', False):
184 return
185 if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
186 # c_wchar_p() might not be necessary if `message` is
187 # already of type unicode()
188 ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
189 elif 'TERM' in os.environ:
190 self.to_screen('\033]0;%s\007' % message, skip_eol=True)
191
8222d8de
JMF
192 def fixed_template(self):
193 """Checks if the output template is fixed."""
194 return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None)
195
196 def trouble(self, message=None, tb=None):
197 """Determine action to take when a download problem appears.
198
199 Depending on if the downloader has been configured to ignore
200 download errors or not, this method may throw an exception or
201 not when errors are found, after printing the message.
202
203 tb, if given, is additional traceback information.
204 """
205 if message is not None:
206 self.to_stderr(message)
207 if self.params.get('verbose'):
208 if tb is None:
209 if sys.exc_info()[0]: # if .trouble has been called from an except block
210 tb = u''
211 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
212 tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
213 tb += compat_str(traceback.format_exc())
214 else:
215 tb_data = traceback.format_list(traceback.extract_stack())
216 tb = u''.join(tb_data)
217 self.to_stderr(tb)
218 if not self.params.get('ignoreerrors', False):
219 if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
220 exc_info = sys.exc_info()[1].exc_info
221 else:
222 exc_info = sys.exc_info()
223 raise DownloadError(message, exc_info)
224 self._download_retcode = 1
225
226 def report_warning(self, message):
227 '''
228 Print the message to stderr, it will be prefixed with 'WARNING:'
229 If stderr is a tty file the 'WARNING:' will be colored
230 '''
231 if sys.stderr.isatty() and os.name != 'nt':
fe7e0c98 232 _msg_header = u'\033[0;33mWARNING:\033[0m'
8222d8de 233 else:
fe7e0c98
JMF
234 _msg_header = u'WARNING:'
235 warning_message = u'%s %s' % (_msg_header, message)
8222d8de
JMF
236 self.to_stderr(warning_message)
237
238 def report_error(self, message, tb=None):
239 '''
240 Do the same as trouble, but prefixes the message with 'ERROR:', colored
241 in red if stderr is a tty file.
242 '''
243 if sys.stderr.isatty() and os.name != 'nt':
244 _msg_header = u'\033[0;31mERROR:\033[0m'
245 else:
246 _msg_header = u'ERROR:'
247 error_message = u'%s %s' % (_msg_header, message)
248 self.trouble(error_message, tb)
249
8222d8de
JMF
250 def report_writedescription(self, descfn):
251 """ Report that the description file is being written """
252 self.to_screen(u'[info] Writing video description to: ' + descfn)
253
254 def report_writesubtitles(self, sub_filename):
255 """ Report that the subtitles file is being written """
256 self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
257
258 def report_writeinfojson(self, infofn):
259 """ Report that the metadata file has been written """
260 self.to_screen(u'[info] Video description metadata as JSON to: ' + infofn)
261
1fb07d10
JG
262 def report_writeannotations(self, annofn):
263 """ Report that the annotations file has been written. """
264 self.to_screen(u'[info] Writing video annotations to: ' + annofn)
265
8222d8de
JMF
266 def report_file_already_downloaded(self, file_name):
267 """Report file has already been fully downloaded."""
268 try:
269 self.to_screen(u'[download] %s has already been downloaded' % file_name)
270 except (UnicodeEncodeError) as err:
271 self.to_screen(u'[download] The file has already been downloaded')
272
273 def increment_downloads(self):
274 """Increment the ordinal that assigns a number to each file."""
275 self._num_downloads += 1
276
277 def prepare_filename(self, info_dict):
278 """Generate the output filename."""
279 try:
280 template_dict = dict(info_dict)
281
282 template_dict['epoch'] = int(time.time())
283 autonumber_size = self.params.get('autonumber_size')
284 if autonumber_size is None:
285 autonumber_size = 5
286 autonumber_templ = u'%0' + str(autonumber_size) + u'd'
287 template_dict['autonumber'] = autonumber_templ % self._num_downloads
702665c0 288 if template_dict.get('playlist_index') is not None:
8222d8de
JMF
289 template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
290
586a91b6 291 sanitize = lambda k, v: sanitize_filename(
8222d8de
JMF
292 u'NA' if v is None else compat_str(v),
293 restricted=self.params.get('restrictfilenames'),
586a91b6
PH
294 is_id=(k == u'id'))
295 template_dict = dict((k, sanitize(k, v))
296 for k, v in template_dict.items())
8222d8de 297
586a91b6
PH
298 tmpl = os.path.expanduser(self.params['outtmpl'])
299 filename = tmpl % template_dict
8222d8de
JMF
300 return filename
301 except KeyError as err:
302 self.report_error(u'Erroneous output template')
303 return None
304 except ValueError as err:
4efba05c 305 self.report_error(u'Error in output template: ' + str(err) + u' (encoding: ' + repr(preferredencoding()) + ')')
8222d8de
JMF
306 return None
307
308 def _match_entry(self, info_dict):
309 """ Returns None iff the file should be downloaded """
310
311 title = info_dict['title']
312 matchtitle = self.params.get('matchtitle', False)
313 if matchtitle:
314 if not re.search(matchtitle, title, re.IGNORECASE):
315 return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
316 rejecttitle = self.params.get('rejecttitle', False)
317 if rejecttitle:
318 if re.search(rejecttitle, title, re.IGNORECASE):
319 return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
320 date = info_dict.get('upload_date', None)
321 if date is not None:
322 dateRange = self.params.get('daterange', DateRange())
323 if date not in dateRange:
324 return u'[download] %s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
8dbe9899
PH
325 age_limit = self.params.get('age_limit')
326 if age_limit is not None:
cfadd183 327 if age_limit < info_dict.get('age_limit', 0):
8dbe9899 328 return u'Skipping "' + title + '" because it is age restricted'
c1c9a79c 329 if self.in_download_archive(info_dict):
ee6c9f95 330 return (u'%(title)s has already been recorded in archive'
c1c9a79c 331 % info_dict)
8222d8de 332 return None
fe7e0c98 333
b6c45014
JMF
334 @staticmethod
335 def add_extra_info(info_dict, extra_info):
336 '''Set the keys from extra_info in info dict if they are missing'''
337 for key, value in extra_info.items():
338 info_dict.setdefault(key, value)
339
8222d8de
JMF
340 def extract_info(self, url, download=True, ie_key=None, extra_info={}):
341 '''
342 Returns a list with a dictionary for each video we find.
343 If 'download', also downloads the videos.
344 extra_info is a dict containing the extra values to add to each result
345 '''
fe7e0c98 346
8222d8de 347 if ie_key:
56c73665 348 ies = [self.get_info_extractor(ie_key)]
8222d8de
JMF
349 else:
350 ies = self._ies
351
352 for ie in ies:
353 if not ie.suitable(url):
354 continue
355
356 if not ie.working():
357 self.report_warning(u'The program functionality for this site has been marked as broken, '
358 u'and will probably not work.')
359
360 try:
361 ie_result = ie.extract(url)
362 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
363 break
364 if isinstance(ie_result, list):
365 # Backwards compatibility: old IE result format
8222d8de
JMF
366 ie_result = {
367 '_type': 'compat_list',
368 'entries': ie_result,
369 }
9103bbc5
JMF
370 self.add_extra_info(ie_result,
371 {
372 'extractor': ie.IE_NAME,
be97abc2
JMF
373 'webpage_url': url,
374 'extractor_key': ie.ie_key(),
9103bbc5 375 })
b6c45014 376 return self.process_ie_result(ie_result, download, extra_info)
8222d8de
JMF
377 except ExtractorError as de: # An error we somewhat expected
378 self.report_error(compat_str(de), de.format_traceback())
379 break
380 except Exception as e:
381 if self.params.get('ignoreerrors', False):
382 self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
383 break
384 else:
385 raise
386 else:
387 self.report_error(u'no suitable InfoExtractor: %s' % url)
fe7e0c98 388
8222d8de
JMF
389 def process_ie_result(self, ie_result, download=True, extra_info={}):
390 """
391 Take the result of the ie(may be modified) and resolve all unresolved
392 references (URLs, playlist items).
393
394 It will also download the videos if 'download'.
395 Returns the resolved ie_result.
396 """
397
398 result_type = ie_result.get('_type', 'video') # If not given we suppose it's a video, support the default old system
399 if result_type == 'video':
b6c45014 400 self.add_extra_info(ie_result, extra_info)
feee2ecf 401 return self.process_video_result(ie_result, download=download)
8222d8de
JMF
402 elif result_type == 'url':
403 # We have to add extra_info to the results because it may be
404 # contained in a playlist
405 return self.extract_info(ie_result['url'],
406 download,
407 ie_key=ie_result.get('ie_key'),
408 extra_info=extra_info)
409 elif result_type == 'playlist':
b6c45014 410 self.add_extra_info(ie_result, extra_info)
8222d8de
JMF
411 # We process each entry in the playlist
412 playlist = ie_result.get('title', None) or ie_result.get('id', None)
fe7e0c98 413 self.to_screen(u'[download] Downloading playlist: %s' % playlist)
8222d8de
JMF
414
415 playlist_results = []
416
417 n_all_entries = len(ie_result['entries'])
418 playliststart = self.params.get('playliststart', 1) - 1
419 playlistend = self.params.get('playlistend', -1)
420
421 if playlistend == -1:
422 entries = ie_result['entries'][playliststart:]
423 else:
424 entries = ie_result['entries'][playliststart:playlistend]
425
426 n_entries = len(entries)
427
428 self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
429 (ie_result['extractor'], playlist, n_all_entries, n_entries))
430
fe7e0c98
JMF
431 for i, entry in enumerate(entries, 1):
432 self.to_screen(u'[download] Downloading video #%s of %s' % (i, n_entries))
8222d8de 433 extra = {
fe7e0c98
JMF
434 'playlist': playlist,
435 'playlist_index': i + playliststart,
b6c45014 436 'extractor': ie_result['extractor'],
9103bbc5 437 'webpage_url': ie_result['webpage_url'],
be97abc2 438 'extractor_key': ie_result['extractor_key'],
fe7e0c98 439 }
8222d8de
JMF
440 entry_result = self.process_ie_result(entry,
441 download=download,
442 extra_info=extra)
443 playlist_results.append(entry_result)
444 ie_result['entries'] = playlist_results
445 return ie_result
446 elif result_type == 'compat_list':
447 def _fixup(r):
b6c45014 448 self.add_extra_info(r,
9103bbc5
JMF
449 {
450 'extractor': ie_result['extractor'],
451 'webpage_url': ie_result['webpage_url'],
be97abc2 452 'extractor_key': ie_result['extractor_key'],
9103bbc5 453 })
8222d8de
JMF
454 return r
455 ie_result['entries'] = [
b6c45014 456 self.process_ie_result(_fixup(r), download, extra_info)
8222d8de
JMF
457 for r in ie_result['entries']
458 ]
459 return ie_result
460 else:
461 raise Exception('Invalid result type: %s' % result_type)
462
a9c58ad9
JMF
463 def select_format(self, format_spec, available_formats):
464 if format_spec == 'best' or format_spec is None:
465 return available_formats[-1]
466 elif format_spec == 'worst':
467 return available_formats[0]
468 else:
49e86983
JMF
469 extensions = [u'mp4', u'flv', u'webm', u'3gp']
470 if format_spec in extensions:
471 filter_f = lambda f: f['ext'] == format_spec
472 else:
473 filter_f = lambda f: f['format_id'] == format_spec
fe7e0c98 474 matches = list(filter(filter_f, available_formats))
a9c58ad9
JMF
475 if matches:
476 return matches[-1]
477 return None
478
dd82ffea
JMF
479 def process_video_result(self, info_dict, download=True):
480 assert info_dict.get('_type', 'video') == 'video'
481
482 if 'playlist' not in info_dict:
483 # It isn't part of a playlist
484 info_dict['playlist'] = None
485 info_dict['playlist_index'] = None
486
6ff000b8 487 # This extractors handle format selection themselves
a7685f3b 488 if info_dict['extractor'] in [u'youtube', u'Youku']:
12893efe
JMF
489 if download:
490 self.process_info(info_dict)
6ff000b8
JMF
491 return info_dict
492
dd82ffea
JMF
493 # We now pick which formats have to be downloaded
494 if info_dict.get('formats') is None:
495 # There's only one format available
496 formats = [info_dict]
497 else:
498 formats = info_dict['formats']
499
500 # We check that all the formats have the format and format_id fields
501 for (i, format) in enumerate(formats):
dd82ffea 502 if format.get('format_id') is None:
8016c922 503 format['format_id'] = compat_str(i)
8c51aa65
JMF
504 if format.get('format') is None:
505 format['format'] = u'{id} - {res}{note}'.format(
506 id=format['format_id'],
507 res=self.format_resolution(format),
71934988 508 note=u' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
8c51aa65 509 )
c1002e96
PH
510 # Automatically determine file extension if missing
511 if 'ext' not in format:
512 format['ext'] = determine_ext(format['url'])
dd82ffea
JMF
513
514 if self.params.get('listformats', None):
515 self.list_formats(info_dict)
516 return
517
99e206d5
JMF
518 format_limit = self.params.get('format_limit', None)
519 if format_limit:
f4d96df0
PH
520 formats = list(takewhile_inclusive(
521 lambda f: f['format_id'] != format_limit, formats
522 ))
e028d0d1
JMF
523 if self.params.get('prefer_free_formats'):
524 def _free_formats_key(f):
525 try:
526 ext_ord = [u'flv', u'mp4', u'webm'].index(f['ext'])
527 except ValueError:
528 ext_ord = -1
529 # We only compare the extension if they have the same height and width
530 return (f.get('height'), f.get('width'), ext_ord)
531 formats = sorted(formats, key=_free_formats_key)
99e206d5 532
dd82ffea 533 req_format = self.params.get('format', 'best')
a9c58ad9
JMF
534 if req_format is None:
535 req_format = 'best'
dd82ffea 536 formats_to_download = []
dd82ffea 537 # The -1 is for supporting YoutubeIE
a9c58ad9 538 if req_format in ('-1', 'all'):
dd82ffea
JMF
539 formats_to_download = formats
540 else:
a9c58ad9 541 # We can accept formats requestd in the format: 34/5/best, we pick
416a5efc 542 # the first that is available, starting from left
dd82ffea
JMF
543 req_formats = req_format.split('/')
544 for rf in req_formats:
a9c58ad9
JMF
545 selected_format = self.select_format(rf, formats)
546 if selected_format is not None:
547 formats_to_download = [selected_format]
dd82ffea
JMF
548 break
549 if not formats_to_download:
78a3a9f8
PH
550 raise ExtractorError(u'requested format not available',
551 expected=True)
dd82ffea
JMF
552
553 if download:
554 if len(formats_to_download) > 1:
555 self.to_screen(u'[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
556 for format in formats_to_download:
557 new_info = dict(info_dict)
558 new_info.update(format)
559 self.process_info(new_info)
560 # We update the info dict with the best quality format (backwards compatibility)
561 info_dict.update(formats_to_download[-1])
562 return info_dict
563
8222d8de
JMF
564 def process_info(self, info_dict):
565 """Process a single resolved IE result."""
566
567 assert info_dict.get('_type', 'video') == 'video'
568 #We increment the download the download count here to match the previous behaviour.
569 self.increment_downloads()
570
571 info_dict['fulltitle'] = info_dict['title']
572 if len(info_dict['title']) > 200:
573 info_dict['title'] = info_dict['title'][:197] + u'...'
574
575 # Keep for backwards compatibility
576 info_dict['stitle'] = info_dict['title']
577
578 if not 'format' in info_dict:
579 info_dict['format'] = info_dict['ext']
580
581 reason = self._match_entry(info_dict)
582 if reason is not None:
583 self.to_screen(u'[download] ' + reason)
584 return
585
586 max_downloads = self.params.get('max_downloads')
587 if max_downloads is not None:
588 if self._num_downloads > int(max_downloads):
589 raise MaxDownloadsReached()
590
591 filename = self.prepare_filename(info_dict)
592
593 # Forced printings
594 if self.params.get('forcetitle', False):
595 compat_print(info_dict['title'])
596 if self.params.get('forceid', False):
597 compat_print(info_dict['id'])
598 if self.params.get('forceurl', False):
edde6c56
PH
599 # For RTMP URLs, also include the playpath
600 compat_print(info_dict['url'] + info_dict.get('play_path', u''))
216d71d0 601 if self.params.get('forcethumbnail', False) and info_dict.get('thumbnail') is not None:
8222d8de 602 compat_print(info_dict['thumbnail'])
216d71d0 603 if self.params.get('forcedescription', False) and info_dict.get('description') is not None:
8222d8de
JMF
604 compat_print(info_dict['description'])
605 if self.params.get('forcefilename', False) and filename is not None:
606 compat_print(filename)
607 if self.params.get('forceformat', False):
608 compat_print(info_dict['format'])
609
610 # Do nothing else if in simulate mode
611 if self.params.get('simulate', False):
612 return
613
614 if filename is None:
615 return
616
617 try:
618 dn = os.path.dirname(encodeFilename(filename))
619 if dn != '' and not os.path.exists(dn):
620 os.makedirs(dn)
621 except (OSError, IOError) as err:
622 self.report_error(u'unable to create directory ' + compat_str(err))
623 return
624
625 if self.params.get('writedescription', False):
626 try:
627 descfn = filename + u'.description'
628 self.report_writedescription(descfn)
629 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
630 descfile.write(info_dict['description'])
b3f0e530 631 except (KeyError, TypeError):
535f59bb 632 self.report_warning(u'There\'s no description to write.')
8222d8de
JMF
633 except (OSError, IOError):
634 self.report_error(u'Cannot write description file ' + descfn)
635 return
636
1fb07d10
JG
637 if self.params.get('writeannotations', False):
638 try:
fe7e0c98
JMF
639 annofn = filename + u'.annotations.xml'
640 self.report_writeannotations(annofn)
641 with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
642 annofile.write(info_dict['annotations'])
1fb07d10
JG
643 except (KeyError, TypeError):
644 self.report_warning(u'There are no annotations to write.')
645 except (OSError, IOError):
fe7e0c98
JMF
646 self.report_error(u'Cannot write annotations file: ' + annofn)
647 return
1fb07d10 648
c4a91be7 649 subtitles_are_requested = any([self.params.get('writesubtitles', False),
0b7f3118 650 self.params.get('writeautomaticsub')])
c4a91be7 651
fe7e0c98 652 if subtitles_are_requested and 'subtitles' in info_dict and info_dict['subtitles']:
8222d8de
JMF
653 # subtitles download errors are already managed as troubles in relevant IE
654 # that way it will silently go on when used with unsupporting IE
8222d8de 655 subtitles = info_dict['subtitles']
ca715127 656 sub_format = self.params.get('subtitlesformat', 'srt')
5d51a883
JMF
657 for sub_lang in subtitles.keys():
658 sub = subtitles[sub_lang]
6804038d
JMF
659 if sub is None:
660 continue
8222d8de 661 try:
d4051a8e 662 sub_filename = subtitles_filename(filename, sub_lang, sub_format)
8222d8de
JMF
663 self.report_writesubtitles(sub_filename)
664 with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
5d51a883 665 subfile.write(sub)
8222d8de
JMF
666 except (OSError, IOError):
667 self.report_error(u'Cannot write subtitles file ' + descfn)
668 return
669
8222d8de
JMF
670 if self.params.get('writeinfojson', False):
671 infofn = filename + u'.info.json'
672 self.report_writeinfojson(infofn)
673 try:
fe7e0c98 674 json_info_dict = dict((k, v) for k, v in info_dict.items() if not k in ['urlhandle'])
8222d8de
JMF
675 write_json_file(json_info_dict, encodeFilename(infofn))
676 except (OSError, IOError):
677 self.report_error(u'Cannot write metadata to JSON file ' + infofn)
678 return
679
680 if self.params.get('writethumbnail', False):
d8269e1d 681 if info_dict.get('thumbnail') is not None:
cbdbb766 682 thumb_format = determine_ext(info_dict['thumbnail'], u'jpg')
8222d8de
JMF
683 thumb_filename = filename.rpartition('.')[0] + u'.' + thumb_format
684 self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
685 (info_dict['extractor'], info_dict['id']))
0a60edcf
JMF
686 try:
687 uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
688 with open(thumb_filename, 'wb') as thumbf:
689 shutil.copyfileobj(uf, thumbf)
690 self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
691 (info_dict['extractor'], info_dict['id'], thumb_filename))
692 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
693 self.report_warning(u'Unable to download thumbnail "%s": %s' %
694 (info_dict['thumbnail'], compat_str(err)))
8222d8de
JMF
695
696 if not self.params.get('skip_download', False):
697 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
698 success = True
699 else:
700 try:
701 success = self.fd._do_download(filename, info_dict)
8222d8de
JMF
702 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
703 self.report_error(u'unable to download video data: %s' % str(err))
704 return
c40c6aaa
JMF
705 except (OSError, IOError) as err:
706 raise UnavailableVideoError(err)
8222d8de
JMF
707 except (ContentTooShortError, ) as err:
708 self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
709 return
710
711 if success:
712 try:
713 self.post_process(filename, info_dict)
714 except (PostProcessingError) as err:
715 self.report_error(u'postprocessing: %s' % str(err))
716 return
717
c1c9a79c
PH
718 self.record_download_archive(info_dict)
719
8222d8de
JMF
720 def download(self, url_list):
721 """Download a given list of URLs."""
722 if len(url_list) > 1 and self.fixed_template():
723 raise SameFileError(self.params['outtmpl'])
724
725 for url in url_list:
726 try:
727 #It also downloads the videos
728 videos = self.extract_info(url)
729 except UnavailableVideoError:
730 self.report_error(u'unable to download video')
731 except MaxDownloadsReached:
732 self.to_screen(u'[info] Maximum number of downloaded files reached.')
733 raise
734
735 return self._download_retcode
736
737 def post_process(self, filename, ie_info):
738 """Run all the postprocessors on the given file."""
739 info = dict(ie_info)
740 info['filepath'] = filename
741 keep_video = None
742 for pp in self._pps:
743 try:
fe7e0c98 744 keep_video_wish, new_info = pp.run(info)
8222d8de
JMF
745 if keep_video_wish is not None:
746 if keep_video_wish:
747 keep_video = keep_video_wish
748 elif keep_video is None:
749 # No clear decision yet, let IE decide
750 keep_video = keep_video_wish
751 except PostProcessingError as e:
bbcbf4d4 752 self.report_error(e.msg)
8222d8de
JMF
753 if keep_video is False and not self.params.get('keepvideo', False):
754 try:
755 self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
756 os.remove(encodeFilename(filename))
757 except (IOError, OSError):
758 self.report_warning(u'Unable to remove downloaded video file')
c1c9a79c
PH
759
760 def in_download_archive(self, info_dict):
761 fn = self.params.get('download_archive')
762 if fn is None:
763 return False
764 vid_id = info_dict['extractor'] + u' ' + info_dict['id']
765 try:
766 with locked_file(fn, 'r', encoding='utf-8') as archive_file:
767 for line in archive_file:
768 if line.strip() == vid_id:
769 return True
770 except IOError as ioe:
771 if ioe.errno != errno.ENOENT:
772 raise
773 return False
774
775 def record_download_archive(self, info_dict):
776 fn = self.params.get('download_archive')
777 if fn is None:
778 return
779 vid_id = info_dict['extractor'] + u' ' + info_dict['id']
780 with locked_file(fn, 'a', encoding='utf-8') as archive_file:
781 archive_file.write(vid_id + u'\n')
dd82ffea 782
8c51aa65 783 @staticmethod
8abeeb94 784 def format_resolution(format, default='unknown'):
57dd9a8f
PH
785 if format.get('_resolution') is not None:
786 return format['_resolution']
8c51aa65
JMF
787 if format.get('height') is not None:
788 if format.get('width') is not None:
789 res = u'%sx%s' % (format['width'], format['height'])
790 else:
791 res = u'%sp' % format['height']
792 else:
8abeeb94 793 res = default
8c51aa65
JMF
794 return res
795
dd82ffea 796 def list_formats(self, info_dict):
91c7271a
PH
797 def format_note(fdict):
798 if fdict.get('format_note') is not None:
799 return fdict['format_note']
800 res = u''
801 if fdict.get('vcodec') is not None:
7150858d
PH
802 res += u'%-5s' % fdict['vcodec']
803 elif fdict.get('vbr') is not None:
804 res += u'video'
91c7271a
PH
805 if fdict.get('vbr') is not None:
806 res += u'@%4dk' % fdict['vbr']
807 if fdict.get('acodec') is not None:
808 if res:
809 res += u', '
7150858d
PH
810 res += u'%-5s' % fdict['acodec']
811 elif fdict.get('abr') is not None:
812 if res:
813 res += u', '
814 res += 'audio'
91c7271a
PH
815 if fdict.get('abr') is not None:
816 res += u'@%3dk' % fdict['abr']
817 return res
818
57dd9a8f 819 def line(format):
897d6cc4 820 return (u'%-20s%-10s%-12s%s' % (
8c51aa65
JMF
821 format['format_id'],
822 format['ext'],
8c51aa65 823 self.format_resolution(format),
91c7271a 824 format_note(format),
8c51aa65
JMF
825 )
826 )
57dd9a8f 827
94badb25
PH
828 formats = info_dict.get('formats', [info_dict])
829 formats_s = list(map(line, formats))
830 if len(formats) > 1:
b5349e87
PH
831 formats_s[0] += (' ' if format_note(formats[0]) else '') + '(worst)'
832 formats_s[-1] += (' ' if format_note(formats[-1]) else '') + '(best)'
57dd9a8f
PH
833
834 header_line = line({
835 'format_id': u'format code', 'ext': u'extension',
836 '_resolution': u'resolution', 'format_note': u'note'})
837 self.to_screen(u'[info] Available formats for %s:\n%s\n%s' %
838 (info_dict['id'], header_line, u"\n".join(formats_s)))