Switch to "INTERNAL" version again
[youtube-dl] / youtube-dl
index 633c87e5d1a370a813cec1839ccfba8b292f1967..bf6973480940cb5d3aaacbb571b2b8d92106b24f 100755 (executable)
@@ -1,9 +1,11 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 # Author: Ricardo Garcia Gonzalez
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 # Author: Ricardo Garcia Gonzalez
+# Author: Danny Colligan
 # License: Public domain code
 import htmlentitydefs
 import httplib
 # License: Public domain code
 import htmlentitydefs
 import httplib
+import locale
 import math
 import netrc
 import os
 import math
 import netrc
 import os
@@ -17,7 +19,7 @@ import urllib
 import urllib2
 
 std_headers = {        
 import urllib2
 
 std_headers = {        
-       'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1',
+       'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5',
        'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
        'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
        'Accept-Language': 'en-us,en;q=0.5',
        'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
        'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
        'Accept-Language': 'en-us,en;q=0.5',
@@ -25,6 +27,31 @@ std_headers = {
 
 simple_title_chars = string.ascii_letters.decode('ascii') + string.digits.decode('ascii')
 
 
 simple_title_chars = string.ascii_letters.decode('ascii') + string.digits.decode('ascii')
 
+class DownloadError(Exception):
+       """Download Error exception.
+       
+       This exception may be thrown by FileDownloader objects if they are not
+       configured to continue on errors. They will contain the appropriate
+       error message.
+       """
+       pass
+
+class SameFileError(Exception):
+       """Same File exception.
+
+       This exception will be thrown by FileDownloader objects if they detect
+       multiple files would have to be downloaded to the same file on disk.
+       """
+       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.
 
 class FileDownloader(object):
        """File Downloader class.
 
@@ -44,10 +71,10 @@ class FileDownloader(object):
 
        File downloaders accept a lot of parameters. In order not to saturate
        the object constructor with arguments, it receives a dictionary of
 
        File downloaders accept a lot of parameters. In order not to saturate
        the object constructor with arguments, it receives a dictionary of
-       options instead. These options are available through the get_params()
-       method for the InfoExtractors to use. The FileDownloader also registers
-       itself as the downloader in charge for the InfoExtractors that are
-       added to it, so this is a "mutual registration".
+       options instead. These options are available through the params
+       attribute for the InfoExtractors to use. The FileDownloader also
+       registers itself as the downloader in charge for the InfoExtractors
+       that are added to it, so this is a "mutual registration".
 
        Available options:
 
 
        Available options:
 
@@ -61,21 +88,26 @@ class FileDownloader(object):
        format:         Video format code.
        outtmpl:        Template for output names.
        ignoreerrors:   Do not stop on download errors.
        format:         Video format code.
        outtmpl:        Template for output names.
        ignoreerrors:   Do not stop on download errors.
+       ratelimit:      Download speed limit, in bytes/sec.
+       nooverwrites:   Prevent overwriting files.
        """
 
        """
 
-       _params = None
+       params = None
        _ies = []
        _ies = []
+       _pps = []
 
        def __init__(self, params):
                """Create a FileDownloader object with the given options."""
                self._ies = []
 
        def __init__(self, params):
                """Create a FileDownloader object with the given options."""
                self._ies = []
-               self.set_params(params)
+               self._pps = []
+               self.params = params
        
        @staticmethod
        def pmkdir(filename):
                """Create directory components in filename. Similar to Unix "mkdir -p"."""
                components = filename.split(os.sep)
                aggregate = [os.sep.join(components[0:x]) for x in xrange(1, len(components))]
        
        @staticmethod
        def pmkdir(filename):
                """Create directory components in filename. Similar to Unix "mkdir -p"."""
                components = filename.split(os.sep)
                aggregate = [os.sep.join(components[0:x]) for x in xrange(1, len(components))]
+               aggregate = ['%s%s' % (x, os.sep) for x in aggregate] # Finish names with separator
                for dir in aggregate:
                        if not os.path.exists(dir):
                                os.mkdir(dir)
                for dir in aggregate:
                        if not os.path.exists(dir):
                                os.mkdir(dir)
@@ -112,7 +144,7 @@ class FileDownloader(object):
                        return '--:--'
                return '%02d:%02d' % (eta_mins, eta_secs)
 
                        return '--:--'
                return '%02d:%02d' % (eta_mins, eta_secs)
 
-       @staticmethod
+       @staticmethod
        def calc_speed(start, now, bytes):
                dif = now - start
                if bytes == 0 or dif < 0.001: # One millisecond
        def calc_speed(start, now, bytes):
                dif = now - start
                if bytes == 0 or dif < 0.001: # One millisecond
@@ -132,55 +164,86 @@ class FileDownloader(object):
                        return int(new_min)
                return int(rate)
 
                        return int(new_min)
                return int(rate)
 
-       def set_params(self, params):
-               """Sets parameters."""
-               if type(params) != dict:
-                       raise ValueError('params: dictionary expected')
-               self._params = params
-       
-       def get_params(self):
-               """Get parameters."""
-               return self._params
+       @staticmethod
+       def parse_bytes(bytestr):
+               """Parse a string indicating a byte quantity into a long integer."""
+               matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
+               if matchobj is None:
+                       return None
+               number = float(matchobj.group(1))
+               multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
+               return long(round(number * multiplier))
 
        def add_info_extractor(self, ie):
                """Add an InfoExtractor object to the end of the list."""
                self._ies.append(ie)
                ie.set_downloader(self)
        
 
        def add_info_extractor(self, ie):
                """Add an InfoExtractor object to the end of the list."""
                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."""
        def to_stdout(self, message, skip_eol=False):
                """Print message to stdout if not in quiet mode."""
-               if not self._params.get('quiet', False):
-                       sys.stdout.write('%s%s' % (message, ['\n', ''][skip_eol]))
+               if not self.params.get('quiet', False):
+                       print u'%s%s' % (message, [u'\n', u''][skip_eol]),
                        sys.stdout.flush()
        
        def to_stderr(self, message):
                """Print message to stderr."""
                        sys.stdout.flush()
        
        def to_stderr(self, message):
                """Print message to stderr."""
-               sys.stderr.write('%s\n' % message)
+               print >>sys.stderr, message
        
        def fixed_template(self):
                """Checks if the output template is fixed."""
        
        def fixed_template(self):
                """Checks if the output template is fixed."""
-               return (re.search(ur'(?u)%\(.+?\)s', self._params['outtmpl']) is None)
+               return (re.search(ur'(?u)%\(.+?\)s', self.params['outtmpl']) is None)
 
        def trouble(self, message=None):
                """Determine action to take when a download problem appears.
 
                Depending on if the downloader has been configured to ignore
 
        def trouble(self, message=None):
                """Determine action to take when a download problem appears.
 
                Depending on if the downloader has been configured to ignore
-               download errors or not, this method may exit the program or
+               download errors or not, this method may throw an exception or
                not when errors are found, after printing the message. If it
                not when errors are found, after printing the message. If it
-               doesn't exit, it returns an error code suitable to be returned
+               doesn't raise, it returns an error code suitable to be returned
                later as a program exit code to indicate error.
                """
                if message is not None:
                        self.to_stderr(message)
                later as a program exit code to indicate error.
                """
                if message is not None:
                        self.to_stderr(message)
-               if not self._params.get('ignoreerrors', False):
-                       sys.exit(1)
+               if not self.params.get('ignoreerrors', False):
+                       raise DownloadError(message)
                return 1
 
                return 1
 
+       def slow_down(self, start_time, byte_counter):
+               """Sleep if the download speed is over the rate limit."""
+               rate_limit = self.params.get('ratelimit', None)
+               if rate_limit is None or byte_counter == 0:
+                       return
+               now = time.time()
+               elapsed = now - start_time
+               if elapsed <= 0.0:
+                       return
+               speed = float(byte_counter) / elapsed
+               if speed > rate_limit:
+                       time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
+
+       def report_destination(self, filename):
+               """Report destination filename."""
+               self.to_stdout(u'[download] Destination: %s' % filename)
+       
+       def report_progress(self, percent_str, data_len_str, speed_str, eta_str):
+               """Report download progress."""
+               self.to_stdout(u'\r[download] %s of %s at %s ETA %s' %
+                               (percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
+       
+       def report_finish(self):
+               """Report download finished."""
+               self.to_stdout(u'')
+
        def download(self, url_list):
                """Download a given list of URLs."""
                retcode = 0
                if len(url_list) > 1 and self.fixed_template():
        def download(self, url_list):
                """Download a given list of URLs."""
                retcode = 0
                if len(url_list) > 1 and self.fixed_template():
-                       sys.exit('ERROR: fixed output name but more than one file to download')
+                       raise SameFileError(self.params['outtmpl'])
 
                for url in url_list:
                        suitable_found = False
 
                for url in url_list:
                        suitable_found = False
@@ -195,24 +258,27 @@ class FileDownloader(object):
                                        retcode = self.trouble()
 
                                if len(results) > 1 and self.fixed_template():
                                        retcode = self.trouble()
 
                                if len(results) > 1 and self.fixed_template():
-                                       sys.exit('ERROR: fixed output name but more than one file to download')
+                                       raise SameFileError(self.params['outtmpl'])
 
                                for result in results:
 
                                for result in results:
-
                                        # Forced printings
                                        # Forced printings
-                                       if self._params.get('forcetitle', False):
+                                       if self.params.get('forcetitle', False):
                                                print result['title']
                                                print result['title']
-                                       if self._params.get('forceurl', False):
+                                       if self.params.get('forceurl', False):
                                                print result['url']
                                                
                                        # Do nothing else if in simulate mode
                                                print result['url']
                                                
                                        # Do nothing else if in simulate mode
-                                       if self._params.get('simulate', False):
+                                       if self.params.get('simulate', False):
                                                continue
 
                                        try:
                                                continue
 
                                        try:
-                                               filename = self._params['outtmpl'] % result
+                                               filename = self.params['outtmpl'] % result
+                                               self.report_destination(filename)
                                        except (ValueError, KeyError), err:
                                        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
+                                       if self.params['nooverwrites'] and os.path.exists(filename):
+                                               self.to_stderr('WARNING: file exists: %s; skipping' % filename)
                                                continue
                                        try:
                                                self.pmkdir(filename)
                                                continue
                                        try:
                                                self.pmkdir(filename)
@@ -233,11 +299,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
                                        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
                                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)
        
        def _do_download(self, stream, url):
                request = urllib2.Request(url, None, std_headers)
@@ -248,12 +329,13 @@ class FileDownloader(object):
                block_size = 1024
                start = time.time()
                while True:
                block_size = 1024
                start = time.time()
                while True:
+                       # Progress message
                        percent_str = self.calc_percent(byte_counter, data_len)
                        eta_str = self.calc_eta(start, time.time(), data_len, byte_counter)
                        speed_str = self.calc_speed(start, time.time(), byte_counter)
                        percent_str = self.calc_percent(byte_counter, data_len)
                        eta_str = self.calc_eta(start, time.time(), data_len, byte_counter)
                        speed_str = self.calc_speed(start, time.time(), byte_counter)
-                       self.to_stdout('\r[download] %s of %s at %s ETA %s' %
-                                       (percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
+                       self.report_progress(percent_str, data_len_str, speed_str, eta_str)
 
 
+                       # Download and write
                        before = time.time()
                        data_block = data.read(block_size)
                        after = time.time()
                        before = time.time()
                        data_block = data.read(block_size)
                        after = time.time()
@@ -264,7 +346,10 @@ class FileDownloader(object):
                        stream.write(data_block)
                        block_size = self.best_block_size(after - before, data_block_len)
 
                        stream.write(data_block)
                        block_size = self.best_block_size(after - before, data_block_len)
 
-               self.to_stdout('')
+                       # Apply rate limit
+                       self.slow_down(start, byte_counter)
+
+               self.report_finish()
                if data_len is not None and str(byte_counter) != data_len:
                        raise ValueError('Content too short: %s/%s bytes' % (byte_counter, data_len))
 
                if data_len is not None and str(byte_counter) != data_len:
                        raise ValueError('Content too short: %s/%s bytes' % (byte_counter, data_len))
 
@@ -303,7 +388,7 @@ class InfoExtractor(object):
        @staticmethod
        def suitable(url):
                """Receives a URL and returns True if suitable for this IE."""
        @staticmethod
        def suitable(url):
                """Receives a URL and returns True if suitable for this IE."""
-               return True
+               return False
 
        def initialize(self):
                """Initializes an instance (authentication, etc)."""
 
        def initialize(self):
                """Initializes an instance (authentication, etc)."""
@@ -322,12 +407,12 @@ class InfoExtractor(object):
        
        def to_stdout(self, message):
                """Print message to stdout if downloader is not in quiet mode."""
        
        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):
+               if self._downloader is None or not self._downloader.params.get('quiet', False):
                        print message
        
        def to_stderr(self, message):
                """Print message to stderr."""
                        print message
        
        def to_stderr(self, message):
                """Print message to stderr."""
-               sys.stderr.write('%s\n' % message)
+               print >>sys.stderr, message
 
        def _real_initialize(self):
                """Real initialization process. Redefine in subclasses."""
 
        def _real_initialize(self):
                """Real initialization process. Redefine in subclasses."""
@@ -340,17 +425,47 @@ class InfoExtractor(object):
 class YoutubeIE(InfoExtractor):
        """Information extractor for youtube.com."""
 
 class YoutubeIE(InfoExtractor):
        """Information extractor for youtube.com."""
 
-       _LOGIN_URL = 'http://www.youtube.com/login?next=/'
-       _AGE_URL = 'http://www.youtube.com/verify_age?next_url=/'
+       _VALID_URL = r'^((?:http://)?(?:\w+\.)?youtube\.com/(?:(?:v/)|(?:(?:watch(?:\.php)?)?\?(?:.+&)?v=)))?([0-9A-Za-z_-]+)(?(1).+)?$'
+       _LANG_URL = r'http://uk.youtube.com/?hl=en&persist_hl=1&gl=US&persist_gl=1&opt_out_ackd=1'
+       _LOGIN_URL = 'http://www.youtube.com/signup?next=/&gl=US&hl=en'
+       _AGE_URL = 'http://www.youtube.com/verify_age?next_url=/&gl=US&hl=en'
        _NETRC_MACHINE = 'youtube'
 
        _NETRC_MACHINE = 'youtube'
 
+       @staticmethod
+       def suitable(url):
+               return (re.match(YoutubeIE._VALID_URL, url) is not None)
+
+       def report_lang(self):
+               """Report attempt to set language."""
+               self.to_stdout(u'[youtube] Setting language')
+
+       def report_login(self):
+               """Report attempt to log in."""
+               self.to_stdout(u'[youtube] Logging in')
+       
+       def report_age_confirmation(self):
+               """Report attempt to confirm age."""
+               self.to_stdout(u'[youtube] Confirming age')
+       
+       def report_webpage_download(self, video_id):
+               """Report attempt to download webpage."""
+               self.to_stdout(u'[youtube] %s: Downloading video webpage' % video_id)
+       
+       def report_information_extraction(self, video_id):
+               """Report attempt to extract video information."""
+               self.to_stdout(u'[youtube] %s: Extracting video information' % video_id)
+       
+       def report_video_url(self, video_id, video_real_url):
+               """Report extracted video URL."""
+               self.to_stdout(u'[youtube] %s: URL: %s' % (video_id, video_real_url))
+
        def _real_initialize(self):
                if self._downloader is None:
                        return
 
                username = None
                password = None
        def _real_initialize(self):
                if self._downloader is None:
                        return
 
                username = None
                password = None
-               downloader_params = self._downloader.get_params()
+               downloader_params = self._downloader.params
 
                # Attempt to use provided username and password or .netrc data
                if downloader_params.get('username', None) is not None:
 
                # Attempt to use provided username and password or .netrc data
                if downloader_params.get('username', None) is not None:
@@ -365,9 +480,18 @@ class YoutubeIE(InfoExtractor):
                                else:
                                        raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
                        except (IOError, netrc.NetrcParseError), err:
                                else:
                                        raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)
                        except (IOError, netrc.NetrcParseError), err:
-                               self.to_stderr('WARNING: parsing .netrc: %s' % str(err))
+                               self.to_stderr(u'WARNING: parsing .netrc: %s' % str(err))
                                return
 
                                return
 
+               # Set language
+               request = urllib2.Request(self._LANG_URL, None, std_headers)
+               try:
+                       self.report_lang()
+                       urllib2.urlopen(request).read()
+               except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                       self.to_stderr(u'WARNING: unable to set language: %s' % str(err))
+                       return
+
                # No authentication to be performed
                if username is None:
                        return
                # No authentication to be performed
                if username is None:
                        return
@@ -382,13 +506,13 @@ class YoutubeIE(InfoExtractor):
                                }
                request = urllib2.Request(self._LOGIN_URL, urllib.urlencode(login_form), std_headers)
                try:
                                }
                request = urllib2.Request(self._LOGIN_URL, urllib.urlencode(login_form), std_headers)
                try:
-                       self.to_stdout('[youtube] Logging in')
+                       self.report_login()
                        login_results = urllib2.urlopen(request).read()
                        if re.search(r'(?i)<form[^>]* name="loginForm"', login_results) is not None:
                        login_results = urllib2.urlopen(request).read()
                        if re.search(r'(?i)<form[^>]* name="loginForm"', login_results) is not None:
-                               self.to_stderr('WARNING: unable to log in: bad username or password')
+                               self.to_stderr(u'WARNING: unable to log in: bad username or password')
                                return
                except (urllib2.URLError, httplib.HTTPException, socket.error), err:
                                return
                except (urllib2.URLError, httplib.HTTPException, socket.error), err:
-                       self.to_stderr('WARNING: unable to log in: %s' % str(err))
+                       self.to_stderr(u'WARNING: unable to log in: %s' % str(err))
                        return
        
                # Confirm age
                        return
        
                # Confirm age
@@ -398,61 +522,67 @@ class YoutubeIE(InfoExtractor):
                                }
                request = urllib2.Request(self._AGE_URL, urllib.urlencode(age_form), std_headers)
                try:
                                }
                request = urllib2.Request(self._AGE_URL, urllib.urlencode(age_form), std_headers)
                try:
-                       self.to_stdout('[youtube] Confirming age')
+                       self.report_age_confirmation()
                        age_results = urllib2.urlopen(request).read()
                except (urllib2.URLError, httplib.HTTPException, socket.error), err:
                        age_results = urllib2.urlopen(request).read()
                except (urllib2.URLError, httplib.HTTPException, socket.error), err:
-                       sys.exit('ERROR: unable to confirm age: %s' % str(err))
+                       self.to_stderr(u'ERROR: unable to confirm age: %s' % str(err))
+                       return
 
        def _real_extract(self, url):
                # Extract video id from URL
 
        def _real_extract(self, url):
                # Extract video id from URL
-               mobj = re.match(r'^((?:http://)?(?:\w+\.)?youtube\.com/(?:(?:v/)|(?:(?:watch(?:\.php)?)?\?(?:.+&)?v=)))?([0-9A-Za-z_-]+)(?(1).+)?$', url)
+               mobj = re.match(self._VALID_URL, url)
                if mobj is None:
                if mobj is None:
-                       self.to_stderr('ERROR: invalid URL: %s' % url)
+                       self.to_stderr(u'ERROR: invalid URL: %s' % url)
                        return [None]
                video_id = mobj.group(2)
 
                # Downloader parameters
                format_param = None
                if self._downloader is not None:
                        return [None]
                video_id = mobj.group(2)
 
                # Downloader parameters
                format_param = None
                if self._downloader is not None:
-                       params = self._downloader.get_params()
+                       params = self._downloader.params
                        format_param = params.get('format', None)
 
                # Extension
                        format_param = params.get('format', None)
 
                # Extension
-               video_extension = {'18': 'mp4'}.get(format_param, 'flv')
+               video_extension = {
+                       '17': '3gp',
+                       '18': 'mp4',
+                       '22': 'mp4',
+               }.get(format_param, 'flv')
 
                # Normalize URL, including format
 
                # Normalize URL, including format
-               normalized_url = 'http://www.youtube.com/watch?v=%s' % video_id
+               normalized_url = 'http://www.youtube.com/watch?v=%s&gl=US&hl=en' % video_id
                if format_param is not None:
                        normalized_url = '%s&fmt=%s' % (normalized_url, format_param)
                request = urllib2.Request(normalized_url, None, std_headers)
                try:
                if format_param is not None:
                        normalized_url = '%s&fmt=%s' % (normalized_url, format_param)
                request = urllib2.Request(normalized_url, None, std_headers)
                try:
-                       self.to_stdout('[youtube] %s: Downloading video webpage' % video_id)
+                       self.report_webpage_download(video_id)
                        video_webpage = urllib2.urlopen(request).read()
                except (urllib2.URLError, httplib.HTTPException, socket.error), err:
                        video_webpage = urllib2.urlopen(request).read()
                except (urllib2.URLError, httplib.HTTPException, socket.error), err:
-                       sys.exit('ERROR: unable to download video: %s' % str(err))
-               self.to_stdout('[youtube] %s: Extracting video information' % video_id)
+                       self.to_stderr(u'ERROR: unable to download video webpage: %s' % str(err))
+                       return [None]
+               self.report_information_extraction(video_id)
                
                # "t" param
                mobj = re.search(r', "t": "([^"]+)"', video_webpage)
                if mobj is None:
                
                # "t" param
                mobj = re.search(r', "t": "([^"]+)"', video_webpage)
                if mobj is None:
-                       self.to_stderr('ERROR: unable to extract "t" parameter')
+                       self.to_stderr(u'ERROR: unable to extract "t" parameter')
                        return [None]
                video_real_url = 'http://www.youtube.com/get_video?video_id=%s&t=%s' % (video_id, mobj.group(1))
                if format_param is not None:
                        video_real_url = '%s&fmt=%s' % (video_real_url, format_param)
                        return [None]
                video_real_url = 'http://www.youtube.com/get_video?video_id=%s&t=%s' % (video_id, mobj.group(1))
                if format_param is not None:
                        video_real_url = '%s&fmt=%s' % (video_real_url, format_param)
-               self.to_stdout('[youtube] %s: URL: %s' % (video_id, video_real_url))
+               self.report_video_url(video_id, video_real_url)
 
                # uploader
 
                # uploader
-               mobj = re.search(r'More From: ([^<]*)<', video_webpage)
+               mobj = re.search(r"var watchUsername = '([^']+)';", video_webpage)
                if mobj is None:
                if mobj is None:
-                       self.to_stderr('ERROR: unable to extract uploader nickname')
+                       self.to_stderr(u'ERROR: unable to extract uploader nickname')
                        return [None]
                video_uploader = mobj.group(1)
 
                # title
                mobj = re.search(r'(?im)<title>YouTube - ([^<]*)</title>', video_webpage)
                if mobj is None:
                        return [None]
                video_uploader = mobj.group(1)
 
                # title
                mobj = re.search(r'(?im)<title>YouTube - ([^<]*)</title>', video_webpage)
                if mobj is None:
-                       self.to_stderr('ERROR: unable to extract video title')
+                       self.to_stderr(u'ERROR: unable to extract video title')
                        return [None]
                video_title = mobj.group(1).decode('utf-8')
                video_title = re.sub(ur'(?u)&(.+?);', lambda x: unichr(htmlentitydefs.name2codepoint[x.group(1)]), video_title)
                        return [None]
                video_title = mobj.group(1).decode('utf-8')
                video_title = re.sub(ur'(?u)&(.+?);', lambda x: unichr(htmlentitydefs.name2codepoint[x.group(1)]), video_title)
@@ -464,14 +594,333 @@ class YoutubeIE(InfoExtractor):
 
                # Return information
                return [{
 
                # Return information
                return [{
-                       'id':           video_id,
-                       'url':          video_real_url,
-                       'uploader':     video_uploader,
+                       'id':           video_id.decode('utf-8'),
+                       'url':          video_real_url.decode('utf-8'),
+                       'uploader':     video_uploader.decode('utf-8'),
                        'title':        video_title,
                        'stitle':       simple_title,
                        'title':        video_title,
                        'stitle':       simple_title,
-                       'ext':          video_extension,
+                       'ext':          video_extension.decode('utf-8'),
                        }]
 
                        }]
 
+class MetacafeIE(InfoExtractor):
+       """Information Extractor for metacafe.com."""
+
+       _VALID_URL = r'(?:http://)?(?:www\.)?metacafe\.com/watch/([^/]+)/([^/]+)/.*'
+       _DISCLAIMER = 'http://www.metacafe.com/family_filter/'
+       _youtube_ie = None
+
+       def __init__(self, youtube_ie, downloader=None):
+               InfoExtractor.__init__(self, downloader)
+               self._youtube_ie = youtube_ie
+
+       @staticmethod
+       def suitable(url):
+               return (re.match(MetacafeIE._VALID_URL, url) is not None)
+
+       def report_disclaimer(self):
+               """Report disclaimer retrieval."""
+               self.to_stdout(u'[metacafe] Retrieving disclaimer')
+
+       def report_age_confirmation(self):
+               """Report attempt to confirm age."""
+               self.to_stdout(u'[metacafe] Confirming age')
+       
+       def report_download_webpage(self, video_id):
+               """Report webpage download."""
+               self.to_stdout(u'[metacafe] %s: Downloading webpage' % video_id)
+       
+       def report_extraction(self, video_id):
+               """Report information extraction."""
+               self.to_stdout(u'[metacafe] %s: Extracting information' % video_id)
+
+       def _real_initialize(self):
+               # Retrieve disclaimer
+               request = urllib2.Request(self._DISCLAIMER, None, std_headers)
+               try:
+                       self.report_disclaimer()
+                       disclaimer = urllib2.urlopen(request).read()
+               except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                       self.to_stderr(u'ERROR: unable to retrieve disclaimer: %s' % str(err))
+                       return
+
+               # Confirm age
+               disclaimer_form = {
+                       'filters': '0',
+                       'submit': "Continue - I'm over 18",
+                       }
+               request = urllib2.Request('http://www.metacafe.com/', urllib.urlencode(disclaimer_form), std_headers)
+               try:
+                       self.report_age_confirmation()
+                       disclaimer = urllib2.urlopen(request).read()
+               except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                       self.to_stderr(u'ERROR: unable to confirm age: %s' % str(err))
+                       return
+       
+       def _real_extract(self, url):
+               # Extract id and simplified title from URL
+               mobj = re.match(self._VALID_URL, url)
+               if mobj is None:
+                       self.to_stderr(u'ERROR: invalid URL: %s' % url)
+                       return [None]
+
+               video_id = mobj.group(1)
+
+               # Check if video comes from YouTube
+               mobj2 = re.match(r'^yt-(.*)$', video_id)
+               if mobj2 is not None:
+                       return self._youtube_ie.extract('http://www.youtube.com/watch?v=%s' % mobj2.group(1))
+
+               simple_title = mobj.group(2).decode('utf-8')
+               video_extension = 'flv'
+
+               # Retrieve video webpage to extract further information
+               request = urllib2.Request('http://www.metacafe.com/watch/%s/' % video_id)
+               try:
+                       self.report_download_webpage(video_id)
+                       webpage = urllib2.urlopen(request).read()
+               except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                       self.to_stderr(u'ERROR: unable retrieve video webpage: %s' % str(err))
+                       return [None]
+
+               # Extract URL, uploader and title from webpage
+               self.report_extraction(video_id)
+               mobj = re.search(r'(?m)"mediaURL":"(http.*?\.flv)"', webpage)
+               if mobj is None:
+                       self.to_stderr(u'ERROR: unable to extract media URL')
+                       return [None]
+               mediaURL = mobj.group(1).replace('\\', '')
+
+               mobj = re.search(r'(?m)"gdaKey":"(.*?)"', webpage)
+               if mobj is None:
+                       self.to_stderr(u'ERROR: unable to extract gdaKey')
+                       return [None]
+               gdaKey = mobj.group(1)
+
+               video_url = '%s?__gda__=%s' % (mediaURL, gdaKey)
+
+               mobj = re.search(r'(?im)<title>(.*) - Video</title>', webpage)
+               if mobj is None:
+                       self.to_stderr(u'ERROR: unable to extract title')
+                       return [None]
+               video_title = mobj.group(1).decode('utf-8')
+
+               mobj = re.search(r'(?m)<li id="ChnlUsr">.*?Submitter:<br />(.*?)</li>', webpage)
+               if mobj is None:
+                       self.to_stderr(u'ERROR: unable to extract uploader nickname')
+                       return [None]
+               video_uploader = re.sub(r'<.*?>', '', mobj.group(1))
+
+               # Return information
+               return [{
+                       'id':           video_id.decode('utf-8'),
+                       'url':          video_url.decode('utf-8'),
+                       'uploader':     video_uploader.decode('utf-8'),
+                       'title':        video_title,
+                       'stitle':       simple_title,
+                       'ext':          video_extension.decode('utf-8'),
+                       }]
+
+
+class YoutubeSearchIE(InfoExtractor):
+       """Information Extractor for YouTube search queries."""
+       _VALID_QUERY = r'ytsearch(\d+|all)?:[\s\S]+'
+       _TEMPLATE_URL = 'http://www.youtube.com/results?search_query=%s&page=%s&gl=US&hl=en'
+       _VIDEO_INDICATOR = r'href="/watch\?v=.+?"'
+       _MORE_PAGES_INDICATOR = r'>Next</a>'
+       _youtube_ie = None
+
+       def __init__(self, youtube_ie, downloader=None): 
+               InfoExtractor.__init__(self, downloader)
+               self._youtube_ie = youtube_ie
+       
+       @staticmethod
+       def suitable(url):
+               return (re.match(YoutubeSearchIE._VALID_QUERY, url) is not None)
+
+       def report_download_page(self, query, pagenum):
+               """Report attempt to download playlist page with given number."""
+               self.to_stdout(u'[youtube] query "%s": Downloading page %s' % (query, pagenum))
+
+       def _real_initialize(self):
+               self._youtube_ie.initialize()
+       
+       def _real_extract(self, query):
+               mobj = re.match(self._VALID_QUERY, query)
+               if mobj is None:
+                       self.to_stderr(u'ERROR: invalid search query "%s"' % query)
+                       return [None]
+
+               prefix, query = query.split(':')
+               prefix = prefix[8:]
+               if prefix == '': 
+                       return self._download_n_results(query, 1)
+               elif prefix == 'all': 
+                       return self._download_n_results(query, -1)
+               else: 
+                       try:
+                               n = int(prefix)
+                               if n <= 0:
+                                       self.to_stderr(u'ERROR: invalid download number %s for query "%s"' % (n, query))
+                                       return [None]
+                               return self._download_n_results(query, n)
+                       except ValueError: # parsing prefix as int fails
+                               return self._download_n_results(query, 1)
+
+       def _download_n_results(self, query, n):
+               """Downloads a specified number of results for a query"""
+
+               video_ids = []
+               already_seen = set()
+               pagenum = 1
+
+               while True:
+                       self.report_download_page(query, pagenum)
+                       result_url = self._TEMPLATE_URL % (urllib.quote_plus(query), pagenum)
+                       request = urllib2.Request(result_url, None, std_headers)
+                       try:
+                               page = urllib2.urlopen(request).read()
+                       except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                               self.to_stderr(u'ERROR: unable to download webpage: %s' % str(err))
+                               return [None]
+
+                       # Extract video identifiers
+                       for mobj in re.finditer(self._VIDEO_INDICATOR, page):
+                               video_id = page[mobj.span()[0]:mobj.span()[1]].split('=')[2][:-1]
+                               if video_id not in already_seen:
+                                       video_ids.append(video_id)
+                                       already_seen.add(video_id)
+                                       if len(video_ids) == n:
+                                               # Specified n videos reached
+                                               information = []
+                                               for id in video_ids:
+                                                       information.extend(self._youtube_ie.extract('http://www.youtube.com/watch?v=%s' % id))
+                                               return information
+
+                       if self._MORE_PAGES_INDICATOR not in page:
+                               information = []
+                               for id in video_ids:
+                                       information.extend(self._youtube_ie.extract('http://www.youtube.com/watch?v=%s' % id))
+                               return information
+
+                       pagenum = pagenum + 1
+
+class YoutubePlaylistIE(InfoExtractor):
+       """Information Extractor for YouTube playlists."""
+
+       _VALID_URL = r'(?:http://)?(?:\w+\.)?youtube.com/view_play_list\?p=(.+)'
+       _TEMPLATE_URL = 'http://www.youtube.com/view_play_list?p=%s&page=%s&gl=US&hl=en'
+       _VIDEO_INDICATOR = r'/watch\?v=(.+?)&'
+       _MORE_PAGES_INDICATOR = r'/view_play_list?p=%s&amp;page=%s'
+       _youtube_ie = None
+
+       def __init__(self, youtube_ie, downloader=None):
+               InfoExtractor.__init__(self, downloader)
+               self._youtube_ie = youtube_ie
+       
+       @staticmethod
+       def suitable(url):
+               return (re.match(YoutubePlaylistIE._VALID_URL, url) is not None)
+
+       def report_download_page(self, playlist_id, pagenum):
+               """Report attempt to download playlist page with given number."""
+               self.to_stdout(u'[youtube] PL %s: Downloading page #%s' % (playlist_id, pagenum))
+
+       def _real_initialize(self):
+               self._youtube_ie.initialize()
+       
+       def _real_extract(self, url):
+               # Extract playlist id
+               mobj = re.match(self._VALID_URL, url)
+               if mobj is None:
+                       self.to_stderr(u'ERROR: invalid url: %s' % url)
+                       return [None]
+
+               # Download playlist pages
+               playlist_id = mobj.group(1)
+               video_ids = []
+               pagenum = 1
+
+               while True:
+                       self.report_download_page(playlist_id, pagenum)
+                       request = urllib2.Request(self._TEMPLATE_URL % (playlist_id, pagenum), None, std_headers)
+                       try:
+                               page = urllib2.urlopen(request).read()
+                       except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                               self.to_stderr(u'ERROR: unable to download webpage: %s' % str(err))
+                               return [None]
+
+                       # Extract video identifiers
+                       ids_in_page = []
+                       for mobj in re.finditer(self._VIDEO_INDICATOR, page):
+                               if mobj.group(1) not in ids_in_page:
+                                       ids_in_page.append(mobj.group(1))
+                       video_ids.extend(ids_in_page)
+
+                       if (self._MORE_PAGES_INDICATOR % (playlist_id, pagenum + 1)) not in page:
+                               break
+                       pagenum = pagenum + 1
+
+               information = []
+               for id in video_ids:
+                       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.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
 if __name__ == '__main__':
        try:
                # Modules needed only when running the main program
@@ -515,30 +964,58 @@ if __name__ == '__main__':
                                action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
                parser.add_option('-f', '--format',
                                dest='format', metavar='FMT', help='video format code')
                                action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
                parser.add_option('-f', '--format',
                                dest='format', metavar='FMT', help='video format code')
-               parser.add_option('-b', '--best-quality',
-                               action='store_const', dest='video_format', help='alias for -f 18', const='18')
+               parser.add_option('-m', '--mobile-version',
+                               action='store_const', dest='format', help='alias for -f 17', const='17')
+               parser.add_option('-d', '--high-def',
+                               action='store_const', dest='format', help='alias for -f 22', const='22')
                parser.add_option('-i', '--ignore-errors',
                                action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
                parser.add_option('-i', '--ignore-errors',
                                action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)
+               parser.add_option('-r', '--rate-limit',
+                               dest='ratelimit', metavar='L', help='download rate limit (e.g. 50k or 44.6m)')
+               parser.add_option('-a', '--batch-file',
+                               dest='batchfile', metavar='F', help='file containing URLs to download')
+               parser.add_option('-w', '--no-overwrites',
+                               action='store_true', dest='nooverwrites', help='do not overwrite files', default=False)
                (opts, args) = parser.parse_args()
 
                (opts, args) = parser.parse_args()
 
+               # Batch file verification
+               batchurls = []
+               if opts.batchfile is not None:
+                       try:
+                               batchurls = [line.strip() for line in open(opts.batchfile, 'r')]
+                       except IOError:
+                               sys.exit(u'ERROR: batch file could not be read')
+               all_urls = batchurls + args
+
                # Conflicting, missing and erroneous options
                # Conflicting, missing and erroneous options
-               if len(args) < 1:
-                       sys.exit('ERROR: you must provide at least one URL')
+               if len(all_urls) < 1:
+                       sys.exit(u'ERROR: you must provide at least one URL')
                if opts.usenetrc and (opts.username is not None or opts.password is not None):
                if opts.usenetrc and (opts.username is not None or opts.password is not None):
-                       sys.exit('ERROR: using .netrc conflicts with giving username/password')
+                       sys.exit(u'ERROR: using .netrc conflicts with giving username/password')
                if opts.password is not None and opts.username is None:
                if opts.password is not None and opts.username is None:
-                       sys.exit('ERROR: account username missing')
+                       sys.exit(u'ERROR: account username missing')
                if opts.outtmpl is not None and (opts.useliteral or opts.usetitle):
                if opts.outtmpl is not None and (opts.useliteral or opts.usetitle):
-                       sys.exit('ERROR: using output template conflicts with using title or literal title')
+                       sys.exit(u'ERROR: using output template conflicts with using title or literal title')
                if opts.usetitle and opts.useliteral:
                if opts.usetitle and opts.useliteral:
-                       sys.exit('ERROR: using title conflicts with using literal title')
+                       sys.exit(u'ERROR: using title conflicts with using literal title')
                if opts.username is not None and opts.password is None:
                if opts.username is not None and opts.password is None:
-                       opts.password = getpass.getpass('Type account password and press return:')
+                       opts.password = getpass.getpass(u'Type account password and press return:')
+               if opts.ratelimit is not None:
+                       numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
+                       if numeric_limit is None:
+                               sys.exit(u'ERROR: invalid rate limit specified')
+                       opts.ratelimit = numeric_limit
 
                # Information extractors
                youtube_ie = YoutubeIE()
 
                # Information extractors
                youtube_ie = YoutubeIE()
+               metacafe_ie = MetacafeIE(youtube_ie)
+               youtube_pl_ie = YoutubePlaylistIE(youtube_ie)
+               youtube_search_ie = YoutubeSearchIE(youtube_ie)
 
                # File downloader
 
                # File downloader
+               charset = locale.getdefaultlocale()[1]
+               if charset is None:
+                       charset = 'ascii'
                fd = FileDownloader({
                        'usenetrc': opts.usenetrc,
                        'username': opts.username,
                fd = FileDownloader({
                        'usenetrc': opts.usenetrc,
                        'username': opts.username,
@@ -548,15 +1025,24 @@ if __name__ == '__main__':
                        'forcetitle': opts.gettitle,
                        'simulate': (opts.simulate or opts.geturl or opts.gettitle),
                        'format': opts.format,
                        'forcetitle': opts.gettitle,
                        'simulate': (opts.simulate or opts.geturl or opts.gettitle),
                        'format': opts.format,
-                       'outtmpl': ((opts.outtmpl is not None and opts.outtmpl)
-                               or (opts.usetitle and '%(stitle)s-%(id)s.%(ext)s')
-                               or (opts.useliteral and '%(title)s-%(id)s.%(ext)s')
-                               or '%(id)s.%(ext)s'),
+                       'outtmpl': ((opts.outtmpl is not None and opts.outtmpl.decode(charset))
+                               or (opts.usetitle and u'%(stitle)s-%(id)s.%(ext)s')
+                               or (opts.useliteral and u'%(title)s-%(id)s.%(ext)s')
+                               or u'%(id)s.%(ext)s'),
                        'ignoreerrors': opts.ignoreerrors,
                        'ignoreerrors': opts.ignoreerrors,
+                       'ratelimit': opts.ratelimit,
+                       'nooverwrites': opts.nooverwrites,
                        })
                        })
+               fd.add_info_extractor(youtube_search_ie)
+               fd.add_info_extractor(youtube_pl_ie)
+               fd.add_info_extractor(metacafe_ie)
                fd.add_info_extractor(youtube_ie)
                fd.add_info_extractor(youtube_ie)
-               retcode = fd.download(args)
+               retcode = fd.download(all_urls)
                sys.exit(retcode)
 
                sys.exit(retcode)
 
+       except DownloadError:
+               sys.exit(1)
+       except SameFileError:
+               sys.exit(u'ERROR: fixed output name but more than one file to download')
        except KeyboardInterrupt:
        except KeyboardInterrupt:
-               sys.exit('\nERROR: Interrupted by user')
+               sys.exit(u'\nERROR: Interrupted by user')