Document -o %(upload_date)s (Closes #228)
[youtube-dl.git] / youtube-dl
index 8fc320c..63ad30f 100755 (executable)
@@ -14,10 +14,11 @@ __author__  = (
        'Sören Schulze',
        'Kevin Ngo',
        'Ori Avtalion',
+       'shizeeg',
        )
 
 __license__ = 'Public Domain'
-__version__ = '2011.10.19'
+__version__ = '2011.11.23'
 
 UPDATE_URL = 'https://raw.github.com/rg3/youtube-dl/master/youtube-dl'
 
@@ -79,8 +80,6 @@ std_headers = {
        'Accept-Language': 'en-us,en;q=0.5',
 }
 
-simple_title_chars = string.ascii_letters.decode('ascii') + string.digits.decode('ascii')
-
 try:
        import json
 except ImportError: # Python <2.6, use trivialjson (https://github.com/phihag/trivialjson):
@@ -280,7 +279,8 @@ def timeconvert(timestr):
        return timestamp
 
 def _simplify_title(title):
-       return re.sub(ur'[^\w\d_\-]+', u'_', title)
+       expr = re.compile(ur'[^\w\d_\-]+', flags=re.UNICODE)
+       return expr.sub(u'_', title).strip(u'_')
 
 class DownloadError(Exception):
        """Download Error exception.
@@ -701,6 +701,13 @@ class FileDownloader(object):
 
        def process_info(self, info_dict):
                """Process a single dictionary returned by an InfoExtractor."""
+
+               max_downloads = int(self.params.get('max_downloads'))
+               if max_downloads is not None:
+                       if self._num_downloads > max_downloads:
+                               self.to_screen(u'[download] Maximum number of downloads reached. Skipping ' + info_dict['title'])
+                               return
+               
                filename = self.prepare_filename(info_dict)
                
                # Forced printings
@@ -1293,8 +1300,7 @@ class YoutubeIE(InfoExtractor):
                video_title = sanitize_title(video_title)
 
                # simplified title
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
-               simple_title = simple_title.strip(ur'_')
+               simple_title = _simplify_title(video_title)
 
                # thumbnail image
                if 'thumbnail_url' not in video_info:
@@ -1695,7 +1701,7 @@ class GoogleIE(InfoExtractor):
                        return
                video_title = mobj.group(1).decode('utf-8')
                video_title = sanitize_title(video_title)
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
+               simple_title = _simplify_title(video_title)
 
                # Extract video description
                mobj = re.search(r'<span id=short-desc-content>([^<]*)</span>', webpage)
@@ -1794,7 +1800,7 @@ class PhotobucketIE(InfoExtractor):
                        return
                video_title = mobj.group(1).decode('utf-8')
                video_title = sanitize_title(video_title)
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
+               simple_title = _simplify_title(vide_title)
 
                video_uploader = mobj.group(2).decode('utf-8')
 
@@ -1888,7 +1894,7 @@ class YahooIE(InfoExtractor):
                        self._downloader.trouble(u'ERROR: unable to extract video title')
                        return
                video_title = mobj.group(1).decode('utf-8')
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
+               simple_title = _simplify_title(video_title)
 
                mobj = re.search(r'<h2 class="ti-5"><a href="http://video\.yahoo\.com/(people|profile)/[0-9]+" beacon=".*">(.*)</a></h2>', webpage)
                if mobj is None:
@@ -2016,7 +2022,7 @@ class VimeoIE(InfoExtractor):
                        self._downloader.trouble(u'ERROR: unable to extract video title')
                        return
                video_title = mobj.group(1).decode('utf-8')
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
+               simple_title = _simplify_title(video_title)
 
                # Extract uploader
                mobj = re.search(r'<uploader_url>http://vimeo.com/(.*?)</uploader_url>', webpage)
@@ -2160,7 +2166,7 @@ class GenericIE(InfoExtractor):
                        return
                video_title = mobj.group(1).decode('utf-8')
                video_title = sanitize_title(video_title)
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
+               simple_title = _simplify_title(video_title)
 
                # video uploader is domain name
                mobj = re.match(r'(?:https?://)?([^/]*)/.*', url)
@@ -2830,9 +2836,7 @@ class FacebookIE(InfoExtractor):
                video_title = video_title.decode('utf-8')
                video_title = sanitize_title(video_title)
 
-               # simplified title
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', video_title)
-               simple_title = simple_title.strip(ur'_')
+               simple_title = _simplify_title(video_title)
 
                # thumbnail image
                if 'thumbnail' not in video_info:
@@ -2923,11 +2927,6 @@ class BlipTVIE(InfoExtractor):
                """Report information extraction."""
                self._downloader.to_screen(u'[%s] %s: Direct download detected' % (self.IE_NAME, title))
 
-       def _simplify_title(self, title):
-               res = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', title)
-               res = res.strip(ur'_')
-               return res
-
        def _real_extract(self, url):
                mobj = re.match(self._VALID_URL, url)
                if mobj is None:
@@ -2947,13 +2946,14 @@ class BlipTVIE(InfoExtractor):
                        if urlh.headers.get('Content-Type', '').startswith('video/'): # Direct download
                                basename = url.split('/')[-1]
                                title,ext = os.path.splitext(basename)
+                               title = title.decode('UTF-8')
                                ext = ext.replace('.', '')
                                self.report_direct_download(title)
                                info = {
                                        'id': title,
                                        'url': url,
                                        'title': title,
-                                       'stitle': self._simplify_title(title),
+                                       'stitle': _simplify_title(title),
                                        'ext': ext,
                                        'urlhandle': urlh
                                }
@@ -2987,7 +2987,7 @@ class BlipTVIE(InfoExtractor):
                                        'uploader': data['display_name'],
                                        'upload_date': upload_date,
                                        'title': data['title'],
-                                       'stitle': self._simplify_title(data['title']),
+                                       'stitle': _simplify_title(data['title']),
                                        'ext': ext,
                                        'format': data['media']['mimeType'],
                                        'thumbnail': data['thumbnailUrl'],
@@ -3030,10 +3030,6 @@ class MyVideoIE(InfoExtractor):
                        return
 
                video_id = mobj.group(1)
-               simple_title = mobj.group(2).decode('utf-8')
-               # should actually not be necessary
-               simple_title = sanitize_title(simple_title)
-               simple_title = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', simple_title)
 
                # Get video webpage
                request = urllib2.Request('http://www.myvideo.de/watch/%s' % video_id)
@@ -3060,6 +3056,8 @@ class MyVideoIE(InfoExtractor):
                video_title = mobj.group(1)
                video_title = sanitize_title(video_title)
 
+               simple_title = _simplify_title(video_title)
+
                try:
                        self._downloader.process_info({
                                'id':           video_id,
@@ -3093,11 +3091,6 @@ class ComedyCentralIE(InfoExtractor):
        def report_player_url(self, episode_id):
                self._downloader.to_screen(u'[comedycentral] %s: Determining player URL' % episode_id)
 
-       def _simplify_title(self, title):
-               res = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', title)
-               res = res.strip(ur'_')
-               return res
-
        def _real_extract(self, url):
                mobj = re.match(self._VALID_URL, url)
                if mobj is None:
@@ -3106,9 +3099,9 @@ class ComedyCentralIE(InfoExtractor):
 
                if mobj.group('shortname'):
                        if mobj.group('shortname') in ('tds', 'thedailyshow'):
-                               url = 'http://www.thedailyshow.com/full-episodes/'
+                               url = u'http://www.thedailyshow.com/full-episodes/'
                        else:
-                               url = 'http://www.colbertnation.com/full-episodes/'
+                               url = u'http://www.colbertnation.com/full-episodes/'
                        mobj = re.match(self._VALID_URL, url)
                        assert mobj is not None
 
@@ -3194,14 +3187,14 @@ class ComedyCentralIE(InfoExtractor):
 
                        self._downloader.increment_downloads()
 
-                       effTitle = showId + '-' + epTitle
+                       effTitle = showId + u'-' + epTitle
                        info = {
                                'id': shortMediaId,
                                'url': video_url,
                                'uploader': showId,
                                'upload_date': officialDate,
                                'title': effTitle,
-                               'stitle': self._simplify_title(effTitle),
+                               'stitle': _simplify_title(effTitle),
                                'ext': 'mp4',
                                'format': format,
                                'thumbnail': None,
@@ -3228,11 +3221,6 @@ class EscapistIE(InfoExtractor):
        def report_config_download(self, showName):
                self._downloader.to_screen(u'[escapist] %s: Downloading configuration' % showName)
 
-       def _simplify_title(self, title):
-               res = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', title)
-               res = res.strip(ur'_')
-               return res
-
        def _real_extract(self, url):
                htmlParser = HTMLParser.HTMLParser()
 
@@ -3285,7 +3273,7 @@ class EscapistIE(InfoExtractor):
                        'uploader': showName,
                        'upload_date': None,
                        'title': showName,
-                       'stitle': self._simplify_title(showName),
+                       'stitle': _simplify_title(showName),
                        'ext': 'flv',
                        'format': 'flv',
                        'thumbnail': imgUrl,
@@ -3313,11 +3301,6 @@ class CollegeHumorIE(InfoExtractor):
                """Report information extraction."""
                self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id))
 
-       def _simplify_title(self, title):
-               res = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', title)
-               res = res.strip(ur'_')
-               return res
-
        def _real_extract(self, url):
                htmlParser = HTMLParser.HTMLParser()
 
@@ -3359,7 +3342,7 @@ class CollegeHumorIE(InfoExtractor):
                        videoNode = mdoc.findall('./video')[0]
                        info['description'] = videoNode.findall('./description')[0].text
                        info['title'] = videoNode.findall('./caption')[0].text
-                       info['stitle'] = self._simplify_title(info['title'])
+                       info['stitle'] = _simplify_title(info['title'])
                        info['url'] = videoNode.findall('./file')[0].text
                        info['thumbnail'] = videoNode.findall('./thumbnail')[0].text
                        info['ext'] = info['url'].rpartition('.')[2]
@@ -3390,11 +3373,6 @@ class XVideosIE(InfoExtractor):
                """Report information extraction."""
                self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id))
 
-       def _simplify_title(self, title):
-               res = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', title)
-               res = res.strip(ur'_')
-               return res
-
        def _real_extract(self, url):
                htmlParser = HTMLParser.HTMLParser()
 
@@ -3448,7 +3426,7 @@ class XVideosIE(InfoExtractor):
                        'uploader': None,
                        'upload_date': None,
                        'title': video_title,
-                       'stitle': self._simplify_title(video_title),
+                       'stitle': _simplify_title(video_title),
                        'ext': 'flv',
                        'format': 'flv',
                        'thumbnail': video_thumbnail,
@@ -3537,7 +3515,7 @@ class SoundcloudIE(InfoExtractor):
                if mobj:
                        try:
                                upload_date = datetime.datetime.strptime(mobj.group(1), '%B %d, %Y %H:%M').strftime('%Y%m%d')
-                       except Exception as e:
+                       except Exception, e:
                                print str(e)
 
                # for soundcloud, a request to a cross domain is required for cookies
@@ -3574,11 +3552,6 @@ class InfoQIE(InfoExtractor):
                """Report information extraction."""
                self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id))
 
-       def _simplify_title(self, title):
-               res = re.sub(ur'(?u)([^%s]+)' % simple_title_chars, ur'_', title)
-               res = res.strip(ur'_')
-               return res
-
        def _real_extract(self, url):
                htmlParser = HTMLParser.HTMLParser()
 
@@ -3630,7 +3603,7 @@ class InfoQIE(InfoExtractor):
                        'uploader': None,
                        'upload_date': None,
                        'title': video_title,
-                       'stitle': self._simplify_title(video_title),
+                       'stitle': _simplify_title(video_title),
                        'ext': extension,
                        'format': extension, # Extension is always(?) mp4, but seems to be flv
                        'thumbnail': None,
@@ -3643,6 +3616,127 @@ class InfoQIE(InfoExtractor):
                except UnavailableVideoError, err:
                        self._downloader.trouble(u'\nERROR: unable to download ' + video_url)
 
+class MixcloudIE(InfoExtractor):
+       """Information extractor for www.mixcloud.com"""
+       _VALID_URL = r'^(?:https?://)?(?:www\.)?mixcloud\.com/([\w\d-]+)/([\w\d-]+)'
+       IE_NAME = u'mixcloud'
+
+       def __init__(self, downloader=None):
+               InfoExtractor.__init__(self, downloader)
+
+       def report_download_json(self, file_id):
+               """Report JSON download."""
+               self._downloader.to_screen(u'[%s] Downloading json' % self.IE_NAME)
+
+       def report_extraction(self, file_id):
+               """Report information extraction."""
+               self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, file_id))
+
+       def get_urls(self, jsonData, fmt, bitrate='best'):
+               """Get urls from 'audio_formats' section in json"""
+               file_url = None
+               try:
+                       bitrate_list = jsonData[fmt]
+                       if bitrate is None or bitrate == 'best' or bitrate not in bitrate_list:
+                               bitrate = max(bitrate_list) # select highest
+
+                       url_list = jsonData[fmt][bitrate]
+               except TypeError: # we have no bitrate info.
+                       url_list = jsonData[fmt]
+                               
+               return url_list
+
+       def check_urls(self, url_list):
+               """Returns 1st active url from list"""
+               for url in url_list:
+                       try:
+                               urllib2.urlopen(url)
+                               return url
+                       except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                               url = None
+
+               return None
+
+       def _print_formats(self, formats):
+               print 'Available formats:'
+               for fmt in formats.keys():
+                       for b in formats[fmt]:
+                               try:
+                                       ext = formats[fmt][b][0]
+                                       print '%s\t%s\t[%s]' % (fmt, b, ext.split('.')[-1])
+                               except TypeError: # we have no bitrate info
+                                       ext = formats[fmt][0]
+                                       print '%s\t%s\t[%s]' % (fmt, '??', ext.split('.')[-1])
+                                       break
+
+       def _real_extract(self, url):
+               mobj = re.match(self._VALID_URL, url)
+               if mobj is None:
+                       self._downloader.trouble(u'ERROR: invalid URL: %s' % url)
+                       return
+               # extract uploader & filename from url
+               uploader = mobj.group(1).decode('utf-8')
+               file_id = uploader + "-" + mobj.group(2).decode('utf-8')
+
+               # construct API request
+               file_url = 'http://www.mixcloud.com/api/1/cloudcast/' + '/'.join(url.split('/')[-3:-1]) + '.json'
+               # retrieve .json file with links to files
+               request = urllib2.Request(file_url)
+               try:
+                       self.report_download_json(file_url)
+                       jsonData = urllib2.urlopen(request).read()
+               except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                       self._downloader.trouble(u'ERROR: Unable to retrieve file: %s' % str(err))
+                       return
+
+               # parse JSON
+               json_data = json.loads(jsonData)
+               player_url = json_data['player_swf_url']
+               formats = dict(json_data['audio_formats'])
+
+               req_format = self._downloader.params.get('format', None)
+               bitrate = None
+
+               if self._downloader.params.get('listformats', None):
+                       self._print_formats(formats)
+                       return
+
+               if req_format is None or req_format == 'best':
+                       for format_param in formats.keys():
+                               url_list = self.get_urls(formats, format_param)
+                               # check urls
+                               file_url = self.check_urls(url_list)
+                               if file_url is not None:
+                                       break # got it!
+               else:
+                       if req_format not in formats.keys():
+                               self._downloader.trouble(u'ERROR: format is not available')
+                               return
+
+                       url_list = self.get_urls(formats, req_format)
+                       file_url = self.check_urls(url_list)
+                       format_param = req_format
+
+               # We have audio
+               self._downloader.increment_downloads()
+               try:
+                       # Process file information
+                       self._downloader.process_info({
+                               'id':           file_id.decode('utf-8'),
+                               'url':          file_url.decode('utf-8'),
+                               'uploader':     uploader.decode('utf-8'),
+                               'upload_date':  u'NA',
+                               'title':        json_data['name'],
+                               'stitle':       _simplify_title(json_data['name']),
+                               'ext':          file_url.split('.')[-1].decode('utf-8'),
+                               'format':       (format_param is None and u'NA' or format_param.decode('utf-8')),
+                               'thumbnail':    json_data['thumbnail_url'],
+                               'description':  json_data['description'],
+                               'player_url':   player_url.decode('utf-8'),
+                       })
+               except UnavailableVideoError, err:
+                       self._downloader.trouble(u'ERROR: unable to download file')
+
 
 
 class PostProcessor(object):
@@ -3910,6 +4004,7 @@ def parseOpts():
                        dest='playlistend', metavar='NUMBER', help='playlist video to end at (default is last)', default=-1)
        selection.add_option('--match-title', dest='matchtitle', metavar='REGEX',help='download only matching titles (regex or caseless sub-string)')
        selection.add_option('--reject-title', dest='rejecttitle', metavar='REGEX',help='skip download for matching titles (regex or caseless sub-string)')
+       selection.add_option('--max-downloads', metavar='NUMBER', dest='max_downloads', help='Abort after downloading NUMBER files', default=None)
 
        authentication.add_option('-u', '--username',
                        dest='username', metavar='USERNAME', help='account username')
@@ -3966,7 +4061,7 @@ def parseOpts():
                        action='store_true', dest='autonumber',
                        help='number downloaded files starting from 00000', default=False)
        filesystem.add_option('-o', '--output',
-                       dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(stitle)s to get the title, %(uploader)s for the uploader name, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, and %% for a literal percent')
+                       dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(stitle)s to get the title, %(uploader)s for the uploader name, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), and %% for a literal percent')
        filesystem.add_option('-a', '--batch-file',
                        dest='batchfile', metavar='FILE', help='file containing URLs to download (\'-\' for stdin)')
        filesystem.add_option('-w', '--no-overwrites',
@@ -4043,6 +4138,7 @@ def gen_extractors():
                XVideosIE(),
                SoundcloudIE(),
                InfoQIE(),
+               MixcloudIE(),
 
                GenericIE()
        ]
@@ -4178,6 +4274,7 @@ def _real_main():
                'writeinfojson': opts.writeinfojson,
                'matchtitle': opts.matchtitle,
                'rejecttitle': opts.rejecttitle,
+               'max_downloads': int(opts.max_downloads),
                })
        for extractor in extractors:
                fd.add_info_extractor(extractor)