Modified Youtube video/playlist matching; fixes #668; fixes #585
authorFilippo Valsorda <filippo.valsorda@gmail.com>
Tue, 26 Feb 2013 18:02:31 +0000 (19:02 +0100)
committerFilippo Valsorda <filippo.valsorda@gmail.com>
Tue, 26 Feb 2013 18:06:41 +0000 (19:06 +0100)
test/test_all_urls.py
test/test_youtube_lists.py
youtube_dl/InfoExtractors.py

index 06de8e7b8e27b00fc5e582d551af07ee9b7cd060..69717b3fc7da65cd5d475289d21ae7f3bc1c39fb 100644 (file)
@@ -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')
index 69b0f444731f60d955b2b1ae924b80eb541cbee6..7055d5a7a9bc10ff13b9336f19bb814eb783d227 100644 (file)
@@ -41,6 +41,18 @@ class TestYoutubeLists(unittest.TestCase):
         self.assertEqual(map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result),
             [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE' ])
 
+        #661
+        DL = FakeDownloader()
+        IE = YoutubePlaylistIE(DL)
+        IE.extract('PLMCmkNmxw6Z9eduM7BZjSEh7HiU543Ig0')
+        self.assertTrue(len(DL.result) > 20)
+
+        #673
+        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)
@@ -48,6 +60,7 @@ class TestYoutubeLists(unittest.TestCase):
         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')
index 021579ce01120dffa25e8d58ca54edb8ab72ac19..490f30491a5c1f878a58ea9e337040c927ab28ae 100755 (executable)
@@ -74,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)."""
@@ -137,7 +139,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
@@ -189,9 +190,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."""
@@ -1668,17 +1671,17 @@ class YoutubePlaylistIE(InfoExtractor):
                         (?:\w+\.)?
                         youtube\.com/
                         (?:
-                           (?:course|view_play_list|my_playlists|artist|playlist)
-                           \? .*? (p|a|list)=
+                           (?:course|view_play_list|my_playlists|artist|playlist|watch)
+                           \? (?:.*?&)*? (?:p|a|list)=
                         |  user/.*?/user/
                         |  p/
                         |  user/.*?#[pg]/c/
                         )
-                        (?:PL|EC)?
-                     |PL|EC)
-                     ([0-9A-Za-z-_]{10,})
-                     (?:/.*?/([0-9A-Za-z_-]+))?
-                     .*"""
+                        ((?: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'
@@ -1686,9 +1689,10 @@ class YoutubePlaylistIE(InfoExtractor):
     def __init__(self, downloader=None):
         InfoExtractor.__init__(self, 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, re.VERBOSE) is not None
+        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."""
@@ -1701,13 +1705,8 @@ class YoutubePlaylistIE(InfoExtractor):
             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 videos from API
-        playlist_id = mobj.group(2)
+        playlist_id = mobj.group(1) or mobj.group(2)
         page_num = 1
         videos = []
 
@@ -1727,7 +1726,12 @@ class YoutubePlaylistIE(InfoExtractor):
                 self._downloader.trouble(u'ERROR: Invalid JSON in API response: ' + compat_str(err))
                 return
 
-            videos += [(entry['yt$position']['$t'], entry['content']['src']) for entry in response['feed']['entry']]
+            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
@@ -2313,9 +2317,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)
@@ -3628,9 +3633,10 @@ class SteamIE(InfoExtractor):
                 (?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)
@@ -4004,9 +4010,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)