]> gitweb @ CieloNegro.org - youtube-dl.git/blobdiff - youtube-dl
Add initial version of postprocessing framework
[youtube-dl.git] / youtube-dl
index 7527a470442db2aca73cc9540a450b680b4983fe..7690898b2e623cb115554524dc52ba1318e4169a 100755 (executable)
@@ -42,6 +42,14 @@ class SameFileError(Exception):
        """
        pass
 
+class PostProcessingError(Exception):
+       """Post Processing exception.
+
+       This exception may be raised by PostProcessor's .run() method to
+       indicate an error in the postprocessing task.
+       """
+       pass
+
 class FileDownloader(object):
        """File Downloader class.
 
@@ -83,10 +91,12 @@ class FileDownloader(object):
 
        _params = None
        _ies = []
+       _pps = []
 
        def __init__(self, params):
                """Create a FileDownloader object with the given options."""
                self._ies = []
+               self._pps = []
                self.set_params(params)
        
        @staticmethod
@@ -176,6 +186,11 @@ class FileDownloader(object):
                self._ies.append(ie)
                ie.set_downloader(self)
        
+       def add_post_processor(self, pp):
+               """Add a PostProcessor object to the end of the chain."""
+               self._pps.append(pp)
+               pp.set_downloader(self)
+       
        def to_stdout(self, message, skip_eol=False):
                """Print message to stdout if not in quiet mode."""
                if not self._params.get('quiet', False):
@@ -267,7 +282,7 @@ class FileDownloader(object):
                                                filename = self._params['outtmpl'] % result
                                                self.report_destination(filename)
                                        except (ValueError, KeyError), err:
-                                               retcode = self.trouble('ERROR: invalid output template: %s' % str(err))
+                                               retcode = self.trouble('ERROR: invalid output template or system charset: %s' % str(err))
                                                continue
                                        try:
                                                self.pmkdir(filename)
@@ -288,11 +303,26 @@ class FileDownloader(object):
                                        except (urllib2.URLError, httplib.HTTPException, socket.error), err:
                                                retcode = self.trouble('ERROR: unable to download video data: %s' % str(err))
                                                continue
+                                       try:
+                                               self.post_process(filename, result)
+                                       except (PostProcessingError), err:
+                                               retcode = self.trouble('ERROR: postprocessing: %s' % str(err))
+                                               continue
+
                                break
                        if not suitable_found:
                                retcode = self.trouble('ERROR: no suitable InfoExtractor: %s' % url)
 
                return retcode
+
+       def post_process(self, filename, ie_info):
+               """Run the postprocessing chain on the given file."""
+               info = dict(ie_info)
+               info['filepath'] = filename
+               for pp in self._pps:
+                       info = pp.run(info)
+                       if info is None:
+                               break
        
        def _do_download(self, stream, url):
                request = urllib2.Request(url, None, std_headers)
@@ -529,7 +559,7 @@ class YoutubeIE(InfoExtractor):
                self.report_video_url(video_id, video_real_url)
 
                # uploader
-               mobj = re.search(r'More From: ([^<]*)<', video_webpage)
+               mobj = re.search(r"var watchUsername = '([^']+)';", video_webpage)
                if mobj is None:
                        self.to_stderr(u'ERROR: unable to extract uploader nickname')
                        return [None]
@@ -682,7 +712,7 @@ class YoutubePlaylistIE(InfoExtractor):
        _VALID_URL = r'(?:http://)?(?:\w+\.)?youtube.com/view_play_list\?p=(.+)'
        _TEMPLATE_URL = 'http://www.youtube.com/view_play_list?p=%s&page=%s'
        _VIDEO_INDICATOR = r'/watch\?v=(.+?)&'
-       _MORE_PAGES_INDICATOR = r'class="pagerNotCurrent">Next</a>'
+       _MORE_PAGES_INDICATOR = r'/view_play_list?p=%s&amp;page=%s'
        _youtube_ie = None
 
        def __init__(self, youtube_ie, downloader=None):
@@ -727,7 +757,7 @@ class YoutubePlaylistIE(InfoExtractor):
                                ids_in_page.add(mobj.group(1))
                        video_ids.extend(list(ids_in_page))
 
-                       if self._MORE_PAGES_INDICATOR not in page:
+                       if (self._MORE_PAGES_INDICATOR % (playlist_id, pagenum + 1)) not in page:
                                break
                        pagenum = pagenum + 1
 
@@ -736,6 +766,62 @@ class YoutubePlaylistIE(InfoExtractor):
                        information.extend(self._youtube_ie.extract('http://www.youtube.com/watch?v=%s' % id))
                return information
 
+class PostProcessor(object):
+       """Post Processor class.
+
+       PostProcessor objects can be added to downloaders with their
+       add_post_processor() method. When the downloader has finished a
+       successful download, it will take its internal chain of PostProcessors
+       and start calling the run() method on each one of them, first with
+       an initial argument and then with the returned value of the previous
+       PostProcessor.
+
+       The chain will be stopped if one of them ever returns None or the end
+       of the chain is reached.
+
+       PostProcessor objects follow a "mutual registration" process similar
+       to InfoExtractor objects.
+       """
+
+       _downloader = None
+
+       def __init__(self, downloader=None):
+               self._downloader = downloader
+
+       def to_stdout(self, message):
+               """Print message to stdout if downloader is not in quiet mode."""
+               if self._downloader is None or not self._downloader.get_params().get('quiet', False):
+                       print message
+       
+       def to_stderr(self, message):
+               """Print message to stderr."""
+               print >>sys.stderr, message
+
+       def set_downloader(self, downloader):
+               """Sets the downloader for this PP."""
+               self._downloader = downloader
+       
+       def run(self, information):
+               """Run the PostProcessor.
+
+               The "information" argument is a dictionary like the ones
+               returned by InfoExtractors. The only difference is that this
+               one has an extra field called "filepath" that points to the
+               downloaded file.
+
+               When this method returns None, the postprocessing chain is
+               stopped. However, this method may return an information
+               dictionary that will be passed to the next postprocessing
+               object in the chain. It can be the one it received after
+               changing some fields.
+
+               In addition, this method may raise a PostProcessingError
+               exception that will be taken into account by the downloader
+               it was called from.
+               """
+               return information # by default, do nothing
+       
+### MAIN PROGRAM ###
 if __name__ == '__main__':
        try:
                # Modules needed only when running the main program
@@ -750,7 +836,7 @@ if __name__ == '__main__':
                # Parse command line
                parser = optparse.OptionParser(
                                usage='Usage: %prog [options] url...',
-                               version='2008.07.22',
+                               version='2008.07.26',
                                conflict_handler='resolve',
                                )
                parser.add_option('-h', '--help',