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