aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xdevscripts/release.sh2
-rw-r--r--test/test_all_urls.py14
-rw-r--r--test/test_youtube_lists.py58
-rw-r--r--test/tests.json30
-rw-r--r--youtube_dl/FileDownloader.py31
-rwxr-xr-xyoutube_dl/InfoExtractors.py326
-rw-r--r--youtube_dl/__init__.py16
-rw-r--r--youtube_dl/update.py16
-rw-r--r--youtube_dl/version.py2
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=(.+?)&amp;([^&"]+&amp;)*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'