diff options
| -rwxr-xr-x | devscripts/release.sh | 2 | ||||
| -rw-r--r-- | test/test_all_urls.py | 14 | ||||
| -rw-r--r-- | test/test_youtube_lists.py | 58 | ||||
| -rw-r--r-- | test/tests.json | 30 | ||||
| -rw-r--r-- | youtube_dl/FileDownloader.py | 31 | ||||
| -rwxr-xr-x | youtube_dl/InfoExtractors.py | 326 | ||||
| -rw-r--r-- | youtube_dl/__init__.py | 16 | ||||
| -rw-r--r-- | youtube_dl/update.py | 16 | ||||
| -rw-r--r-- | youtube_dl/version.py | 2 | 
9 files changed, 287 insertions, 208 deletions
diff --git a/devscripts/release.sh b/devscripts/release.sh index ced5d4e2f..ee650f221 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -22,7 +22,7 @@ if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit  /bin/echo -e "\n### First of all, testing..."  make cleanall -nosetests --with-coverage --cover-package=youtube_dl --cover-html test || exit 1 +nosetests --with-coverage --cover-package=youtube_dl --cover-html test --stop || exit 1  /bin/echo -e "\n### Changing version in version.py..."  sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/version.py diff --git a/test/test_all_urls.py b/test/test_all_urls.py index 06de8e7b8..69717b3fc 100644 --- a/test/test_all_urls.py +++ b/test/test_all_urls.py @@ -11,12 +11,18 @@ from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE  class TestAllURLsMatching(unittest.TestCase):      def test_youtube_playlist_matching(self): -        self.assertTrue(YoutubePlaylistIE().suitable(u'ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) -        self.assertTrue(YoutubePlaylistIE().suitable(u'PL63F0C78739B09958')) -        self.assertFalse(YoutubePlaylistIE().suitable(u'PLtS2H6bU1M')) +        self.assertTrue(YoutubePlaylistIE.suitable(u'ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) +        self.assertTrue(YoutubePlaylistIE.suitable(u'UUBABnxM4Ar9ten8Mdjj1j0Q')) #585 +        self.assertTrue(YoutubePlaylistIE.suitable(u'PL63F0C78739B09958')) +        self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')) +        self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) +        self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')) +        self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668 +        self.assertFalse(YoutubePlaylistIE.suitable(u'PLtS2H6bU1M'))      def test_youtube_matching(self): -        self.assertTrue(YoutubeIE().suitable(u'PLtS2H6bU1M')) +        self.assertTrue(YoutubeIE.suitable(u'PLtS2H6bU1M')) +        self.assertFalse(YoutubeIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668      def test_youtube_extract(self):          self.assertEqual(YoutubeIE()._extract_id('http://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc') diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index 3044e0852..f4705bc5b 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -8,7 +8,7 @@ import json  import os  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from youtube_dl.InfoExtractors import YoutubeUserIE,YoutubePlaylistIE +from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE  from youtube_dl.utils import *  PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") @@ -35,39 +35,51 @@ class FakeDownloader(object):  class TestYoutubeLists(unittest.TestCase):      def test_youtube_playlist(self): -        DL = FakeDownloader() -        IE = YoutubePlaylistIE(DL) -        IE.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') -        self.assertEqual(DL.result, [ -            ['http://www.youtube.com/watch?v=bV9L5Ht9LgY'], -            ['http://www.youtube.com/watch?v=FXxLjLQi3Fg'], -            ['http://www.youtube.com/watch?v=tU3Bgo5qJZE'] -        ]) +        dl = FakeDownloader() +        ie = YoutubePlaylistIE(dl) +        ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') +        ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result] +        self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE']) + +    def test_issue_673(self): +        dl = FakeDownloader() +        ie = YoutubePlaylistIE(dl) +        ie.extract('PLBB231211A4F62143') +        self.assertTrue(len(dl.result) > 40)      def test_youtube_playlist_long(self): -        DL = FakeDownloader() -        IE = YoutubePlaylistIE(DL) -        IE.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') -        self.assertTrue(len(DL.result) >= 799) +        dl = FakeDownloader() +        ie = YoutubePlaylistIE(dl) +        ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') +        self.assertTrue(len(dl.result) >= 799) + +    def test_youtube_playlist_with_deleted(self): +        #651 +        dl = FakeDownloader() +        ie = YoutubePlaylistIE(dl) +        ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') +        ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result] +        self.assertFalse('pElCt5oNDuI' in ytie_results) +        self.assertFalse('KdPEApIVdWM' in ytie_results)      def test_youtube_course(self): -        DL = FakeDownloader() -        IE = YoutubePlaylistIE(DL) +        dl = FakeDownloader() +        ie = YoutubePlaylistIE(dl)          # TODO find a > 100 (paginating?) videos course -        IE.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') -        self.assertEqual(DL.result[0], ['http://www.youtube.com/watch?v=j9WZyLZCBzs']) -        self.assertEqual(len(DL.result), 25) -        self.assertEqual(DL.result[-1], ['http://www.youtube.com/watch?v=rYefUsYuEp0']) +        ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') +        self.assertEqual(YoutubeIE()._extract_id(dl.result[0][0]), 'j9WZyLZCBzs') +        self.assertEqual(len(dl.result), 25) +        self.assertEqual(YoutubeIE()._extract_id(dl.result[-1][0]), 'rYefUsYuEp0')      def test_youtube_channel(self):          # I give up, please find a channel that does paginate and test this like test_youtube_playlist_long          pass # TODO      def test_youtube_user(self): -        DL = FakeDownloader() -        IE = YoutubeUserIE(DL) -        IE.extract('https://www.youtube.com/user/TheLinuxFoundation') -        self.assertTrue(len(DL.result) >= 320) +        dl = FakeDownloader() +        ie = YoutubeUserIE(dl) +        ie.extract('https://www.youtube.com/user/TheLinuxFoundation') +        self.assertTrue(len(dl.result) >= 320)  if __name__ == '__main__':      unittest.main() diff --git a/test/tests.json b/test/tests.json index a3c31ae51..fd9d33332 100644 --- a/test/tests.json +++ b/test/tests.json @@ -129,18 +129,6 @@      "md5": "f647e9e90064b53b6e046e75d0241fbd"    },    { -    "name": "TweetReel", -    "url": "http://tweetreel.com/?77smq", -    "file": "77smq.mov", -    "md5": "56b4d9ca9de467920f3f99a6d91255d6", -    "info_dict": { -        "uploader": "itszero", -        "uploader_id": "itszero", -        "upload_date": "20091225", -        "description": "Installing Gentoo Linux on Powerbook G4, it turns out the sleep indicator becomes HDD activity indicator :D" -    } -  }, -  {      "name": "Steam",      "url": "http://store.steampowered.com/video/105600/",      "playlist": [ @@ -293,7 +281,8 @@      "file": "102.mp4",      "md5": "7bc087e71d16f18f9b8ab9fa62a8a031",      "info_dict": { -        "title": "Dan Dennett: The illusion of consciousness" +        "title": "Dan Dennett: The illusion of consciousness", +        "thumbnail": "http://images.ted.com/images/ted/488_389x292.jpg"      }    },    { @@ -304,5 +293,20 @@      "info_dict": {          "title": "Absolute Mehrheit vom 17.02.2013 - Die Highlights, Teil 2"      } +  }, +  { +    "name": "Generic", +    "url": "http://www.hodiho.fr/2013/02/regis-plante-sa-jeep.html", +    "file": "13601338388002.mp4", +    "md5": "85b90ccc9d73b4acd9138d3af4c27f89" +  }, +  { +    "name": "Spiegel", +    "url": "http://www.spiegel.de/video/vulkan-tungurahua-in-ecuador-ist-wieder-aktiv-video-1259285.html", +    "file": "1259285.mp4", +    "md5": "2c2754212136f35fb4b19767d242f66e", +    "info_dict": { +        "title": "Vulkanausbruch in Ecuador: Der \"Feuerschlund\" ist wieder aktiv" +    }    }  ] diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 164d25e54..725d4a016 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -108,7 +108,7 @@ class FileDownloader(object):          self.params = params          if '%(stitle)s' in self.params['outtmpl']: -            self.to_stderr(u'WARNING: %(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.') +            self.report_warning(u'%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')      @staticmethod      def format_bytes(bytes): @@ -238,6 +238,18 @@ class FileDownloader(object):              raise DownloadError(message)          self._download_retcode = 1 +    def report_warning(self, message): +        ''' +        Print the message to stderr, it will be prefixed with 'WARNING:' +        If stderr is a tty file the 'WARNING:' will be colored +        ''' +        if sys.stderr.isatty(): +            _msg_header=u'\033[0;33mWARNING:\033[0m' +        else: +            _msg_header=u'WARNING:' +        warning_message=u'%s %s' % (_msg_header,message) +        self.to_stderr(warning_message) +      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) @@ -364,8 +376,11 @@ class FileDownloader(object):              filename = self.params['outtmpl'] % template_dict              return filename -        except (ValueError, KeyError) as err: -            self.trouble(u'ERROR: invalid system charset or erroneous output template') +        except KeyError as err: +            self.trouble(u'ERROR: Erroneous output template') +            return None +        except ValueError as err: +            self.trouble(u'ERROR: Insufficient system charset ' + repr(preferredencoding()))              return None      def _match_entry(self, info_dict): @@ -521,8 +536,8 @@ class FileDownloader(object):                  # Warn if the _WORKING attribute is False                  if not ie.working(): -                    self.to_stderr(u'WARNING: the program functionality for this site has been marked as broken, ' -                                   u'and will probably not work. If you want to go on, use the -i option.') +                    self.report_warning(u'the program functionality for this site has been marked as broken, ' +                                        u'and will probably not work. If you want to go on, use the -i option.')                  # Suitable InfoExtractor found                  suitable_found = True @@ -577,10 +592,10 @@ class FileDownloader(object):                  self.to_stderr(u'ERROR: ' + e.msg)          if keep_video is False and not self.params.get('keepvideo', False):              try: -                self.to_stderr(u'Deleting original file %s (pass -k to keep)' % filename) +                self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)                  os.remove(encodeFilename(filename))              except (IOError, OSError): -                self.to_stderr(u'WARNING: Unable to remove downloaded video file') +                self.report_warning(u'Unable to remove downloaded video file')      def _download_with_rtmpdump(self, filename, url, player_url, page_url):          self.report_destination(filename) @@ -588,7 +603,7 @@ class FileDownloader(object):          # Check for rtmpdump first          try: -            subprocess.call(['rtmpdump', '-h'], stdout=(file(os.path.devnull, 'w')), stderr=subprocess.STDOUT) +            subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)          except (OSError, IOError):              self.trouble(u'ERROR: RTMP download detected but "rtmpdump" could not be run')              return False diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index ab8bd2104..14fd644a2 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -15,6 +15,7 @@ import email.utils  import xml.etree.ElementTree  import random  import math +import operator  from .utils import * @@ -73,13 +74,15 @@ class InfoExtractor(object):          self._ready = False          self.set_downloader(downloader) -    def suitable(self, url): +    @classmethod +    def suitable(cls, url):          """Receives a URL and returns True if suitable for this IE.""" -        return re.match(self._VALID_URL, url) is not None +        return re.match(cls._VALID_URL, url) is not None -    def working(self): +    @classmethod +    def working(cls):          """Getter method for _WORKING.""" -        return self._WORKING +        return cls._WORKING      def initialize(self):          """Initializes an instance (authentication, etc).""" @@ -123,8 +126,14 @@ class InfoExtractor(object):      def _download_webpage(self, url_or_request, video_id, note=None, errnote=None):          """ Returns the data of the page as a string """          urlh = self._request_webpage(url_or_request, video_id, note, errnote) +        content_type = urlh.headers.get('Content-Type', '') +        m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type) +        if m: +            encoding = m.group(1) +        else: +            encoding = 'utf-8'          webpage_bytes = urlh.read() -        return webpage_bytes.decode('utf-8', 'replace') +        return webpage_bytes.decode(encoding, 'replace')  class YoutubeIE(InfoExtractor): @@ -136,7 +145,6 @@ class YoutubeIE(InfoExtractor):                           (?:youtu\.be/|(?:\w+\.)?youtube(?:-nocookie)?\.com/|                              tube\.majestyc\.net/)                             # the various hostnames, with wildcard subdomains                           (?:.*?\#/)?                                          # handle anchor (#/) redirect urls -                         (?!view_play_list|my_playlists|artist|playlist)      # ignore playlist URLs                           (?:                                                  # the various things that can precede the ID:                               (?:(?:v|embed|e)/)                               # v/ or embed/ or e/                               |(?:                                             # or the v= param in all its forms @@ -188,9 +196,11 @@ class YoutubeIE(InfoExtractor):      }      IE_NAME = u'youtube' -    def suitable(self, url): +    @classmethod +    def suitable(cls, url):          """Receives a URL and returns True if suitable for this IE.""" -        return re.match(self._VALID_URL, url, re.VERBOSE) is not None +        if YoutubePlaylistIE.suitable(url): return False +        return re.match(cls._VALID_URL, url, re.VERBOSE) is not None      def report_lang(self):          """Report attempt to set language.""" @@ -321,7 +331,7 @@ class YoutubeIE(InfoExtractor):                  else:                      raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)              except (IOError, netrc.NetrcParseError) as err: -                self._downloader.to_stderr(u'WARNING: parsing .netrc: %s' % compat_str(err)) +                self._downloader.report_warning(u'parsing .netrc: %s' % compat_str(err))                  return          # Set language @@ -330,7 +340,7 @@ class YoutubeIE(InfoExtractor):              self.report_lang()              compat_urllib_request.urlopen(request).read()          except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: -            self._downloader.to_stderr(u'WARNING: unable to set language: %s' % compat_str(err)) +            self._downloader.report_warning(u'unable to set language: %s' % compat_str(err))              return          # No authentication to be performed @@ -341,7 +351,7 @@ class YoutubeIE(InfoExtractor):          try:              login_page = compat_urllib_request.urlopen(request).read().decode('utf-8')          except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: -            self._downloader.to_stderr(u'WARNING: unable to fetch login page: %s' % compat_str(err)) +            self._downloader.report_warning(u'unable to fetch login page: %s' % compat_str(err))              return          galx = None @@ -385,10 +395,10 @@ class YoutubeIE(InfoExtractor):              self.report_login()              login_results = compat_urllib_request.urlopen(request).read().decode('utf-8')              if re.search(r'(?i)<form[^>]* id="gaia_loginform"', login_results) is not None: -                self._downloader.to_stderr(u'WARNING: unable to log in: bad username or password') +                self._downloader.report_warning(u'unable to log in: bad username or password')                  return          except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: -            self._downloader.to_stderr(u'WARNING: unable to log in: %s' % compat_str(err)) +            self._downloader.report_warning(u'unable to log in: %s' % compat_str(err))              return          # Confirm age @@ -1308,7 +1318,8 @@ class GenericIE(InfoExtractor):      def report_download_webpage(self, video_id):          """Report webpage download.""" -        self._downloader.to_screen(u'WARNING: Falling back on generic information extractor.') +        if not self._downloader.params.get('test', False): +            self._downloader.to_screen(u'WARNING: Falling back on generic information extractor.')          self._downloader.to_screen(u'[generic] %s: Downloading webpage' % video_id)      def report_extraction(self, video_id): @@ -1378,13 +1389,8 @@ class GenericIE(InfoExtractor):          if self._test_redirect(url): return          video_id = url.split('/')[-1] -        request = compat_urllib_request.Request(url)          try: -            self.report_download_webpage(video_id) -            webpage = compat_urllib_request.urlopen(request).read() -        except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: -            self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) -            return +            webpage = self._download_webpage(url, video_id)          except ValueError as err:              # since this is the last-resort InfoExtractor, if              # this error is thrown, it'll be thrown here @@ -1483,7 +1489,7 @@ class YoutubeSearchIE(InfoExtractor):                      self._downloader.trouble(u'ERROR: invalid download number %s for query "%s"' % (n, query))                      return                  elif n > self._max_youtube_results: -                    self._downloader.to_stderr(u'WARNING: ytsearch returns max %i results (you requested %i)' % (self._max_youtube_results, n)) +                    self._downloader.report_warning(u'ytsearch returns max %i results (you requested %i)' % (self._max_youtube_results, n))                      n = self._max_youtube_results                  self._download_n_results(query, n)                  return @@ -1503,12 +1509,16 @@ class YoutubeSearchIE(InfoExtractor):              result_url = self._API_URL % (compat_urllib_parse.quote_plus(query), (50*pagenum)+1)              request = compat_urllib_request.Request(result_url)              try: -                data = compat_urllib_request.urlopen(request).read() +                data = compat_urllib_request.urlopen(request).read().decode('utf-8')              except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:                  self._downloader.trouble(u'ERROR: unable to download API page: %s' % compat_str(err))                  return              api_response = json.loads(data)['data'] +            if not 'items' in api_response: +                self._downloader.trouble(u'[youtube] No video results') +                return +              new_ids = list(video['id'] for video in api_response['items'])              video_ids += new_ids @@ -1561,7 +1571,7 @@ class GoogleSearchIE(InfoExtractor):                      self._downloader.trouble(u'ERROR: invalid download number %s for query "%s"' % (n, query))                      return                  elif n > self._max_google_results: -                    self._downloader.to_stderr(u'WARNING: gvsearch returns max %i results (you requested %i)' % (self._max_google_results, n)) +                    self._downloader.report_warning(u'gvsearch returns max %i results (you requested %i)' % (self._max_google_results, n))                      n = self._max_google_results                  self._download_n_results(query, n)                  return @@ -1645,7 +1655,7 @@ class YahooSearchIE(InfoExtractor):                      self._downloader.trouble(u'ERROR: invalid download number %s for query "%s"' % (n, query))                      return                  elif n > self._max_yahoo_results: -                    self._downloader.to_stderr(u'WARNING: yvsearch returns max %i results (you requested %i)' % (self._max_yahoo_results, n)) +                    self._downloader.report_warning(u'yvsearch returns max %i results (you requested %i)' % (self._max_yahoo_results, n))                      n = self._max_yahoo_results                  self._download_n_results(query, n)                  return @@ -1693,80 +1703,94 @@ class YahooSearchIE(InfoExtractor):  class YoutubePlaylistIE(InfoExtractor):      """Information Extractor for YouTube playlists.""" -    _VALID_URL = r'(?:(?:https?://)?(?:\w+\.)?youtube\.com/(?:(?:course|view_play_list|my_playlists|artist|playlist)\?.*?(p|a|list)=|user/.*?/user/|p/|user/.*?#[pg]/c/)(?:PL|EC)?|PL|EC)([0-9A-Za-z-_]{10,})(?:/.*?/([0-9A-Za-z_-]+))?.*' -    _TEMPLATE_URL = 'http://www.youtube.com/%s?%s=%s&page=%s&gl=US&hl=en' -    _VIDEO_INDICATOR_TEMPLATE = r'/watch\?v=(.+?)&([^&"]+&)*list=.*?%s' -    _MORE_PAGES_INDICATOR = u"Next \N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}" +    _VALID_URL = r"""(?: +                        (?:https?://)? +                        (?:\w+\.)? +                        youtube\.com/ +                        (?: +                           (?:course|view_play_list|my_playlists|artist|playlist|watch) +                           \? (?:.*?&)*? (?:p|a|list)= +                        |  user/.*?/user/ +                        |  p/ +                        |  user/.*?#[pg]/c/ +                        ) +                        ((?:PL|EC|UU)?[0-9A-Za-z-_]{10,}) +                        .* +                     | +                        ((?:PL|EC|UU)[0-9A-Za-z-_]{10,}) +                     )""" +    _TEMPLATE_URL = 'https://gdata.youtube.com/feeds/api/playlists/%s?max-results=%i&start-index=%i&v=2&alt=json' +    _MAX_RESULTS = 50      IE_NAME = u'youtube:playlist'      def __init__(self, downloader=None):          InfoExtractor.__init__(self, downloader) +    @classmethod +    def suitable(cls, url): +        """Receives a URL and returns True if suitable for this IE.""" +        return re.match(cls._VALID_URL, url, re.VERBOSE) is not None +      def report_download_page(self, playlist_id, pagenum):          """Report attempt to download playlist page with given number."""          self._downloader.to_screen(u'[youtube] PL %s: Downloading page #%s' % (playlist_id, pagenum))      def _real_extract(self, url):          # Extract playlist id -        mobj = re.match(self._VALID_URL, url) +        mobj = re.match(self._VALID_URL, url, re.VERBOSE)          if mobj is None:              self._downloader.trouble(u'ERROR: invalid url: %s' % url)              return -        # Single video case -        if mobj.group(3) is not None: -            self._downloader.download([mobj.group(3)]) -            return - -        # Download playlist pages -        # prefix is 'p' as default for playlists but there are other types that need extra care -        playlist_prefix = mobj.group(1) -        if playlist_prefix == 'a': -            playlist_access = 'artist' -        else: -            playlist_prefix = 'p' -            playlist_access = 'view_play_list' -        playlist_id = mobj.group(2) -        video_ids = [] -        pagenum = 1 +        # Download playlist videos from API +        playlist_id = mobj.group(1) or mobj.group(2) +        page_num = 1 +        videos = []          while True: -            self.report_download_page(playlist_id, pagenum) -            url = self._TEMPLATE_URL % (playlist_access, playlist_prefix, playlist_id, pagenum) -            request = compat_urllib_request.Request(url) +            self.report_download_page(playlist_id, page_num) + +            url = self._TEMPLATE_URL % (playlist_id, self._MAX_RESULTS, self._MAX_RESULTS * (page_num - 1) + 1)              try: -                page = compat_urllib_request.urlopen(request).read().decode('utf-8') +                page = compat_urllib_request.urlopen(url).read().decode('utf8')              except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:                  self._downloader.trouble(u'ERROR: unable to download webpage: %s' % compat_str(err))                  return -            # Extract video identifiers -            ids_in_page = [] -            for mobj in re.finditer(self._VIDEO_INDICATOR_TEMPLATE % playlist_id, page): -                if mobj.group(1) not in ids_in_page: -                    ids_in_page.append(mobj.group(1)) -            video_ids.extend(ids_in_page) +            try: +                response = json.loads(page) +            except ValueError as err: +                self._downloader.trouble(u'ERROR: Invalid JSON in API response: ' + compat_str(err)) +                return -            if self._MORE_PAGES_INDICATOR not in page: +            if not 'feed' in response or not 'entry' in response['feed']: +                self._downloader.trouble(u'ERROR: Got a malformed response from YouTube API') +                return +            videos += [ (entry['yt$position']['$t'], entry['content']['src']) +                        for entry in response['feed']['entry'] +                        if 'content' in entry ] + +            if len(response['feed']['entry']) < self._MAX_RESULTS:                  break -            pagenum = pagenum + 1 +            page_num += 1 -        total = len(video_ids) +        videos = [v[1] for v in sorted(videos)] +        total = len(videos)          playliststart = self._downloader.params.get('playliststart', 1) - 1          playlistend = self._downloader.params.get('playlistend', -1)          if playlistend == -1: -            video_ids = video_ids[playliststart:] +            videos = videos[playliststart:]          else: -            video_ids = video_ids[playliststart:playlistend] +            videos = videos[playliststart:playlistend] -        if len(video_ids) == total: +        if len(videos) == total:              self._downloader.to_screen(u'[youtube] PL %s: Found %i videos' % (playlist_id, total))          else: -            self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(video_ids))) +            self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(videos))) -        for id in video_ids: -            self._downloader.download(['http://www.youtube.com/watch?v=%s' % id]) +        for video in videos: +            self._downloader.download([video])          return @@ -1952,9 +1976,8 @@ class BlipTVUserIE(InfoExtractor):          while True:              self.report_download_page(username, pagenum) - -            request = compat_urllib_request.Request( page_base + "&page=" + str(pagenum) ) - +            url = page_base + "&page=" + str(pagenum) +            request = compat_urllib_request.Request( url )              try:                  page = compat_urllib_request.urlopen(request).read().decode('utf-8')              except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: @@ -2090,7 +2113,7 @@ class FacebookIE(InfoExtractor):                  else:                      raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE)              except (IOError, netrc.NetrcParseError) as err: -                self._downloader.to_stderr(u'WARNING: parsing .netrc: %s' % compat_str(err)) +                self._downloader.report_warning(u'parsing .netrc: %s' % compat_str(err))                  return          if useremail is None: @@ -2107,10 +2130,10 @@ class FacebookIE(InfoExtractor):              self.report_login()              login_results = compat_urllib_request.urlopen(request).read()              if re.search(r'<form(.*)name="login"(.*)</form>', login_results) is not None: -                self._downloader.to_stderr(u'WARNING: unable to log in: bad username/password, or exceded login rate limit (~3/min). Check credentials or wait.') +                self._downloader.report_warning(u'unable to log in: bad username/password, or exceded login rate limit (~3/min). Check credentials or wait.')                  return          except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: -            self._downloader.to_stderr(u'WARNING: unable to log in: %s' % compat_str(err)) +            self._downloader.report_warning(u'unable to log in: %s' % compat_str(err))              return      def _real_extract(self, url): @@ -2175,6 +2198,17 @@ class BlipTVIE(InfoExtractor):              self._downloader.trouble(u'ERROR: invalid URL: %s' % url)              return +        urlp = compat_urllib_parse_urlparse(url) +        if urlp.path.startswith('/play/'): +            request = compat_urllib_request.Request(url) +            response = compat_urllib_request.urlopen(request) +            redirecturl = response.geturl() +            rurlp = compat_urllib_parse_urlparse(redirecturl) +            file_id = compat_parse_qs(rurlp.fragment)['file'][0].rpartition('/')[2] +            url = 'http://blip.tv/a/a-' + file_id +            return self._real_extract(url) + +          if '?' in url:              cchar = '&'          else: @@ -2330,9 +2364,10 @@ class ComedyCentralIE(InfoExtractor):          '400': '384x216',      } -    def suitable(self, url): +    @classmethod +    def suitable(cls, url):          """Receives a URL and returns True if suitable for this IE.""" -        return re.match(self._VALID_URL, url, re.VERBOSE) is not None +        return re.match(cls._VALID_URL, url, re.VERBOSE) is not None      def report_extraction(self, episode_id):          self._downloader.to_screen(u'[comedycentral] %s: Extracting information' % episode_id) @@ -2555,7 +2590,7 @@ class EscapistIE(InfoExtractor):              'uploader': showName,              'upload_date': None,              'title': showName, -            'ext': 'flv', +            'ext': 'mp4',              'thumbnail': imgUrl,              'description': description,              'player_url': playerUrl, @@ -3589,65 +3624,17 @@ class FunnyOrDieIE(InfoExtractor):          }          return [info] -class TweetReelIE(InfoExtractor): -    _VALID_URL = r'^(?:https?://)?(?:www\.)?tweetreel\.com/[?](?P<id>[0-9a-z]+)$' - -    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 - -        video_id = mobj.group('id') -        webpage = self._download_webpage(url, video_id) - -        m = re.search(r'<div id="left" status_id="([0-9]+)">', webpage) -        if not m: -            self._downloader.trouble(u'ERROR: Cannot find status ID') -        status_id = m.group(1) - -        m = re.search(r'<div class="tweet_text">(.*?)</div>', webpage, flags=re.DOTALL) -        if not m: -            self._downloader.trouble(u'WARNING: Cannot find description') -        desc = unescapeHTML(re.sub('<a.*?</a>', '', m.group(1))).strip() - -        m = re.search(r'<div class="tweet_info">.*?from <a target="_blank" href="https?://twitter.com/(?P<uploader_id>.+?)">(?P<uploader>.+?)</a>', webpage, flags=re.DOTALL) -        if not m: -            self._downloader.trouble(u'ERROR: Cannot find uploader') -        uploader = unescapeHTML(m.group('uploader')) -        uploader_id = unescapeHTML(m.group('uploader_id')) - -        m = re.search(r'<span unixtime="([0-9]+)"', webpage) -        if not m: -            self._downloader.trouble(u'ERROR: Cannot find upload date') -        upload_date = datetime.datetime.fromtimestamp(int(m.group(1))).strftime('%Y%m%d') - -        title = desc -        video_url = 'http://files.tweetreel.com/video/' + status_id + '.mov' - -        info = { -            'id': video_id, -            'url': video_url, -            'ext': 'mov', -            'title': title, -            'description': desc, -            'uploader': uploader, -            'uploader_id': uploader_id, -            'internal_id': status_id, -            'upload_date': upload_date -        } -        return [info] -          class SteamIE(InfoExtractor): -    _VALID_URL = r"""http://store.steampowered.com/  +    _VALID_URL = r"""http://store.steampowered.com/                  (?P<urltype>video|app)/ #If the page is only for videos or for a game                  (?P<gameID>\d+)/?                  (?P<videoID>\d*)(?P<extra>\??) #For urltype == video we sometimes get the videoID                  """ -    def suitable(self, url): +    @classmethod +    def suitable(cls, url):          """Receives a URL and returns True if suitable for this IE.""" -        return re.match(self._VALID_URL, url, re.VERBOSE) is not None +        return re.match(cls._VALID_URL, url, re.VERBOSE) is not None      def _real_extract(self, url):          m = re.match(self._VALID_URL, url, re.VERBOSE) @@ -3658,18 +3645,22 @@ class SteamIE(InfoExtractor):          mweb = re.finditer(urlRE, webpage)          namesRE = r'<span class="title">(?P<videoName>.+?)</span>'          titles = re.finditer(namesRE, webpage) +        thumbsRE = r'<img class="movie_thumb" src="(?P<thumbnail>.+?)">' +        thumbs = re.finditer(thumbsRE, webpage)          videos = [] -        for vid,vtitle in zip(mweb,titles): +        for vid,vtitle,thumb in zip(mweb,titles,thumbs):              video_id = vid.group('videoID')              title = vtitle.group('videoName')              video_url = vid.group('videoURL') +            video_thumb = thumb.group('thumbnail')              if not video_url:                  self._downloader.trouble(u'ERROR: Cannot find video url for %s' % video_id)              info = {                  'id':video_id,                  'url':video_url,                  'ext': 'flv', -                'title': unescapeHTML(title) +                'title': unescapeHTML(title), +                'thumbnail': video_thumb                    }              videos.append(info)          return videos @@ -3735,7 +3726,7 @@ class RBMARadioIE(InfoExtractor):  class YouPornIE(InfoExtractor):      """Information extractor for youporn.com."""      _VALID_URL = r'^(?:https?://)?(?:\w+\.)?youporn\.com/watch/(?P<videoid>[0-9]+)/(?P<title>[^/]+)' -    +      def _print_formats(self, formats):          """Print all available formats"""          print(u'Available formats:') @@ -3771,7 +3762,7 @@ class YouPornIE(InfoExtractor):          # Get the video date          result = re.search(r'Date:</label>(?P<date>.*) </li>', webpage)          if result is None: -            self._downloader.to_stderr(u'WARNING: unable to extract video date') +            self._downloader.report_warning(u'unable to extract video date')              upload_date = None          else:              upload_date = result.group('date').strip() @@ -3779,7 +3770,7 @@ class YouPornIE(InfoExtractor):          # Get the video uploader          result = re.search(r'Submitted:</label>(?P<uploader>.*)</li>', webpage)          if result is None: -            self._downloader.to_stderr(u'WARNING: unable to extract uploader') +            self._downloader.report_warning(u'unable to extract uploader')              video_uploader = None          else:              video_uploader = result.group('uploader').strip() @@ -3797,8 +3788,8 @@ class YouPornIE(InfoExtractor):          links = re.findall(LINK_RE, download_list_html)          if(len(links) == 0):              raise ExtractorError(u'ERROR: no known formats available for video') -         -        self._downloader.to_screen(u'[youporn] Links found: %d' % len(links))    + +        self._downloader.to_screen(u'[youporn] Links found: %d' % len(links))          formats = []          for link in links: @@ -3849,7 +3840,7 @@ class YouPornIE(InfoExtractor):                  return              return [format] -         +  class PornotubeIE(InfoExtractor):      """Information extractor for pornotube.com.""" @@ -3921,7 +3912,7 @@ class YouJizzIE(InfoExtractor):          embed_page_url = result.group(0).strip()          video_id = result.group('videoid') -     +          webpage = self._download_webpage(embed_page_url, video_id)          # Get the video URL @@ -4017,9 +4008,10 @@ class TEDIE(InfoExtractor):                     /(?P<name>\w+) # Here goes the name and then ".html"                     ''' -    def suitable(self, url): +    @classmethod +    def suitable(cls, url):          """Receives a URL and returns True if suitable for this IE.""" -        return re.match(self._VALID_URL, url, re.VERBOSE) is not None +        return re.match(cls._VALID_URL, url, re.VERBOSE) is not None      def _real_extract(self, url):          m=re.match(self._VALID_URL, url, re.VERBOSE) @@ -4042,31 +4034,30 @@ class TEDIE(InfoExtractor):                       ([.\s]*?)data-playlist_item_id="(\d+)"                       ([.\s]*?)data-mediaslug="(?P<mediaSlug>.+?)"                       ''' -        video_name_RE=r'<p\ class="talk-title"><a href="/talks/(.+).html">(?P<fullname>.+?)</a></p>' +        video_name_RE=r'<p\ class="talk-title"><a href="(?P<talk_url>/talks/(.+).html)">(?P<fullname>.+?)</a></p>'          webpage=self._download_webpage(url, playlist_id, 'Downloading playlist webpage')          m_videos=re.finditer(video_RE,webpage,re.VERBOSE)          m_names=re.finditer(video_name_RE,webpage)          info=[]          for m_video, m_name in zip(m_videos,m_names): -            video_dic={ -                       'id': m_video.group('video_id'), -                       'url': self._talk_video_link(m_video.group('mediaSlug')), -                       'ext': 'mp4', -                       'title': m_name.group('fullname') -                       } -            info.append(video_dic) +            video_id=m_video.group('video_id') +            talk_url='http://www.ted.com%s' % m_name.group('talk_url') +            info.append(self._talk_info(talk_url,video_id))          return info +      def _talk_info(self, url, video_id=0):          """Return the video for the talk in the url"""          m=re.match(self._VALID_URL, url,re.VERBOSE)          videoName=m.group('name')          webpage=self._download_webpage(url, video_id, 'Downloading \"%s\" page' % videoName)          # If the url includes the language we get the title translated -        title_RE=r'<h1><span id="altHeadline" >(?P<title>[\s\w:/\.\?=\+-\\\']*)</span></h1>' +        title_RE=r'<h1><span id="altHeadline" >(?P<title>.*)</span></h1>'          title=re.search(title_RE, webpage).group('title')          info_RE=r'''<script\ type="text/javascript">var\ talkDetails\ =(.*?)                          "id":(?P<videoID>[\d]+).*?                          "mediaSlug":"(?P<mediaSlug>[\w\d]+?)"''' +        thumb_RE=r'</span>[\s.]*</div>[\s.]*<img src="(?P<thumbnail>.*?)"' +        thumb_match=re.search(thumb_RE,webpage)          info_match=re.search(info_RE,webpage,re.VERBOSE)          video_id=info_match.group('videoID')          mediaSlug=info_match.group('mediaSlug') @@ -4075,13 +4066,14 @@ class TEDIE(InfoExtractor):                  'id': video_id,                  'url': video_url,                  'ext': 'mp4', -                'title': title +                'title': title, +                'thumbnail': thumb_match.group('thumbnail')                  }          return info  class MySpassIE(InfoExtractor):      _VALID_URL = r'http://www.myspass.de/.*' -     +      def _real_extract(self, url):          META_DATA_URL_TEMPLATE = 'http://www.myspass.de/myspass/includes/apps/video/getvideometadataxml.php?id=%s' @@ -4091,12 +4083,12 @@ class MySpassIE(InfoExtractor):          url_parent_path, video_id = os.path.split(url_path)          if not video_id:              _, video_id = os.path.split(url_parent_path) -         +          # get metadata          metadata_url = META_DATA_URL_TEMPLATE % video_id          metadata_text = self._download_webpage(metadata_url, video_id)          metadata = xml.etree.ElementTree.fromstring(metadata_text.encode('utf-8')) -         +          # extract values from metadata          url_flv_el = metadata.find('url_flv')          if url_flv_el is None: @@ -4135,6 +4127,40 @@ class MySpassIE(InfoExtractor):          }          return [info] +class SpiegelIE(InfoExtractor): +    _VALID_URL = r'https?://(?:www\.)?spiegel\.de/video/[^/]*-(?P<videoID>[0-9]+)(?:\.html)?$' + +    def _real_extract(self, url): +        m = re.match(self._VALID_URL, url) +        video_id = m.group('videoID') + +        webpage = self._download_webpage(url, video_id) +        m = re.search(r'<div class="spVideoTitle">(.*?)</div>', webpage) +        if not m: +            raise ExtractorError(u'Cannot find title') +        video_title = unescapeHTML(m.group(1)) + +        xml_url = u'http://video2.spiegel.de/flash/' + video_id + u'.xml' +        xml_code = self._download_webpage(xml_url, video_id, +                    note=u'Downloading XML', errnote=u'Failed to download XML') + +        idoc = xml.etree.ElementTree.fromstring(xml_code) +        last_type = idoc[-1] +        filename = last_type.findall('./filename')[0].text +        duration = float(last_type.findall('./duration')[0].text) + +        video_url = 'http://video2.spiegel.de/flash/' + filename +        video_ext = filename.rpartition('.')[2] +        info = { +            'id': video_id, +            'url': video_url, +            'ext': video_ext, +            'title': video_title, +            'duration': duration, +        } +        return [info] + +  def gen_extractors():      """ Return a list of an instance of every supported extractor.      The order does matter; the first extractor matched is the one handling the URL. @@ -4176,7 +4202,6 @@ def gen_extractors():          NBAIE(),          JustinTVIE(),          FunnyOrDieIE(), -        TweetReelIE(),          SteamIE(),          UstreamIE(),          RBMARadioIE(), @@ -4184,6 +4209,7 @@ def gen_extractors():          KeekIE(),          TEDIE(),          MySpassIE(), +        SpiegelIE(),          GenericIE()      ] diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index c4f64893d..807b73541 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -126,7 +126,7 @@ def parseOpts():      general.add_option('-i', '--ignore-errors',              action='store_true', dest='ignoreerrors', help='continue on download errors', default=False)      general.add_option('-r', '--rate-limit', -            dest='ratelimit', metavar='LIMIT', help='download rate limit (e.g. 50k or 44.6m)') +            dest='ratelimit', metavar='LIMIT', help='maximum download rate (e.g. 50k or 44.6m)')      general.add_option('-R', '--retries',              dest='retries', metavar='RETRIES', help='number of retries (default is %default)', default=10)      general.add_option('--buffer-size', @@ -286,12 +286,20 @@ def parseOpts():      xdg_config_home = os.environ.get('XDG_CONFIG_HOME')      if xdg_config_home: -        userConf = os.path.join(xdg_config_home, 'youtube-dl.conf') +        userConfFile = os.path.join(xdg_config_home, 'youtube-dl.conf')      else: -        userConf = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf') -    argv = _readOptions('/etc/youtube-dl.conf') + _readOptions(userConf) + sys.argv[1:] +        userConfFile = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf') +    systemConf = _readOptions('/etc/youtube-dl.conf') +    userConf = _readOptions(userConfFile) +    commandLineConf = sys.argv[1:] +    argv = systemConf + userConf + commandLineConf      opts, args = parser.parse_args(argv) +    if opts.verbose: +        print(u'[debug] System config: ' + repr(systemConf)) +        print(u'[debug] User config: ' + repr(userConf)) +        print(u'[debug] Command-line args: ' + repr(commandLineConf)) +      return parser, opts, args  def _real_main(): diff --git a/youtube_dl/update.py b/youtube_dl/update.py index f6e3e5c69..b446dd94c 100644 --- a/youtube_dl/update.py +++ b/youtube_dl/update.py @@ -77,10 +77,8 @@ def update_self(to_screen, verbose, filename):      to_screen(u'Updating to version ' + versions_info['latest'] + '...')      version = versions_info['versions'][versions_info['latest']] -    if version.get('notes'): -        to_screen(u'PLEASE NOTE:') -        for note in version['notes']: -            to_screen(note) + +    print_notes(versions_info['versions'])      if not os.access(filename, os.W_OK):          to_screen(u'ERROR: no write permissions on %s' % filename) @@ -158,3 +156,13 @@ del "%s"              return      to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.') + +def print_notes(versions, fromVersion=__version__): +    notes = [] +    for v,vdata in sorted(versions.items()): +        if v > fromVersion: +            notes.extend(vdata.get('notes', [])) +    if notes: +        to_screen(u'PLEASE NOTE:') +        for note in notes: +            to_screen(note) diff --git a/youtube_dl/version.py b/youtube_dl/version.py index 65d9194f5..ce8f6ca23 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.02.19' +__version__ = '2013.02.25'  | 
