release 2013.09.24
[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
108         if (sys.version_info >= (3,) and sys.platform != 'win32' and
109                 sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
110                 and not params['restrictfilenames']):
111             # On Python 3, the Unicode filesystem API will throw errors (#1474)
112             self.report_warning(
113                 u'Assuming --restrict-filenames isnce file system encoding '
114                 u'cannot encode all charactes. '
115                 u'Set the LC_ALL environment variable to fix this.')
116             params['restrictfilenames'] = True
117
118         self.params = params
119         self.fd = FileDownloader(self, self.params)
120
121         if '%(stitle)s' in self.params['outtmpl']:
122             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.')
123
124     def add_info_extractor(self, ie):
125         """Add an InfoExtractor object to the end of the list."""
126         self._ies.append(ie)
127         self._ies_instances[ie.ie_key()] = ie
128         ie.set_downloader(self)
129
130     def get_info_extractor(self, ie_key):
131         """
132         Get an instance of an IE with name ie_key, it will try to get one from
133         the _ies list, if there's no instance it will create a new one and add
134         it to the extractor list.
135         """
136         ie = self._ies_instances.get(ie_key)
137         if ie is None:
138             ie = get_info_extractor(ie_key)()
139             self.add_info_extractor(ie)
140         return ie
141
142     def add_default_info_extractors(self):
143         """
144         Add the InfoExtractors returned by gen_extractors to the end of the list
145         """
146         for ie in gen_extractors():
147             self.add_info_extractor(ie)
148
149     def add_post_processor(self, pp):
150         """Add a PostProcessor object to the end of the chain."""
151         self._pps.append(pp)
152         pp.set_downloader(self)
153
154     def to_screen(self, message, skip_eol=False):
155         """Print message to stdout if not in quiet mode."""
156         if not self.params.get('quiet', False):
157             terminator = [u'\n', u''][skip_eol]
158             output = message + terminator
159             write_string(output, self._screen_file)
160
161     def to_stderr(self, message):
162         """Print message to stderr."""
163         assert type(message) == type(u'')
164         output = message + u'\n'
165         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
166             output = output.encode(preferredencoding())
167         sys.stderr.write(output)
168
169     def fixed_template(self):
170         """Checks if the output template is fixed."""
171         return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None)
172
173     def trouble(self, message=None, tb=None):
174         """Determine action to take when a download problem appears.
175
176         Depending on if the downloader has been configured to ignore
177         download errors or not, this method may throw an exception or
178         not when errors are found, after printing the message.
179
180         tb, if given, is additional traceback information.
181         """
182         if message is not None:
183             self.to_stderr(message)
184         if self.params.get('verbose'):
185             if tb is None:
186                 if sys.exc_info()[0]:  # if .trouble has been called from an except block
187                     tb = u''
188                     if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
189                         tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
190                     tb += compat_str(traceback.format_exc())
191                 else:
192                     tb_data = traceback.format_list(traceback.extract_stack())
193                     tb = u''.join(tb_data)
194             self.to_stderr(tb)
195         if not self.params.get('ignoreerrors', False):
196             if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
197                 exc_info = sys.exc_info()[1].exc_info
198             else:
199                 exc_info = sys.exc_info()
200             raise DownloadError(message, exc_info)
201         self._download_retcode = 1
202
203     def report_warning(self, message):
204         '''
205         Print the message to stderr, it will be prefixed with 'WARNING:'
206         If stderr is a tty file the 'WARNING:' will be colored
207         '''
208         if sys.stderr.isatty() and os.name != 'nt':
209             _msg_header=u'\033[0;33mWARNING:\033[0m'
210         else:
211             _msg_header=u'WARNING:'
212         warning_message=u'%s %s' % (_msg_header,message)
213         self.to_stderr(warning_message)
214
215     def report_error(self, message, tb=None):
216         '''
217         Do the same as trouble, but prefixes the message with 'ERROR:', colored
218         in red if stderr is a tty file.
219         '''
220         if sys.stderr.isatty() and os.name != 'nt':
221             _msg_header = u'\033[0;31mERROR:\033[0m'
222         else:
223             _msg_header = u'ERROR:'
224         error_message = u'%s %s' % (_msg_header, message)
225         self.trouble(error_message, tb)
226
227     def slow_down(self, start_time, byte_counter):
228         """Sleep if the download speed is over the rate limit."""
229         rate_limit = self.params.get('ratelimit', None)
230         if rate_limit is None or byte_counter == 0:
231             return
232         now = time.time()
233         elapsed = now - start_time
234         if elapsed <= 0.0:
235             return
236         speed = float(byte_counter) / elapsed
237         if speed > rate_limit:
238             time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
239
240     def report_writedescription(self, descfn):
241         """ Report that the description file is being written """
242         self.to_screen(u'[info] Writing video description to: ' + descfn)
243
244     def report_writesubtitles(self, sub_filename):
245         """ Report that the subtitles file is being written """
246         self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
247
248     def report_writeinfojson(self, infofn):
249         """ Report that the metadata file has been written """
250         self.to_screen(u'[info] Video description metadata as JSON to: ' + infofn)
251
252     def report_file_already_downloaded(self, file_name):
253         """Report file has already been fully downloaded."""
254         try:
255             self.to_screen(u'[download] %s has already been downloaded' % file_name)
256         except (UnicodeEncodeError) as err:
257             self.to_screen(u'[download] The file has already been downloaded')
258
259     def increment_downloads(self):
260         """Increment the ordinal that assigns a number to each file."""
261         self._num_downloads += 1
262
263     def prepare_filename(self, info_dict):
264         """Generate the output filename."""
265         try:
266             template_dict = dict(info_dict)
267
268             template_dict['epoch'] = int(time.time())
269             autonumber_size = self.params.get('autonumber_size')
270             if autonumber_size is None:
271                 autonumber_size = 5
272             autonumber_templ = u'%0' + str(autonumber_size) + u'd'
273             template_dict['autonumber'] = autonumber_templ % self._num_downloads
274             if template_dict['playlist_index'] is not None:
275                 template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
276
277             sanitize = lambda k,v: sanitize_filename(
278                 u'NA' if v is None else compat_str(v),
279                 restricted=self.params.get('restrictfilenames'),
280                 is_id=(k==u'id'))
281             template_dict = dict((k, sanitize(k, v)) for k,v in template_dict.items())
282
283             filename = self.params['outtmpl'] % template_dict
284             return filename
285         except KeyError as err:
286             self.report_error(u'Erroneous output template')
287             return None
288         except ValueError as err:
289             self.report_error(u'Error in output template: ' + str(err) + u' (encoding: ' + repr(preferredencoding()) + ')')
290             return None
291
292     def _match_entry(self, info_dict):
293         """ Returns None iff the file should be downloaded """
294
295         title = info_dict['title']
296         matchtitle = self.params.get('matchtitle', False)
297         if matchtitle:
298             if not re.search(matchtitle, title, re.IGNORECASE):
299                 return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
300         rejecttitle = self.params.get('rejecttitle', False)
301         if rejecttitle:
302             if re.search(rejecttitle, title, re.IGNORECASE):
303                 return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
304         date = info_dict.get('upload_date', None)
305         if date is not None:
306             dateRange = self.params.get('daterange', DateRange())
307             if date not in dateRange:
308                 return u'[download] %s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
309         return None
310         
311     def extract_info(self, url, download=True, ie_key=None, extra_info={}):
312         '''
313         Returns a list with a dictionary for each video we find.
314         If 'download', also downloads the videos.
315         extra_info is a dict containing the extra values to add to each result
316          '''
317         
318         if ie_key:
319             ies = [self.get_info_extractor(ie_key)]
320         else:
321             ies = self._ies
322
323         for ie in ies:
324             if not ie.suitable(url):
325                 continue
326
327             if not ie.working():
328                 self.report_warning(u'The program functionality for this site has been marked as broken, '
329                                     u'and will probably not work.')
330
331             try:
332                 ie_result = ie.extract(url)
333                 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
334                     break
335                 if isinstance(ie_result, list):
336                     # Backwards compatibility: old IE result format
337                     for result in ie_result:
338                         result.update(extra_info)
339                     ie_result = {
340                         '_type': 'compat_list',
341                         'entries': ie_result,
342                     }
343                 else:
344                     ie_result.update(extra_info)
345                 if 'extractor' not in ie_result:
346                     ie_result['extractor'] = ie.IE_NAME
347                 return self.process_ie_result(ie_result, download=download)
348             except ExtractorError as de: # An error we somewhat expected
349                 self.report_error(compat_str(de), de.format_traceback())
350                 break
351             except Exception as e:
352                 if self.params.get('ignoreerrors', False):
353                     self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
354                     break
355                 else:
356                     raise
357         else:
358             self.report_error(u'no suitable InfoExtractor: %s' % url)
359         
360     def process_ie_result(self, ie_result, download=True, extra_info={}):
361         """
362         Take the result of the ie(may be modified) and resolve all unresolved
363         references (URLs, playlist items).
364
365         It will also download the videos if 'download'.
366         Returns the resolved ie_result.
367         """
368
369         result_type = ie_result.get('_type', 'video') # If not given we suppose it's a video, support the default old system
370         if result_type == 'video':
371             ie_result.update(extra_info)
372             if 'playlist' not in ie_result:
373                 # It isn't part of a playlist
374                 ie_result['playlist'] = None
375                 ie_result['playlist_index'] = None
376             if download:
377                 self.process_info(ie_result)
378             return ie_result
379         elif result_type == 'url':
380             # We have to add extra_info to the results because it may be
381             # contained in a playlist
382             return self.extract_info(ie_result['url'],
383                                      download,
384                                      ie_key=ie_result.get('ie_key'),
385                                      extra_info=extra_info)
386         elif result_type == 'playlist':
387             # We process each entry in the playlist
388             playlist = ie_result.get('title', None) or ie_result.get('id', None)
389             self.to_screen(u'[download] Downloading playlist: %s'  % playlist)
390
391             playlist_results = []
392
393             n_all_entries = len(ie_result['entries'])
394             playliststart = self.params.get('playliststart', 1) - 1
395             playlistend = self.params.get('playlistend', -1)
396
397             if playlistend == -1:
398                 entries = ie_result['entries'][playliststart:]
399             else:
400                 entries = ie_result['entries'][playliststart:playlistend]
401
402             n_entries = len(entries)
403
404             self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
405                 (ie_result['extractor'], playlist, n_all_entries, n_entries))
406
407             for i,entry in enumerate(entries,1):
408                 self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_entries))
409                 extra = {
410                          'playlist': playlist, 
411                          'playlist_index': i + playliststart,
412                          }
413                 if not 'extractor' in entry:
414                     # We set the extractor, if it's an url it will be set then to
415                     # the new extractor, but if it's already a video we must make
416                     # sure it's present: see issue #877
417                     entry['extractor'] = ie_result['extractor']
418                 entry_result = self.process_ie_result(entry,
419                                                       download=download,
420                                                       extra_info=extra)
421                 playlist_results.append(entry_result)
422             ie_result['entries'] = playlist_results
423             return ie_result
424         elif result_type == 'compat_list':
425             def _fixup(r):
426                 r.setdefault('extractor', ie_result['extractor'])
427                 return r
428             ie_result['entries'] = [
429                 self.process_ie_result(_fixup(r), download=download)
430                 for r in ie_result['entries']
431             ]
432             return ie_result
433         else:
434             raise Exception('Invalid result type: %s' % result_type)
435
436     def process_info(self, info_dict):
437         """Process a single resolved IE result."""
438
439         assert info_dict.get('_type', 'video') == 'video'
440         #We increment the download the download count here to match the previous behaviour.
441         self.increment_downloads()
442
443         info_dict['fulltitle'] = info_dict['title']
444         if len(info_dict['title']) > 200:
445             info_dict['title'] = info_dict['title'][:197] + u'...'
446
447         # Keep for backwards compatibility
448         info_dict['stitle'] = info_dict['title']
449
450         if not 'format' in info_dict:
451             info_dict['format'] = info_dict['ext']
452
453         reason = self._match_entry(info_dict)
454         if reason is not None:
455             self.to_screen(u'[download] ' + reason)
456             return
457
458         max_downloads = self.params.get('max_downloads')
459         if max_downloads is not None:
460             if self._num_downloads > int(max_downloads):
461                 raise MaxDownloadsReached()
462
463         filename = self.prepare_filename(info_dict)
464
465         # Forced printings
466         if self.params.get('forcetitle', False):
467             compat_print(info_dict['title'])
468         if self.params.get('forceid', False):
469             compat_print(info_dict['id'])
470         if self.params.get('forceurl', False):
471             # For RTMP URLs, also include the playpath
472             compat_print(info_dict['url'] + info_dict.get('play_path', u''))
473         if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
474             compat_print(info_dict['thumbnail'])
475         if self.params.get('forcedescription', False) and 'description' in info_dict:
476             compat_print(info_dict['description'])
477         if self.params.get('forcefilename', False) and filename is not None:
478             compat_print(filename)
479         if self.params.get('forceformat', False):
480             compat_print(info_dict['format'])
481
482         # Do nothing else if in simulate mode
483         if self.params.get('simulate', False):
484             return
485
486         if filename is None:
487             return
488
489         try:
490             dn = os.path.dirname(encodeFilename(filename))
491             if dn != '' and not os.path.exists(dn):
492                 os.makedirs(dn)
493         except (OSError, IOError) as err:
494             self.report_error(u'unable to create directory ' + compat_str(err))
495             return
496
497         if self.params.get('writedescription', False):
498             try:
499                 descfn = filename + u'.description'
500                 self.report_writedescription(descfn)
501                 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
502                     descfile.write(info_dict['description'])
503             except (KeyError, TypeError):
504                 self.report_warning(u'There\'s no description to write.')
505             except (OSError, IOError):
506                 self.report_error(u'Cannot write description file ' + descfn)
507                 return
508
509         subtitles_are_requested = any([self.params.get('writesubtitles', False),
510                                        self.params.get('writeautomaticsub')])
511
512         if  subtitles_are_requested and 'subtitles' in info_dict and info_dict['subtitles']:
513             # subtitles download errors are already managed as troubles in relevant IE
514             # that way it will silently go on when used with unsupporting IE
515             subtitles = info_dict['subtitles']
516             sub_format = self.params.get('subtitlesformat')
517             for sub_lang in subtitles.keys():
518                 sub = subtitles[sub_lang]
519                 if sub is None:
520                     continue
521                 try:
522                     sub_filename = subtitles_filename(filename, sub_lang, sub_format)
523                     self.report_writesubtitles(sub_filename)
524                     with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
525                             subfile.write(sub)
526                 except (OSError, IOError):
527                     self.report_error(u'Cannot write subtitles file ' + descfn)
528                     return
529
530         if self.params.get('writeinfojson', False):
531             infofn = filename + u'.info.json'
532             self.report_writeinfojson(infofn)
533             try:
534                 json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle'])
535                 write_json_file(json_info_dict, encodeFilename(infofn))
536             except (OSError, IOError):
537                 self.report_error(u'Cannot write metadata to JSON file ' + infofn)
538                 return
539
540         if self.params.get('writethumbnail', False):
541             if info_dict.get('thumbnail') is not None:
542                 thumb_format = determine_ext(info_dict['thumbnail'], u'jpg')
543                 thumb_filename = filename.rpartition('.')[0] + u'.' + thumb_format
544                 self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
545                                (info_dict['extractor'], info_dict['id']))
546                 uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
547                 with open(thumb_filename, 'wb') as thumbf:
548                     shutil.copyfileobj(uf, thumbf)
549                 self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
550                                (info_dict['extractor'], info_dict['id'], thumb_filename))
551
552         if not self.params.get('skip_download', False):
553             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
554                 success = True
555             else:
556                 try:
557                     success = self.fd._do_download(filename, info_dict)
558                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
559                     self.report_error(u'unable to download video data: %s' % str(err))
560                     return
561                 except (OSError, IOError) as err:
562                     raise UnavailableVideoError(err)
563                 except (ContentTooShortError, ) as err:
564                     self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
565                     return
566
567             if success:
568                 try:
569                     self.post_process(filename, info_dict)
570                 except (PostProcessingError) as err:
571                     self.report_error(u'postprocessing: %s' % str(err))
572                     return
573
574     def download(self, url_list):
575         """Download a given list of URLs."""
576         if len(url_list) > 1 and self.fixed_template():
577             raise SameFileError(self.params['outtmpl'])
578
579         for url in url_list:
580             try:
581                 #It also downloads the videos
582                 videos = self.extract_info(url)
583             except UnavailableVideoError:
584                 self.report_error(u'unable to download video')
585             except MaxDownloadsReached:
586                 self.to_screen(u'[info] Maximum number of downloaded files reached.')
587                 raise
588
589         return self._download_retcode
590
591     def post_process(self, filename, ie_info):
592         """Run all the postprocessors on the given file."""
593         info = dict(ie_info)
594         info['filepath'] = filename
595         keep_video = None
596         for pp in self._pps:
597             try:
598                 keep_video_wish,new_info = pp.run(info)
599                 if keep_video_wish is not None:
600                     if keep_video_wish:
601                         keep_video = keep_video_wish
602                     elif keep_video is None:
603                         # No clear decision yet, let IE decide
604                         keep_video = keep_video_wish
605             except PostProcessingError as e:
606                 self.report_error(e.msg)
607         if keep_video is False and not self.params.get('keepvideo', False):
608             try:
609                 self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
610                 os.remove(encodeFilename(filename))
611             except (IOError, OSError):
612                 self.report_warning(u'Unable to remove downloaded video file')