Merge remote-tracking branch 'origin/master'
[youtube-dl] / youtube_dl / YoutubeDL.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import absolute_import
5
6 import io
7 import os
8 import re
9 import shutil
10 import socket
11 import sys
12 import time
13 import traceback
14
15 from .utils import *
16 from .extractor import get_info_extractor, gen_extractors
17 from .FileDownloader import FileDownloader
18
19
20 class YoutubeDL(object):
21     """YoutubeDL class.
22
23     YoutubeDL objects are the ones responsible of downloading the
24     actual video file and writing it to disk if the user has requested
25     it, among some other tasks. In most cases there should be one per
26     program. As, given a video URL, the downloader doesn't know how to
27     extract all the needed information, task that InfoExtractors do, it
28     has to pass the URL to one of them.
29
30     For this, YoutubeDL objects have a method that allows
31     InfoExtractors to be registered in a given order. When it is passed
32     a URL, the YoutubeDL object handles it to the first InfoExtractor it
33     finds that reports being able to handle it. The InfoExtractor extracts
34     all the information about the video or videos the URL refers to, and
35     YoutubeDL process the extracted information, possibly using a File
36     Downloader to download the video.
37
38     YoutubeDL objects accept a lot of parameters. In order not to saturate
39     the object constructor with arguments, it receives a dictionary of
40     options instead. These options are available through the params
41     attribute for the InfoExtractors to use. The YoutubeDL also
42     registers itself as the downloader in charge for the InfoExtractors
43     that are added to it, so this is a "mutual registration".
44
45     Available options:
46
47     username:          Username for authentication purposes.
48     password:          Password for authentication purposes.
49     videopassword:     Password for acces a video.
50     usenetrc:          Use netrc for authentication instead.
51     verbose:           Print additional info to stdout.
52     quiet:             Do not print messages to stdout.
53     forceurl:          Force printing final URL.
54     forcetitle:        Force printing title.
55     forceid:           Force printing ID.
56     forcethumbnail:    Force printing thumbnail URL.
57     forcedescription:  Force printing description.
58     forcefilename:     Force printing final filename.
59     simulate:          Do not download the video files.
60     format:            Video format code.
61     format_limit:      Highest quality format to try.
62     outtmpl:           Template for output names.
63     restrictfilenames: Do not allow "&" and spaces in file names
64     ignoreerrors:      Do not stop on download errors.
65     nooverwrites:      Prevent overwriting files.
66     playliststart:     Playlist item to start at.
67     playlistend:       Playlist item to end at.
68     matchtitle:        Download only matching titles.
69     rejecttitle:       Reject downloads for matching titles.
70     logtostderr:       Log messages to stderr instead of stdout.
71     writedescription:  Write the video description to a .description file
72     writeinfojson:     Write the video description to a .info.json file
73     writethumbnail:    Write the thumbnail image to a file
74     writesubtitles:    Write the video subtitles to a file
75     writeautomaticsub: Write the automatic subtitles to a file
76     allsubtitles:      Downloads all the subtitles of the video
77                        (requires writesubtitles or writeautomaticsub)
78     listsubtitles:     Lists all available subtitles for the video
79     subtitlesformat:   Subtitle format [srt/sbv/vtt] (default=srt)
80     subtitleslangs:    List of languages of the subtitles to download
81     keepvideo:         Keep the video file after post-processing
82     daterange:         A DateRange object, download only if the upload_date is in the range.
83     skip_download:     Skip the actual download of the video file
84     
85     The following parameters are not used by YoutubeDL itself, they are used by
86     the FileDownloader:
87     nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
88     noresizebuffer, retries, continuedl, noprogress, consoletitle
89     """
90
91     params = None
92     _ies = []
93     _pps = []
94     _download_retcode = None
95     _num_downloads = None
96     _screen_file = None
97
98     def __init__(self, params):
99         """Create a FileDownloader object with the given options."""
100         self._ies = []
101         self._ies_instances = {}
102         self._pps = []
103         self._progress_hooks = []
104         self._download_retcode = 0
105         self._num_downloads = 0
106         self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
107         self.params = params
108         self.fd = FileDownloader(self, self.params)
109
110         if '%(stitle)s' in self.params['outtmpl']:
111             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.')
112
113     def add_info_extractor(self, ie):
114         """Add an InfoExtractor object to the end of the list."""
115         self._ies.append(ie)
116         self._ies_instances[ie.ie_key()] = ie
117         ie.set_downloader(self)
118
119     def get_info_extractor(self, ie_key):
120         """
121         Get an instance of an IE with name ie_key, it will try to get one from
122         the _ies list, if there's no instance it will create a new one and add
123         it to the extractor list.
124         """
125         ie = self._ies_instances.get(ie_key)
126         if ie is None:
127             ie = get_info_extractor(ie_key)()
128             self.add_info_extractor(ie)
129         return ie
130
131     def add_default_info_extractors(self):
132         """
133         Add the InfoExtractors returned by gen_extractors to the end of the list
134         """
135         for ie in gen_extractors():
136             self.add_info_extractor(ie)
137
138     def add_post_processor(self, pp):
139         """Add a PostProcessor object to the end of the chain."""
140         self._pps.append(pp)
141         pp.set_downloader(self)
142
143     def to_screen(self, message, skip_eol=False):
144         """Print message to stdout if not in quiet mode."""
145         assert type(message) == type(u'')
146         if not self.params.get('quiet', False):
147             terminator = [u'\n', u''][skip_eol]
148             output = message + terminator
149             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
150                 output = output.encode(preferredencoding(), 'ignore')
151             self._screen_file.write(output)
152             self._screen_file.flush()
153
154     def to_stderr(self, message):
155         """Print message to stderr."""
156         assert type(message) == type(u'')
157         output = message + u'\n'
158         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
159             output = output.encode(preferredencoding())
160         sys.stderr.write(output)
161
162     def fixed_template(self):
163         """Checks if the output template is fixed."""
164         return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None)
165
166     def trouble(self, message=None, tb=None):
167         """Determine action to take when a download problem appears.
168
169         Depending on if the downloader has been configured to ignore
170         download errors or not, this method may throw an exception or
171         not when errors are found, after printing the message.
172
173         tb, if given, is additional traceback information.
174         """
175         if message is not None:
176             self.to_stderr(message)
177         if self.params.get('verbose'):
178             if tb is None:
179                 if sys.exc_info()[0]:  # if .trouble has been called from an except block
180                     tb = u''
181                     if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
182                         tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
183                     tb += compat_str(traceback.format_exc())
184                 else:
185                     tb_data = traceback.format_list(traceback.extract_stack())
186                     tb = u''.join(tb_data)
187             self.to_stderr(tb)
188         if not self.params.get('ignoreerrors', False):
189             if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
190                 exc_info = sys.exc_info()[1].exc_info
191             else:
192                 exc_info = sys.exc_info()
193             raise DownloadError(message, exc_info)
194         self._download_retcode = 1
195
196     def report_warning(self, message):
197         '''
198         Print the message to stderr, it will be prefixed with 'WARNING:'
199         If stderr is a tty file the 'WARNING:' will be colored
200         '''
201         if sys.stderr.isatty() and os.name != 'nt':
202             _msg_header=u'\033[0;33mWARNING:\033[0m'
203         else:
204             _msg_header=u'WARNING:'
205         warning_message=u'%s %s' % (_msg_header,message)
206         self.to_stderr(warning_message)
207
208     def report_error(self, message, tb=None):
209         '''
210         Do the same as trouble, but prefixes the message with 'ERROR:', colored
211         in red if stderr is a tty file.
212         '''
213         if sys.stderr.isatty() and os.name != 'nt':
214             _msg_header = u'\033[0;31mERROR:\033[0m'
215         else:
216             _msg_header = u'ERROR:'
217         error_message = u'%s %s' % (_msg_header, message)
218         self.trouble(error_message, tb)
219
220     def slow_down(self, start_time, byte_counter):
221         """Sleep if the download speed is over the rate limit."""
222         rate_limit = self.params.get('ratelimit', None)
223         if rate_limit is None or byte_counter == 0:
224             return
225         now = time.time()
226         elapsed = now - start_time
227         if elapsed <= 0.0:
228             return
229         speed = float(byte_counter) / elapsed
230         if speed > rate_limit:
231             time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
232
233     def report_writedescription(self, descfn):
234         """ Report that the description file is being written """
235         self.to_screen(u'[info] Writing video description to: ' + descfn)
236
237     def report_writesubtitles(self, sub_filename):
238         """ Report that the subtitles file is being written """
239         self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
240
241     def report_writeinfojson(self, infofn):
242         """ Report that the metadata file has been written """
243         self.to_screen(u'[info] Video description metadata as JSON to: ' + infofn)
244
245     def report_file_already_downloaded(self, file_name):
246         """Report file has already been fully downloaded."""
247         try:
248             self.to_screen(u'[download] %s has already been downloaded' % file_name)
249         except (UnicodeEncodeError) as err:
250             self.to_screen(u'[download] The file has already been downloaded')
251
252     def increment_downloads(self):
253         """Increment the ordinal that assigns a number to each file."""
254         self._num_downloads += 1
255
256     def prepare_filename(self, info_dict):
257         """Generate the output filename."""
258         try:
259             template_dict = dict(info_dict)
260
261             template_dict['epoch'] = int(time.time())
262             autonumber_size = self.params.get('autonumber_size')
263             if autonumber_size is None:
264                 autonumber_size = 5
265             autonumber_templ = u'%0' + str(autonumber_size) + u'd'
266             template_dict['autonumber'] = autonumber_templ % self._num_downloads
267             if template_dict['playlist_index'] is not None:
268                 template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
269
270             sanitize = lambda k,v: sanitize_filename(
271                 u'NA' if v is None else compat_str(v),
272                 restricted=self.params.get('restrictfilenames'),
273                 is_id=(k==u'id'))
274             template_dict = dict((k, sanitize(k, v)) for k,v in template_dict.items())
275
276             filename = self.params['outtmpl'] % template_dict
277             return filename
278         except KeyError as err:
279             self.report_error(u'Erroneous output template')
280             return None
281         except ValueError as err:
282             self.report_error(u'Error in output template: ' + str(err) + u' (encoding: ' + repr(preferredencoding()) + ')')
283             return None
284
285     def _match_entry(self, info_dict):
286         """ Returns None iff the file should be downloaded """
287
288         title = info_dict['title']
289         matchtitle = self.params.get('matchtitle', False)
290         if matchtitle:
291             if not re.search(matchtitle, title, re.IGNORECASE):
292                 return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
293         rejecttitle = self.params.get('rejecttitle', False)
294         if rejecttitle:
295             if re.search(rejecttitle, title, re.IGNORECASE):
296                 return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
297         date = info_dict.get('upload_date', None)
298         if date is not None:
299             dateRange = self.params.get('daterange', DateRange())
300             if date not in dateRange:
301                 return u'[download] %s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
302         return None
303         
304     def extract_info(self, url, download=True, ie_key=None, extra_info={}):
305         '''
306         Returns a list with a dictionary for each video we find.
307         If 'download', also downloads the videos.
308         extra_info is a dict containing the extra values to add to each result
309          '''
310         
311         if ie_key:
312             ies = [self.get_info_extractor(ie_key)]
313         else:
314             ies = self._ies
315
316         for ie in ies:
317             if not ie.suitable(url):
318                 continue
319
320             if not ie.working():
321                 self.report_warning(u'The program functionality for this site has been marked as broken, '
322                                     u'and will probably not work.')
323
324             try:
325                 ie_result = ie.extract(url)
326                 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
327                     break
328                 if isinstance(ie_result, list):
329                     # Backwards compatibility: old IE result format
330                     for result in ie_result:
331                         result.update(extra_info)
332                     ie_result = {
333                         '_type': 'compat_list',
334                         'entries': ie_result,
335                     }
336                 else:
337                     ie_result.update(extra_info)
338                 if 'extractor' not in ie_result:
339                     ie_result['extractor'] = ie.IE_NAME
340                 return self.process_ie_result(ie_result, download=download)
341             except ExtractorError as de: # An error we somewhat expected
342                 self.report_error(compat_str(de), de.format_traceback())
343                 break
344             except Exception as e:
345                 if self.params.get('ignoreerrors', False):
346                     self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
347                     break
348                 else:
349                     raise
350         else:
351             self.report_error(u'no suitable InfoExtractor: %s' % url)
352         
353     def process_ie_result(self, ie_result, download=True, extra_info={}):
354         """
355         Take the result of the ie(may be modified) and resolve all unresolved
356         references (URLs, playlist items).
357
358         It will also download the videos if 'download'.
359         Returns the resolved ie_result.
360         """
361
362         result_type = ie_result.get('_type', 'video') # If not given we suppose it's a video, support the default old system
363         if result_type == 'video':
364             ie_result.update(extra_info)
365             if 'playlist' not in ie_result:
366                 # It isn't part of a playlist
367                 ie_result['playlist'] = None
368                 ie_result['playlist_index'] = None
369             if download:
370                 self.process_info(ie_result)
371             return ie_result
372         elif result_type == 'url':
373             # We have to add extra_info to the results because it may be
374             # contained in a playlist
375             return self.extract_info(ie_result['url'],
376                                      download,
377                                      ie_key=ie_result.get('ie_key'),
378                                      extra_info=extra_info)
379         elif result_type == 'playlist':
380             # We process each entry in the playlist
381             playlist = ie_result.get('title', None) or ie_result.get('id', None)
382             self.to_screen(u'[download] Downloading playlist: %s'  % playlist)
383
384             playlist_results = []
385
386             n_all_entries = len(ie_result['entries'])
387             playliststart = self.params.get('playliststart', 1) - 1
388             playlistend = self.params.get('playlistend', -1)
389
390             if playlistend == -1:
391                 entries = ie_result['entries'][playliststart:]
392             else:
393                 entries = ie_result['entries'][playliststart:playlistend]
394
395             n_entries = len(entries)
396
397             self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
398                 (ie_result['extractor'], playlist, n_all_entries, n_entries))
399
400             for i,entry in enumerate(entries,1):
401                 self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_entries))
402                 extra = {
403                          'playlist': playlist, 
404                          'playlist_index': i + playliststart,
405                          }
406                 if not 'extractor' in entry:
407                     # We set the extractor, if it's an url it will be set then to
408                     # the new extractor, but if it's already a video we must make
409                     # sure it's present: see issue #877
410                     entry['extractor'] = ie_result['extractor']
411                 entry_result = self.process_ie_result(entry,
412                                                       download=download,
413                                                       extra_info=extra)
414                 playlist_results.append(entry_result)
415             ie_result['entries'] = playlist_results
416             return ie_result
417         elif result_type == 'compat_list':
418             def _fixup(r):
419                 r.setdefault('extractor', ie_result['extractor'])
420                 return r
421             ie_result['entries'] = [
422                 self.process_ie_result(_fixup(r), download=download)
423                 for r in ie_result['entries']
424             ]
425             return ie_result
426         else:
427             raise Exception('Invalid result type: %s' % result_type)
428
429     def process_info(self, info_dict):
430         """Process a single resolved IE result."""
431
432         assert info_dict.get('_type', 'video') == 'video'
433         #We increment the download the download count here to match the previous behaviour.
434         self.increment_downloads()
435
436         info_dict['fulltitle'] = info_dict['title']
437         if len(info_dict['title']) > 200:
438             info_dict['title'] = info_dict['title'][:197] + u'...'
439
440         # Keep for backwards compatibility
441         info_dict['stitle'] = info_dict['title']
442
443         if not 'format' in info_dict:
444             info_dict['format'] = info_dict['ext']
445
446         reason = self._match_entry(info_dict)
447         if reason is not None:
448             self.to_screen(u'[download] ' + reason)
449             return
450
451         max_downloads = self.params.get('max_downloads')
452         if max_downloads is not None:
453             if self._num_downloads > int(max_downloads):
454                 raise MaxDownloadsReached()
455
456         filename = self.prepare_filename(info_dict)
457
458         # Forced printings
459         if self.params.get('forcetitle', False):
460             compat_print(info_dict['title'])
461         if self.params.get('forceid', False):
462             compat_print(info_dict['id'])
463         if self.params.get('forceurl', False):
464             # For RTMP URLs, also include the playpath
465             compat_print(info_dict['url'] + info_dict.get('play_path', u''))
466         if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
467             compat_print(info_dict['thumbnail'])
468         if self.params.get('forcedescription', False) and 'description' in info_dict:
469             compat_print(info_dict['description'])
470         if self.params.get('forcefilename', False) and filename is not None:
471             compat_print(filename)
472         if self.params.get('forceformat', False):
473             compat_print(info_dict['format'])
474
475         # Do nothing else if in simulate mode
476         if self.params.get('simulate', False):
477             return
478
479         if filename is None:
480             return
481
482         try:
483             dn = os.path.dirname(encodeFilename(filename))
484             if dn != '' and not os.path.exists(dn):
485                 os.makedirs(dn)
486         except (OSError, IOError) as err:
487             self.report_error(u'unable to create directory ' + compat_str(err))
488             return
489
490         if self.params.get('writedescription', False):
491             try:
492                 descfn = filename + u'.description'
493                 self.report_writedescription(descfn)
494                 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
495                     descfile.write(info_dict['description'])
496             except (KeyError, TypeError):
497                 self.report_warning(u'There\'s no description to write.')
498             except (OSError, IOError):
499                 self.report_error(u'Cannot write description file ' + descfn)
500                 return
501
502         subtitles_are_requested = any([self.params.get('writesubtitles', False),
503                                        self.params.get('writeautomaticsub')])
504
505         if  subtitles_are_requested and 'subtitles' in info_dict and info_dict['subtitles']:
506             # subtitles download errors are already managed as troubles in relevant IE
507             # that way it will silently go on when used with unsupporting IE
508             subtitles = info_dict['subtitles']
509             sub_format = self.params.get('subtitlesformat')
510             for sub_lang in subtitles.keys():
511                 sub = subtitles[sub_lang]
512                 if sub is None:
513                     continue
514                 try:
515                     sub_filename = subtitles_filename(filename, sub_lang, sub_format)
516                     self.report_writesubtitles(sub_filename)
517                     with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
518                             subfile.write(sub)
519                 except (OSError, IOError):
520                     self.report_error(u'Cannot write subtitles file ' + descfn)
521                     return
522
523         if self.params.get('writeinfojson', False):
524             infofn = filename + u'.info.json'
525             self.report_writeinfojson(infofn)
526             try:
527                 json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle'])
528                 write_json_file(json_info_dict, encodeFilename(infofn))
529             except (OSError, IOError):
530                 self.report_error(u'Cannot write metadata to JSON file ' + infofn)
531                 return
532
533         if self.params.get('writethumbnail', False):
534             if info_dict.get('thumbnail') is not None:
535                 thumb_format = determine_ext(info_dict['thumbnail'], u'jpg')
536                 thumb_filename = filename.rpartition('.')[0] + u'.' + thumb_format
537                 self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
538                                (info_dict['extractor'], info_dict['id']))
539                 uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
540                 with open(thumb_filename, 'wb') as thumbf:
541                     shutil.copyfileobj(uf, thumbf)
542                 self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
543                                (info_dict['extractor'], info_dict['id'], thumb_filename))
544
545         if not self.params.get('skip_download', False):
546             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
547                 success = True
548             else:
549                 try:
550                     success = self.fd._do_download(filename, info_dict)
551                 except (OSError, IOError) as err:
552                     raise UnavailableVideoError(err)
553                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
554                     self.report_error(u'unable to download video data: %s' % str(err))
555                     return
556                 except (ContentTooShortError, ) as err:
557                     self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
558                     return
559
560             if success:
561                 try:
562                     self.post_process(filename, info_dict)
563                 except (PostProcessingError) as err:
564                     self.report_error(u'postprocessing: %s' % str(err))
565                     return
566
567     def download(self, url_list):
568         """Download a given list of URLs."""
569         if len(url_list) > 1 and self.fixed_template():
570             raise SameFileError(self.params['outtmpl'])
571
572         for url in url_list:
573             try:
574                 #It also downloads the videos
575                 videos = self.extract_info(url)
576             except UnavailableVideoError:
577                 self.report_error(u'unable to download video')
578             except MaxDownloadsReached:
579                 self.to_screen(u'[info] Maximum number of downloaded files reached.')
580                 raise
581
582         return self._download_retcode
583
584     def post_process(self, filename, ie_info):
585         """Run all the postprocessors on the given file."""
586         info = dict(ie_info)
587         info['filepath'] = filename
588         keep_video = None
589         for pp in self._pps:
590             try:
591                 keep_video_wish,new_info = pp.run(info)
592                 if keep_video_wish is not None:
593                     if keep_video_wish:
594                         keep_video = keep_video_wish
595                     elif keep_video is None:
596                         # No clear decision yet, let IE decide
597                         keep_video = keep_video_wish
598             except PostProcessingError as e:
599                 self.report_error(e.msg)
600         if keep_video is False and not self.params.get('keepvideo', False):
601             try:
602                 self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
603                 os.remove(encodeFilename(filename))
604             except (IOError, OSError):
605                 self.report_warning(u'Unable to remove downloaded video file')