Use non-greedy regexps, for safety.
[youtube-dl] / youtube-dl
index e28788e3c9b463efc694d4ff23fce85c09f138cc..e7459062df1ce0f14b33ea37e66867ffe9bdac42 100755 (executable)
@@ -283,6 +283,7 @@ class FileDownloader(object):
        logtostderr:      Log messages to stderr instead of stdout.
        consoletitle:     Display progress in console window's titlebar.
        nopart:           Do not use temporary .part files.
+       updatetime:       Use the Last-modified header to set output file timestamps.
        """
 
        params = None
@@ -460,6 +461,23 @@ class FileDownloader(object):
                        os.rename(old_filename, new_filename)
                except (IOError, OSError), err:
                        self.trouble(u'ERROR: unable to rename file')
+       
+       def try_utime(self, filename, last_modified_hdr):
+               """Try to set the last-modified time of the given file."""
+               if last_modified_hdr is None:
+                       return
+               if not os.path.isfile(filename):
+                       return
+               timestr = last_modified_hdr
+               if timestr is None:
+                       return
+               filetime = timeconvert(timestr)
+               if filetime is None:
+                       return
+               try:
+                       os.utime(filename,(time.time(), filetime))
+               except:
+                       pass
 
        def report_destination(self, filename):
                """Report destination filename."""
@@ -757,15 +775,11 @@ class FileDownloader(object):
                if data_len is not None and byte_counter != data_len:
                        raise ContentTooShortError(byte_counter, long(data_len))
                self.try_rename(tmpfilename, filename)
+
                # Update file modification time
-               timestr = data.info().get('last-modified', None)
-               if timestr is not None:
-                       filetime = timeconvert(timestr)
-                       if filetime is not None:
-                               try:
-                                       os.utime(filename,(time.time(), filetime))
-                               except:
-                                       pass
+               if self.params.get('updatetime', True):
+                       self.try_utime(filename, data.info().get('last-modified', None))
+
                return True
 
 class InfoExtractor(object):
@@ -1704,6 +1718,118 @@ class YahooIE(InfoExtractor):
                        self._downloader.trouble(u'\nERROR: unable to download video')
 
 
+class VimeoIE(InfoExtractor):
+       """Information extractor for vimeo.com."""
+
+       # _VALID_URL matches Vimeo URLs
+       _VALID_URL = r'(?:http://)?vimeo\.com/([0-9]+)'
+
+       def __init__(self, downloader=None):
+               InfoExtractor.__init__(self, downloader)
+
+       @staticmethod
+       def suitable(url):
+               return (re.match(VimeoIE._VALID_URL, url) is not None)
+
+       def report_download_webpage(self, video_id):
+               """Report webpage download."""
+               self._downloader.to_screen(u'[video.vimeo] %s: Downloading webpage' % video_id)
+
+       def report_extraction(self, video_id):
+               """Report information extraction."""
+               self._downloader.to_screen(u'[video.vimeo] %s: Extracting information' % video_id)
+
+       def _real_initialize(self):
+               return
+
+       def _real_extract(self, url, new_video=True):
+               # Extract ID from URL
+               mobj = re.match(self._VALID_URL, url)
+               if mobj is None:
+                       self._downloader.trouble(u'ERROR: Invalid URL: %s' % url)
+                       return
+
+               # At this point we have a new video
+               self._downloader.increment_downloads()
+               video_id = mobj.group(1)
+               video_extension = 'flv' # FIXME
+
+               # Retrieve video webpage to extract further information
+               request = urllib2.Request("http://vimeo.com/moogaloop/load/clip:%s" % video_id, None, std_headers)
+               try:
+                       self.report_download_webpage(video_id)
+                       webpage = urllib2.urlopen(request).read()
+               except (urllib2.URLError, httplib.HTTPException, socket.error), err:
+                       self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % str(err))
+                       return
+
+               # Extract uploader and title from webpage
+               self.report_extraction(video_id)
+               mobj = re.search(r'<caption>(.*?)</caption>', webpage)
+               if mobj is None:
+                       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)
+
+               mobj = re.search(r'<uploader_url>http://vimeo.com/(.*?)</uploader_url>', webpage)
+               if mobj is None:
+                       self._downloader.trouble(u'ERROR: unable to extract video uploader')
+                       return
+               video_uploader = mobj.group(1).decode('utf-8')
+
+               # Extract video thumbnail
+               mobj = re.search(r'<thumbnail>(.*?)</thumbnail>', webpage)
+               if mobj is None:
+                       self._downloader.trouble(u'ERROR: unable to extract video thumbnail')
+                       return
+               video_thumbnail = mobj.group(1).decode('utf-8')
+
+               # # Extract video description
+               # mobj = re.search(r'<meta property="og:description" content="(.*)" />', webpage)
+               # if mobj is None:
+               #       self._downloader.trouble(u'ERROR: unable to extract video description')
+               #       return
+               # video_description = mobj.group(1).decode('utf-8')
+               # if not video_description: video_description = 'No description available.'
+               video_description = 'Foo.'
+
+               # Extract request signature
+               mobj = re.search(r'<request_signature>(.*?)</request_signature>', webpage)
+               if mobj is None:
+                       self._downloader.trouble(u'ERROR: unable to extract request signature')
+                       return
+               sig = mobj.group(1).decode('utf-8')
+
+               # Extract request signature expiration
+               mobj = re.search(r'<request_signature_expires>(.*?)</request_signature_expires>', webpage)
+               if mobj is None:
+                       self._downloader.trouble(u'ERROR: unable to extract request signature expiration')
+                       return
+               sig_exp = mobj.group(1).decode('utf-8')
+
+               video_url = "http://vimeo.com/moogaloop/play/clip:%s/%s/%s" % (video_id, sig, sig_exp)
+
+               try:
+                       # Process video information
+                       self._downloader.process_info({
+                               'id':           video_id.decode('utf-8'),
+                               'url':          video_url,
+                               'uploader':     video_uploader,
+                               'upload_date':  u'NA',
+                               'title':        video_title,
+                               'stitle':       simple_title,
+                               'ext':          video_extension.decode('utf-8'),
+                               'thumbnail':    video_thumbnail.decode('utf-8'),
+                               'description':  video_description,
+                               'thumbnail':    video_thumbnail,
+                               'description':  video_description,
+                               'player_url':   None,
+                       })
+               except UnavailableVideoError:
+                       self._downloader.trouble(u'ERROR: unable to download video')
+
+
 class GenericIE(InfoExtractor):
        """Generic last-resort information extractor."""
 
@@ -2379,7 +2505,8 @@ if __name__ == '__main__':
                parser.add_option('--playlist-end',
                                dest='playlistend', metavar='NUMBER', help='playlist video to end at (default is last)', default=-1)
                parser.add_option('--dump-user-agent',
-                               action='store_true', dest='dump_user_agent', help='display the current browser identification', default=False)
+                               action='store_true', dest='dump_user_agent',
+                               help='display the current browser identification', default=False)
 
                authentication = optparse.OptionGroup(parser, 'Authentication Options')
                authentication.add_option('-u', '--username',
@@ -2409,15 +2536,19 @@ if __name__ == '__main__':
                verbosity.add_option('-e', '--get-title',
                                action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
                verbosity.add_option('--get-thumbnail',
-                               action='store_true', dest='getthumbnail', help='simulate, quiet but print thumbnail URL', default=False)
+                               action='store_true', dest='getthumbnail',
+                               help='simulate, quiet but print thumbnail URL', default=False)
                verbosity.add_option('--get-description',
-                               action='store_true', dest='getdescription', help='simulate, quiet but print video description', default=False)
+                               action='store_true', dest='getdescription',
+                               help='simulate, quiet but print video description', default=False)
                verbosity.add_option('--get-filename',
-                               action='store_true', dest='getfilename', help='simulate, quiet but print output filename', default=False)
+                               action='store_true', dest='getfilename',
+                               help='simulate, quiet but print output filename', default=False)
                verbosity.add_option('--no-progress',
                                action='store_true', dest='noprogress', help='do not print progress bar', default=False)
                verbosity.add_option('--console-title',
-                               action='store_true', dest='consoletitle', help='display progress in console titlebar', default=False)
+                               action='store_true', dest='consoletitle',
+                               help='display progress in console titlebar', default=False)
                parser.add_option_group(verbosity)
 
                filesystem = optparse.OptionGroup(parser, 'Filesystem Options')
@@ -2426,7 +2557,8 @@ if __name__ == '__main__':
                filesystem.add_option('-l', '--literal',
                                action='store_true', dest='useliteral', help='use literal title in file name', default=False)
                filesystem.add_option('-A', '--auto-number',
-                               action='store_true', dest='autonumber', help='number downloaded files starting from 00000', default=False)
+                               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')
                filesystem.add_option('-a', '--batch-file',
@@ -2439,6 +2571,9 @@ if __name__ == '__main__':
                                dest='cookiefile', metavar='FILE', help='file to dump cookie jar to')
                filesystem.add_option('--no-part',
                                action='store_true', dest='nopart', help='do not use .part files', default=False)
+               filesystem.add_option('--no-mtime',
+                               action='store_false', dest='updatetime',
+                               help='do not use the Last-modified header to set the file modification time', default=True)
                parser.add_option_group(filesystem)
 
                (opts, args) = parser.parse_args()
@@ -2514,6 +2649,7 @@ if __name__ == '__main__':
                        parser.error(u'invalid playlist end number specified')
 
                # Information extractors
+               vimeo_ie = VimeoIE()
                youtube_ie = YoutubeIE()
                metacafe_ie = MetacafeIE(youtube_ie)
                dailymotion_ie = DailymotionIE()
@@ -2563,7 +2699,9 @@ if __name__ == '__main__':
                        'logtostderr': opts.outtmpl == '-',
                        'consoletitle': opts.consoletitle,
                        'nopart': opts.nopart,
+                       'updatetime': opts.updatetime,
                        })
+               fd.add_info_extractor(vimeo_ie)
                fd.add_info_extractor(youtube_search_ie)
                fd.add_info_extractor(youtube_pl_ie)
                fd.add_info_extractor(youtube_user_ie)