aboutsummaryrefslogtreecommitdiff
path: root/youtube_dl/extractor
diff options
context:
space:
mode:
Diffstat (limited to 'youtube_dl/extractor')
-rw-r--r--youtube_dl/extractor/__init__.py19
-rw-r--r--youtube_dl/extractor/archiveorg.py7
-rw-r--r--youtube_dl/extractor/bloomberg.py27
-rw-r--r--youtube_dl/extractor/brightcove.py64
-rw-r--r--youtube_dl/extractor/dailymotion.py47
-rw-r--r--youtube_dl/extractor/dreisat.py6
-rw-r--r--youtube_dl/extractor/ebaumsworld.py37
-rw-r--r--youtube_dl/extractor/facebook.py4
-rw-r--r--youtube_dl/extractor/fktv.py79
-rw-r--r--youtube_dl/extractor/francetv.py62
-rw-r--r--youtube_dl/extractor/funnyordie.py3
-rw-r--r--youtube_dl/extractor/generic.py11
-rw-r--r--youtube_dl/extractor/googleplus.py3
-rw-r--r--youtube_dl/extractor/hotnewhiphop.py4
-rw-r--r--youtube_dl/extractor/livestream.py14
-rw-r--r--youtube_dl/extractor/mixcloud.py122
-rw-r--r--youtube_dl/extractor/newgrounds.py38
-rw-r--r--youtube_dl/extractor/ooyala.py10
-rw-r--r--youtube_dl/extractor/soundcloud.py45
-rw-r--r--youtube_dl/extractor/southparkstudios.py38
-rw-r--r--youtube_dl/extractor/subtitles.py5
-rw-r--r--youtube_dl/extractor/trilulilu.py4
-rw-r--r--youtube_dl/extractor/vice.py38
-rw-r--r--youtube_dl/extractor/xhamster.py19
-rw-r--r--youtube_dl/extractor/youku.py7
-rw-r--r--youtube_dl/extractor/youtube.py737
26 files changed, 1259 insertions, 191 deletions
diff --git a/youtube_dl/extractor/__init__.py b/youtube_dl/extractor/__init__.py
index 06f9542d2..d1b7e5f99 100644
--- a/youtube_dl/extractor/__init__.py
+++ b/youtube_dl/extractor/__init__.py
@@ -6,6 +6,7 @@ from .arte import ArteTvIE
from .auengine import AUEngineIE
from .bandcamp import BandcampIE
from .bliptv import BlipTVIE, BlipTVUserIE
+from .bloomberg import BloombergIE
from .breakcom import BreakIE
from .brightcove import BrightcoveIE
from .c56 import C56IE
@@ -17,21 +18,32 @@ from .comedycentral import ComedyCentralIE
from .condenast import CondeNastIE
from .criterion import CriterionIE
from .cspan import CSpanIE
-from .dailymotion import DailymotionIE, DailymotionPlaylistIE
+from .dailymotion import (
+ DailymotionIE,
+ DailymotionPlaylistIE,
+ DailymotionUserIE,
+)
from .daum import DaumIE
from .depositfiles import DepositFilesIE
from .dotsub import DotsubIE
from .dreisat import DreiSatIE
from .defense import DefenseGouvFrIE
+from .ebaumsworld import EbaumsWorldIE
from .ehow import EHowIE
from .eighttracks import EightTracksIE
from .escapist import EscapistIE
from .exfm import ExfmIE
from .facebook import FacebookIE
+from .fktv import (
+ FKTVIE,
+ FKTVPosteckeIE,
+)
from .flickr import FlickrIE
from .francetv import (
PluzzIE,
FranceTvInfoIE,
+ France2IE,
+ GenerationQuoiIE
)
from .freesound import FreesoundIE
from .funnyordie import FunnyOrDieIE
@@ -67,6 +79,7 @@ from .myvideo import MyVideoIE
from .naver import NaverIE
from .nba import NBAIE
from .nbc import NBCNewsIE
+from .newgrounds import NewgroundsIE
from .ooyala import OoyalaIE
from .orf import ORFIE
from .pbs import PBSIE
@@ -82,7 +95,8 @@ from .sina import SinaIE
from .slashdot import SlashdotIE
from .slideshare import SlideshareIE
from .sohu import SohuIE
-from .soundcloud import SoundcloudIE, SoundcloudSetIE
+from .soundcloud import SoundcloudIE, SoundcloudSetIE, SoundcloudUserIE
+from .southparkstudios import SouthParkStudiosIE
from .spiegel import SpiegelIE
from .stanfordoc import StanfordOpenClassroomIE
from .statigram import StatigramIE
@@ -102,6 +116,7 @@ from .vbox7 import Vbox7IE
from .veehd import VeeHDIE
from .veoh import VeohIE
from .vevo import VevoIE
+from .vice import ViceIE
from .videofyme import VideofyMeIE
from .vimeo import VimeoIE, VimeoChannelIE
from .vine import VineIE
diff --git a/youtube_dl/extractor/archiveorg.py b/youtube_dl/extractor/archiveorg.py
index 7efd1d823..61ce4469a 100644
--- a/youtube_dl/extractor/archiveorg.py
+++ b/youtube_dl/extractor/archiveorg.py
@@ -46,6 +46,8 @@ class ArchiveOrgIE(InfoExtractor):
for fn,fdata in data['files'].items()
if 'Video' in fdata['format']]
formats.sort(key=lambda fdata: fdata['file_size'])
+ for f in formats:
+ f['ext'] = determine_ext(f['url'])
info = {
'_type': 'video',
@@ -61,7 +63,6 @@ class ArchiveOrgIE(InfoExtractor):
info['thumbnail'] = thumbnail
# TODO: Remove when #980 has been merged
- info['url'] = formats[-1]['url']
- info['ext'] = determine_ext(formats[-1]['url'])
+ info.update(formats[-1])
- return info \ No newline at end of file
+ return info
diff --git a/youtube_dl/extractor/bloomberg.py b/youtube_dl/extractor/bloomberg.py
new file mode 100644
index 000000000..3666a780b
--- /dev/null
+++ b/youtube_dl/extractor/bloomberg.py
@@ -0,0 +1,27 @@
+import re
+
+from .common import InfoExtractor
+
+
+class BloombergIE(InfoExtractor):
+ _VALID_URL = r'https?://www\.bloomberg\.com/video/(?P<name>.+?).html'
+
+ _TEST = {
+ u'url': u'http://www.bloomberg.com/video/shah-s-presentation-on-foreign-exchange-strategies-qurhIVlJSB6hzkVi229d8g.html',
+ u'file': u'12bzhqZTqQHmmlA8I-i0NpzJgcG5NNYX.mp4',
+ u'info_dict': {
+ u'title': u'Shah\'s Presentation on Foreign-Exchange Strategies',
+ u'description': u'md5:abc86e5236f9f0e4866c59ad36736686',
+ },
+ u'params': {
+ # Requires ffmpeg (m3u8 manifest)
+ u'skip_download': True,
+ },
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ name = mobj.group('name')
+ webpage = self._download_webpage(url, name)
+ ooyala_url = self._og_search_video_url(webpage)
+ return self.url_result(ooyala_url, ie='Ooyala')
diff --git a/youtube_dl/extractor/brightcove.py b/youtube_dl/extractor/brightcove.py
index 71e3c7883..558b3d009 100644
--- a/youtube_dl/extractor/brightcove.py
+++ b/youtube_dl/extractor/brightcove.py
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
import re
import json
import xml.etree.ElementTree
@@ -7,15 +9,39 @@ from ..utils import (
compat_urllib_parse,
find_xpath_attr,
compat_urlparse,
+
+ ExtractorError,
)
class BrightcoveIE(InfoExtractor):
_VALID_URL = r'https?://.*brightcove\.com/(services|viewer).*\?(?P<query>.*)'
_FEDERATED_URL_TEMPLATE = 'http://c.brightcove.com/services/viewer/htmlFederated?%s'
_PLAYLIST_URL_TEMPLATE = 'http://c.brightcove.com/services/json/experience/runtime/?command=get_programming_for_experience&playerKey=%s'
-
- # There is a test for Brigtcove in GenericIE, that way we test both the download
- # and the detection of videos, and we don't have to find an URL that is always valid
+
+ _TESTS = [
+ {
+ # From http://www.8tv.cat/8aldia/videos/xavier-sala-i-martin-aquesta-tarda-a-8-al-dia/
+ u'url': u'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1654948606001&flashID=myExperience&%40videoPlayer=2371591881001',
+ u'file': u'2371591881001.mp4',
+ u'md5': u'9e80619e0a94663f0bdc849b4566af19',
+ u'note': u'Test Brightcove downloads and detection in GenericIE',
+ u'info_dict': {
+ u'title': u'Xavier Sala i Martín: “Un banc que no presta és un banc zombi que no serveix per a res”',
+ u'uploader': u'8TV',
+ u'description': u'md5:a950cc4285c43e44d763d036710cd9cd',
+ }
+ },
+ {
+ # From http://medianetwork.oracle.com/video/player/1785452137001
+ u'url': u'http://c.brightcove.com/services/viewer/htmlFederated?playerID=1217746023001&flashID=myPlayer&%40videoPlayer=1785452137001',
+ u'file': u'1785452137001.flv',
+ u'info_dict': {
+ u'title': u'JVMLS 2012: Arrays 2.0 - Opportunities and Challenges',
+ u'description': u'John Rose speaks at the JVM Language Summit, August 1, 2012.',
+ u'uploader': u'Oracle',
+ },
+ },
+ ]
@classmethod
def _build_brighcove_url(cls, object_str):
@@ -72,15 +98,27 @@ class BrightcoveIE(InfoExtractor):
playlist_title=playlist_info['mediaCollectionDTO']['displayName'])
def _extract_video_info(self, video_info):
- renditions = video_info['renditions']
- renditions = sorted(renditions, key=lambda r: r['size'])
- best_format = renditions[-1]
+ info = {
+ 'id': video_info['id'],
+ 'title': video_info['displayName'],
+ 'description': video_info.get('shortDescription'),
+ 'thumbnail': video_info.get('videoStillURL') or video_info.get('thumbnailURL'),
+ 'uploader': video_info.get('publisherName'),
+ }
- return {'id': video_info['id'],
- 'title': video_info['displayName'],
- 'url': best_format['defaultURL'],
+ renditions = video_info.get('renditions')
+ if renditions:
+ renditions = sorted(renditions, key=lambda r: r['size'])
+ best_format = renditions[-1]
+ info.update({
+ 'url': best_format['defaultURL'],
'ext': 'mp4',
- 'description': video_info.get('shortDescription'),
- 'thumbnail': video_info.get('videoStillURL') or video_info.get('thumbnailURL'),
- 'uploader': video_info.get('publisherName'),
- }
+ })
+ elif video_info.get('FLVFullLengthURL') is not None:
+ info.update({
+ 'url': video_info['FLVFullLengthURL'],
+ 'ext': 'flv',
+ })
+ else:
+ raise ExtractorError(u'Unable to extract video url for %s' % info['id'])
+ return info
diff --git a/youtube_dl/extractor/dailymotion.py b/youtube_dl/extractor/dailymotion.py
index 360113f9c..64b89aae8 100644
--- a/youtube_dl/extractor/dailymotion.py
+++ b/youtube_dl/extractor/dailymotion.py
@@ -63,6 +63,9 @@ class DailymotionIE(SubtitlesInfoExtractor):
info = self._search_regex(r'var info = ({.*?}),$', embed_page,
'video info', flags=re.MULTILINE)
info = json.loads(info)
+ if info.get('error') is not None:
+ msg = 'Couldn\'t get video, Dailymotion says: %s' % info['error']['title']
+ raise ExtractorError(msg, expected=True)
# TODO: support choosing qualities
@@ -111,28 +114,54 @@ class DailymotionIE(SubtitlesInfoExtractor):
class DailymotionPlaylistIE(InfoExtractor):
+ IE_NAME = u'dailymotion:playlist'
_VALID_URL = r'(?:https?://)?(?:www\.)?dailymotion\.[a-z]{2,3}/playlist/(?P<id>.+?)/'
_MORE_PAGES_INDICATOR = r'<div class="next">.*?<a.*?href="/playlist/.+?".*?>.*?</a>.*?</div>'
+ _PAGE_TEMPLATE = 'https://www.dailymotion.com/playlist/%s/%s'
- def _real_extract(self, url):
- mobj = re.match(self._VALID_URL, url)
- playlist_id = mobj.group('id')
+ def _extract_entries(self, id):
video_ids = []
-
for pagenum in itertools.count(1):
- webpage = self._download_webpage('https://www.dailymotion.com/playlist/%s/%s' % (playlist_id, pagenum),
- playlist_id, u'Downloading page %s' % pagenum)
+ webpage = self._download_webpage(self._PAGE_TEMPLATE % (id, pagenum),
+ id, u'Downloading page %s' % pagenum)
playlist_el = get_element_by_attribute(u'class', u'video_list', webpage)
video_ids.extend(re.findall(r'data-id="(.+?)" data-ext-id', playlist_el))
if re.search(self._MORE_PAGES_INDICATOR, webpage, re.DOTALL) is None:
break
-
- entries = [self.url_result('http://www.dailymotion.com/video/%s' % video_id, 'Dailymotion')
+ return [self.url_result('http://www.dailymotion.com/video/%s' % video_id, 'Dailymotion')
for video_id in video_ids]
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ playlist_id = mobj.group('id')
+ webpage = self._download_webpage(url, playlist_id)
+
return {'_type': 'playlist',
'id': playlist_id,
'title': get_element_by_id(u'playlist_name', webpage),
- 'entries': entries,
+ 'entries': self._extract_entries(playlist_id),
}
+
+
+class DailymotionUserIE(DailymotionPlaylistIE):
+ IE_NAME = u'dailymotion:user'
+ _VALID_URL = r'(?:https?://)?(?:www\.)?dailymotion\.[a-z]{2,3}/user/(?P<user>[^/]+)'
+ _MORE_PAGES_INDICATOR = r'<div class="next">.*?<a.*?href="/user/.+?".*?>.*?</a>.*?</div>'
+ _PAGE_TEMPLATE = 'http://www.dailymotion.com/user/%s/%s'
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ user = mobj.group('user')
+ webpage = self._download_webpage(url, user)
+ full_user = self._html_search_regex(
+ r'<a class="label" href="/%s".*?>(.*?)</' % re.escape(user),
+ webpage, u'user', flags=re.DOTALL)
+
+ return {
+ '_type': 'playlist',
+ 'id': user,
+ 'title': full_user,
+ 'entries': self._extract_entries(user),
+ }
diff --git a/youtube_dl/extractor/dreisat.py b/youtube_dl/extractor/dreisat.py
index 64b465805..765cb1f37 100644
--- a/youtube_dl/extractor/dreisat.py
+++ b/youtube_dl/extractor/dreisat.py
@@ -54,6 +54,7 @@ class DreiSatIE(InfoExtractor):
'width': int(fe.find('./width').text),
'height': int(fe.find('./height').text),
'url': fe.find('./url').text,
+ 'ext': determine_ext(fe.find('./url').text),
'filesize': int(fe.find('./filesize').text),
'video_bitrate': int(fe.find('./videoBitrate').text),
'3sat_qualityname': fe.find('./quality').text,
@@ -79,7 +80,6 @@ class DreiSatIE(InfoExtractor):
}
# TODO: Remove when #980 has been merged
- info['url'] = formats[-1]['url']
- info['ext'] = determine_ext(formats[-1]['url'])
+ info.update(formats[-1])
- return info \ No newline at end of file
+ return info
diff --git a/youtube_dl/extractor/ebaumsworld.py b/youtube_dl/extractor/ebaumsworld.py
new file mode 100644
index 000000000..f02c6998b
--- /dev/null
+++ b/youtube_dl/extractor/ebaumsworld.py
@@ -0,0 +1,37 @@
+import re
+import xml.etree.ElementTree
+
+from .common import InfoExtractor
+from ..utils import determine_ext
+
+
+class EbaumsWorldIE(InfoExtractor):
+ _VALID_URL = r'https?://www\.ebaumsworld\.com/video/watch/(?P<id>\d+)'
+
+ _TEST = {
+ u'url': u'http://www.ebaumsworld.com/video/watch/83367677/',
+ u'file': u'83367677.mp4',
+ u'info_dict': {
+ u'title': u'A Giant Python Opens The Door',
+ u'description': u'This is how nightmares start...',
+ u'uploader': u'jihadpizza',
+ },
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ video_id = mobj.group('id')
+ config_xml = self._download_webpage(
+ 'http://www.ebaumsworld.com/video/player/%s' % video_id, video_id)
+ config = xml.etree.ElementTree.fromstring(config_xml.encode('utf-8'))
+ video_url = config.find('file').text
+
+ return {
+ 'id': video_id,
+ 'title': config.find('title').text,
+ 'url': video_url,
+ 'ext': determine_ext(video_url),
+ 'description': config.find('description').text,
+ 'thumbnail': config.find('image').text,
+ 'uploader': config.find('username').text,
+ }
diff --git a/youtube_dl/extractor/facebook.py b/youtube_dl/extractor/facebook.py
index beaa5b4bd..9d1bc0751 100644
--- a/youtube_dl/extractor/facebook.py
+++ b/youtube_dl/extractor/facebook.py
@@ -106,8 +106,8 @@ class FacebookIE(InfoExtractor):
video_duration = int(video_data['video_duration'])
thumbnail = video_data['thumbnail_src']
- video_title = self._html_search_regex('<h2 class="uiHeaderTitle">([^<]+)</h2>',
- webpage, u'title')
+ video_title = self._html_search_regex(
+ r'<h2 class="uiHeaderTitle">([^<]*)</h2>', webpage, u'title')
info = {
'id': video_id,
diff --git a/youtube_dl/extractor/fktv.py b/youtube_dl/extractor/fktv.py
new file mode 100644
index 000000000..9c89362ef
--- /dev/null
+++ b/youtube_dl/extractor/fktv.py
@@ -0,0 +1,79 @@
+import re
+import random
+import json
+
+from .common import InfoExtractor
+from ..utils import (
+ determine_ext,
+ get_element_by_id,
+ clean_html,
+)
+
+
+class FKTVIE(InfoExtractor):
+ IE_NAME = u'fernsehkritik.tv'
+ _VALID_URL = r'(?:http://)?(?:www\.)?fernsehkritik.tv/folge-(?P<ep>[0-9]+)(?:/.*)?'
+
+ _TEST = {
+ u'url': u'http://fernsehkritik.tv/folge-1',
+ u'file': u'00011.flv',
+ u'info_dict': {
+ u'title': u'Folge 1 vom 10. April 2007',
+ u'description': u'md5:fb4818139c7cfe6907d4b83412a6864f',
+ },
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ episode = int(mobj.group('ep'))
+
+ server = random.randint(2, 4)
+ video_thumbnail = 'http://fernsehkritik.tv/images/magazin/folge%d.jpg' % episode
+ start_webpage = self._download_webpage('http://fernsehkritik.tv/folge-%d/Start' % episode,
+ episode)
+ playlist = self._search_regex(r'playlist = (\[.*?\]);', start_webpage,
+ u'playlist', flags=re.DOTALL)
+ files = json.loads(re.sub('{[^{}]*?}', '{}', playlist))
+ # TODO: return a single multipart video
+ videos = []
+ for i, _ in enumerate(files, 1):
+ video_id = '%04d%d' % (episode, i)
+ video_url = 'http://dl%d.fernsehkritik.tv/fernsehkritik%d%s.flv' % (server, episode, '' if i == 1 else '-%d' % i)
+ video_title = 'Fernsehkritik %d.%d' % (episode, i)
+ videos.append({
+ 'id': video_id,
+ 'url': video_url,
+ 'ext': determine_ext(video_url),
+ 'title': clean_html(get_element_by_id('eptitle', start_webpage)),
+ 'description': clean_html(get_element_by_id('contentlist', start_webpage)),
+ 'thumbnail': video_thumbnail
+ })
+ return videos
+
+
+class FKTVPosteckeIE(InfoExtractor):
+ IE_NAME = u'fernsehkritik.tv:postecke'
+ _VALID_URL = r'(?:http://)?(?:www\.)?fernsehkritik.tv/inline-video/postecke.php\?(.*&)?ep=(?P<ep>[0-9]+)(&|$)'
+ _TEST = {
+ u'url': u'http://fernsehkritik.tv/inline-video/postecke.php?iframe=true&width=625&height=440&ep=120',
+ u'file': u'0120.flv',
+ u'md5': u'262f0adbac80317412f7e57b4808e5c4',
+ u'info_dict': {
+ u"title": u"Postecke 120"
+ }
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ episode = int(mobj.group('ep'))
+
+ server = random.randint(2, 4)
+ video_id = '%04d' % episode
+ video_url = 'http://dl%d.fernsehkritik.tv/postecke/postecke%d.flv' % (server, episode)
+ video_title = 'Postecke %d' % episode
+ return {
+ 'id': video_id,
+ 'url': video_url,
+ 'ext': determine_ext(video_url),
+ 'title': video_title,
+ }
diff --git a/youtube_dl/extractor/francetv.py b/youtube_dl/extractor/francetv.py
index f2b12c884..b1530e549 100644
--- a/youtube_dl/extractor/francetv.py
+++ b/youtube_dl/extractor/francetv.py
@@ -1,6 +1,7 @@
# encoding: utf-8
import re
import xml.etree.ElementTree
+import json
from .common import InfoExtractor
from ..utils import (
@@ -34,17 +35,7 @@ class PluzzIE(FranceTVBaseInfoExtractor):
IE_NAME = u'pluzz.francetv.fr'
_VALID_URL = r'https?://pluzz\.francetv\.fr/videos/(.*?)\.html'
- _TEST = {
- u'url': u'http://pluzz.francetv.fr/videos/allo_rufo_saison5_,88439064.html',
- u'file': u'88439064.mp4',
- u'info_dict': {
- u'title': u'Allô Rufo',
- u'description': u'md5:d909f1ebdf963814b65772aea250400e',
- },
- u'params': {
- u'skip_download': True,
- },
- }
+ # Can't use tests, videos expire in 7 days
def _real_extract(self, url):
title = re.match(self._VALID_URL, url).group(1)
@@ -75,3 +66,52 @@ class FranceTvInfoIE(FranceTVBaseInfoExtractor):
webpage = self._download_webpage(url, page_title)
video_id = self._search_regex(r'id-video=(\d+?)"', webpage, u'video id')
return self._extract_video(video_id)
+
+
+class France2IE(FranceTVBaseInfoExtractor):
+ IE_NAME = u'france2.fr'
+ _VALID_URL = r'https?://www\.france2\.fr/emissions/.*?/videos/(?P<id>\d+)'
+
+ _TEST = {
+ u'url': u'http://www.france2.fr/emissions/13h15-le-samedi-le-dimanche/videos/75540104',
+ u'file': u'75540104.mp4',
+ u'info_dict': {
+ u'title': u'13h15, le samedi...',
+ u'description': u'md5:2e5b58ba7a2d3692b35c792be081a03d',
+ },
+ u'params': {
+ u'skip_download': True,
+ },
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ video_id = mobj.group('id')
+ return self._extract_video(video_id)
+
+
+class GenerationQuoiIE(InfoExtractor):
+ IE_NAME = u'http://generation-quoi.france2.fr'
+ _VALID_URL = r'https?://generation-quoi\.france2\.fr/portrait/(?P<name>.*)(\?|$)'
+
+ _TEST = {
+ u'url': u'http://generation-quoi.france2.fr/portrait/garde-a-vous',
+ u'file': u'k7FJX8VBcvvLmX4wA5Q.mp4',
+ u'info_dict': {
+ u'title': u'Génération Quoi - Garde à Vous',
+ u'uploader': u'Génération Quoi',
+ },
+ u'params': {
+ # It uses Dailymotion
+ u'skip_download': True,
+ },
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ name = mobj.group('name')
+ info_url = compat_urlparse.urljoin(url, '/medias/video/%s.json' % name)
+ info_json = self._download_webpage(info_url, name)
+ info = json.loads(info_json)
+ return self.url_result('http://www.dailymotion.com/video/%s' % info['id'],
+ ie='Dailymotion')
diff --git a/youtube_dl/extractor/funnyordie.py b/youtube_dl/extractor/funnyordie.py
index f3d86a711..2ccdb7073 100644
--- a/youtube_dl/extractor/funnyordie.py
+++ b/youtube_dl/extractor/funnyordie.py
@@ -21,7 +21,8 @@ class FunnyOrDieIE(InfoExtractor):
video_id = mobj.group('id')
webpage = self._download_webpage(url, video_id)
- video_url = self._search_regex(r'type="video/mp4" src="(.*?)"',
+ video_url = self._search_regex(
+ [r'type="video/mp4" src="(.*?)"', r'src="([^>]*?)" type=\'video/mp4\''],
webpage, u'video URL', flags=re.DOTALL)
info = {
diff --git a/youtube_dl/extractor/generic.py b/youtube_dl/extractor/generic.py
index f92e61fea..764070635 100644
--- a/youtube_dl/extractor/generic.py
+++ b/youtube_dl/extractor/generic.py
@@ -29,17 +29,6 @@ class GenericIE(InfoExtractor):
u"title": u"R\u00e9gis plante sa Jeep"
}
},
- {
- u'url': u'http://www.8tv.cat/8aldia/videos/xavier-sala-i-martin-aquesta-tarda-a-8-al-dia/',
- u'file': u'2371591881001.mp4',
- u'md5': u'9e80619e0a94663f0bdc849b4566af19',
- u'note': u'Test Brightcove downloads and detection in GenericIE',
- u'info_dict': {
- u'title': u'Xavier Sala i Martín: “Un banc que no presta és un banc zombi que no serveix per a res”',
- u'uploader': u'8TV',
- u'description': u'md5:a950cc4285c43e44d763d036710cd9cd',
- }
- },
]
def report_download_webpage(self, video_id):
diff --git a/youtube_dl/extractor/googleplus.py b/youtube_dl/extractor/googleplus.py
index f1cd88983..8895ad289 100644
--- a/youtube_dl/extractor/googleplus.py
+++ b/youtube_dl/extractor/googleplus.py
@@ -40,7 +40,8 @@ class GooglePlusIE(InfoExtractor):
self.report_extraction(video_id)
# Extract update date
- upload_date = self._html_search_regex('title="Timestamp">(.*?)</a>',
+ upload_date = self._html_search_regex(
+ ['title="Timestamp">(.*?)</a>', r'<a.+?class="g-M.+?>(.+?)</a>'],
webpage, u'upload date', fatal=False)
if upload_date:
# Convert timestring to a format suitable for filename
diff --git a/youtube_dl/extractor/hotnewhiphop.py b/youtube_dl/extractor/hotnewhiphop.py
index ccca1d7e0..3798118a7 100644
--- a/youtube_dl/extractor/hotnewhiphop.py
+++ b/youtube_dl/extractor/hotnewhiphop.py
@@ -7,11 +7,11 @@ from .common import InfoExtractor
class HotNewHipHopIE(InfoExtractor):
_VALID_URL = r'http://www\.hotnewhiphop.com/.*\.(?P<id>.*)\.html'
_TEST = {
- u'url': u"http://www.hotnewhiphop.com/freddie-gibbs-lay-it-down-song.1435540.html'",
+ u'url': u"http://www.hotnewhiphop.com/freddie-gibbs-lay-it-down-song.1435540.html",
u'file': u'1435540.mp3',
u'md5': u'2c2cd2f76ef11a9b3b581e8b232f3d96',
u'info_dict': {
- u"title": u"Freddie Gibbs Songs - Lay It Down"
+ u"title": u"Freddie Gibbs - Lay It Down"
}
}
diff --git a/youtube_dl/extractor/livestream.py b/youtube_dl/extractor/livestream.py
index 309921078..d04da98c8 100644
--- a/youtube_dl/extractor/livestream.py
+++ b/youtube_dl/extractor/livestream.py
@@ -2,7 +2,12 @@ import re
import json
from .common import InfoExtractor
-from ..utils import compat_urllib_parse_urlparse, compat_urlparse
+from ..utils import (
+ compat_urllib_parse_urlparse,
+ compat_urlparse,
+ get_meta_content,
+ ExtractorError,
+)
class LivestreamIE(InfoExtractor):
@@ -35,8 +40,11 @@ class LivestreamIE(InfoExtractor):
if video_id is None:
# This is an event page:
- api_url = self._search_regex(r'event_design_eventId: \'(.+?)\'',
- webpage, 'api url')
+ player = get_meta_content('twitter:player', webpage)
+ if player is None:
+ raise ExtractorError('Couldn\'t extract event api url')
+ api_url = player.replace('/player', '')
+ api_url = re.sub(r'^(https?://)(new\.)', r'\1api.\2', api_url)
info = json.loads(self._download_webpage(api_url, event_name,
u'Downloading event info'))
videos = [self._extract_video_info(video_data['data'])
diff --git a/youtube_dl/extractor/mixcloud.py b/youtube_dl/extractor/mixcloud.py
index 8245b5583..a200dcd74 100644
--- a/youtube_dl/extractor/mixcloud.py
+++ b/youtube_dl/extractor/mixcloud.py
@@ -5,34 +5,27 @@ import socket
from .common import InfoExtractor
from ..utils import (
compat_http_client,
- compat_str,
compat_urllib_error,
compat_urllib_request,
-
- ExtractorError,
+ unified_strdate,
)
class MixcloudIE(InfoExtractor):
- _WORKING = False # New API, but it seems good http://www.mixcloud.com/developers/documentation/
_VALID_URL = r'^(?:https?://)?(?:www\.)?mixcloud\.com/([\w\d-]+)/([\w\d-]+)'
IE_NAME = u'mixcloud'
- def report_download_json(self, file_id):
- """Report JSON download."""
- self.to_screen(u'Downloading json')
-
- def get_urls(self, jsonData, fmt, bitrate='best'):
- """Get urls from 'audio_formats' section in json"""
- try:
- bitrate_list = jsonData[fmt]
- if bitrate is None or bitrate == 'best' or bitrate not in bitrate_list:
- bitrate = max(bitrate_list) # select highest
-
- url_list = jsonData[fmt][bitrate]
- except TypeError: # we have no bitrate info.
- url_list = jsonData[fmt]
- return url_list
+ _TEST = {
+ u'url': u'http://www.mixcloud.com/dholbach/cryptkeeper/',
+ u'file': u'dholbach-cryptkeeper.mp3',
+ u'info_dict': {
+ u'title': u'Cryptkeeper',
+ u'description': u'After quite a long silence from myself, finally another Drum\'n\'Bass mix with my favourite current dance floor bangers.',
+ u'uploader': u'Daniel Holbach',
+ u'uploader_id': u'dholbach',
+ u'upload_date': u'20111115',
+ },
+ }
def check_urls(self, url_list):
"""Returns 1st active url from list"""
@@ -45,71 +38,32 @@ class MixcloudIE(InfoExtractor):
return None
- def _print_formats(self, formats):
- print('Available formats:')
- for fmt in formats.keys():
- for b in formats[fmt]:
- try:
- ext = formats[fmt][b][0]
- print('%s\t%s\t[%s]' % (fmt, b, ext.split('.')[-1]))
- except TypeError: # we have no bitrate info
- ext = formats[fmt][0]
- print('%s\t%s\t[%s]' % (fmt, '??', ext.split('.')[-1]))
- break
-
def _real_extract(self, url):
mobj = re.match(self._VALID_URL, url)
- if mobj is None:
- raise ExtractorError(u'Invalid URL: %s' % url)
- # extract uploader & filename from url
- uploader = mobj.group(1).decode('utf-8')
- file_id = uploader + "-" + mobj.group(2).decode('utf-8')
-
- # construct API request
- file_url = 'http://www.mixcloud.com/api/1/cloudcast/' + '/'.join(url.split('/')[-3:-1]) + '.json'
- # retrieve .json file with links to files
- request = compat_urllib_request.Request(file_url)
- try:
- self.report_download_json(file_url)
- jsonData = compat_urllib_request.urlopen(request).read()
- except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
- raise ExtractorError(u'Unable to retrieve file: %s' % compat_str(err))
-
- # parse JSON
- json_data = json.loads(jsonData)
- player_url = json_data['player_swf_url']
- formats = dict(json_data['audio_formats'])
-
- req_format = self._downloader.params.get('format', None)
-
- if self._downloader.params.get('listformats', None):
- self._print_formats(formats)
- return
-
- if req_format is None or req_format == 'best':
- for format_param in formats.keys():
- url_list = self.get_urls(formats, format_param)
- # check urls
- file_url = self.check_urls(url_list)
- if file_url is not None:
- break # got it!
- else:
- if req_format not in formats:
- raise ExtractorError(u'Format is not available')
-
- url_list = self.get_urls(formats, req_format)
- file_url = self.check_urls(url_list)
- format_param = req_format
- return [{
- 'id': file_id.decode('utf-8'),
- 'url': file_url.decode('utf-8'),
- 'uploader': uploader.decode('utf-8'),
- 'upload_date': None,
- 'title': json_data['name'],
- 'ext': file_url.split('.')[-1].decode('utf-8'),
- 'format': (format_param is None and u'NA' or format_param.decode('utf-8')),
- 'thumbnail': json_data['thumbnail_url'],
- 'description': json_data['description'],
- 'player_url': player_url.decode('utf-8'),
- }]
+ uploader = mobj.group(1)
+ cloudcast_name = mobj.group(2)
+ track_id = '-'.join((uploader, cloudcast_name))
+ api_url = 'http://api.mixcloud.com/%s/%s/' % (uploader, cloudcast_name)
+ webpage = self._download_webpage(url, track_id)
+ json_data = self._download_webpage(api_url, track_id,
+ u'Downloading cloudcast info')
+ info = json.loads(json_data)
+
+ preview_url = self._search_regex(r'data-preview-url="(.+?)"', webpage, u'preview url')
+ song_url = preview_url.replace('/previews/', '/cloudcasts/originals/')
+ template_url = re.sub(r'(stream\d*)', 'stream%d', song_url)
+ final_song_url = self.check_urls(template_url % i for i in range(30))
+
+ return {
+ 'id': track_id,
+ 'title': info['name'],
+ 'url': final_song_url,
+ 'ext': 'mp3',
+ 'description': info['description'],
+ 'thumbnail': info['pictures'].get('extra_large'),
+ 'uploader': info['user']['name'],
+ 'uploader_id': info['user']['username'],
+ 'upload_date': unified_strdate(info['created_time']),
+ 'view_count': info['play_count'],
+ }
diff --git a/youtube_dl/extractor/newgrounds.py b/youtube_dl/extractor/newgrounds.py
new file mode 100644
index 000000000..2ef80bce0
--- /dev/null
+++ b/youtube_dl/extractor/newgrounds.py
@@ -0,0 +1,38 @@
+import json
+import re
+
+from .common import InfoExtractor
+from ..utils import determine_ext
+
+
+class NewgroundsIE(InfoExtractor):
+ _VALID_URL = r'(?:https?://)?(?:www\.)?newgrounds\.com/audio/listen/(?P<id>\d+)'
+ _TEST = {
+ u'url': u'http://www.newgrounds.com/audio/listen/549479',
+ u'file': u'549479.mp3',
+ u'md5': u'fe6033d297591288fa1c1f780386f07a',
+ u'info_dict': {
+ u"title": u"B7 - BusMode",
+ u"uploader": u"Burn7",
+ }
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ music_id = mobj.group('id')
+ webpage = self._download_webpage(url, music_id)
+
+ title = self._html_search_regex(r',"name":"([^"]+)",', webpage, u'music title')
+ uploader = self._html_search_regex(r',"artist":"([^"]+)",', webpage, u'music uploader')
+
+ music_url_json_string = self._html_search_regex(r'({"url":"[^"]+"),', webpage, u'music url') + '}'
+ music_url_json = json.loads(music_url_json_string)
+ music_url = music_url_json['url']
+
+ return {
+ 'id': music_id,
+ 'title': title,
+ 'url': music_url,
+ 'uploader': uploader,
+ 'ext': determine_ext(music_url),
+ }
diff --git a/youtube_dl/extractor/ooyala.py b/youtube_dl/extractor/ooyala.py
index b734722d0..1f7b4d2e7 100644
--- a/youtube_dl/extractor/ooyala.py
+++ b/youtube_dl/extractor/ooyala.py
@@ -18,11 +18,15 @@ class OoyalaIE(InfoExtractor):
},
}
+ @staticmethod
+ def _url_for_embed_code(embed_code):
+ return 'http://player.ooyala.com/player.js?embedCode=%s' % embed_code
+
def _extract_result(self, info, more_info):
return {'id': info['embedCode'],
'ext': 'mp4',
'title': unescapeHTML(info['title']),
- 'url': info['url'],
+ 'url': info.get('ipad_url') or info['url'],
'description': unescapeHTML(more_info['description']),
'thumbnail': more_info['promo'],
}
@@ -35,7 +39,9 @@ class OoyalaIE(InfoExtractor):
mobile_url = self._search_regex(r'mobile_player_url="(.+?)&device="',
player, u'mobile player url')
mobile_player = self._download_webpage(mobile_url, embedCode)
- videos_info = self._search_regex(r'eval\("\((\[{.*?stream_redirect.*?}\])\)"\);', mobile_player, u'info').replace('\\"','"')
+ videos_info = self._search_regex(
+ r'var streams=window.oo_testEnv\?\[\]:eval\("\((\[{.*?}\])\)"\);',
+ mobile_player, u'info').replace('\\"','"')
videos_more_info = self._search_regex(r'eval\("\(({.*?\\"promo\\".*?})\)"', mobile_player, u'more info').replace('\\"','"')
videos_info = json.loads(videos_info)
videos_more_info =json.loads(videos_more_info)
diff --git a/youtube_dl/extractor/soundcloud.py b/youtube_dl/extractor/soundcloud.py
index 5f3a5540d..29cd5617c 100644
--- a/youtube_dl/extractor/soundcloud.py
+++ b/youtube_dl/extractor/soundcloud.py
@@ -1,10 +1,12 @@
import json
import re
+import itertools
from .common import InfoExtractor
from ..utils import (
compat_str,
compat_urlparse,
+ compat_urllib_parse,
ExtractorError,
unified_strdate,
@@ -53,10 +55,11 @@ class SoundcloudIE(InfoExtractor):
def _resolv_url(cls, url):
return 'http://api.soundcloud.com/resolve.json?url=' + url + '&client_id=' + cls._CLIENT_ID
- def _extract_info_dict(self, info, full_title=None):
+ def _extract_info_dict(self, info, full_title=None, quiet=False):
video_id = info['id']
name = full_title or video_id
- self.report_extraction(name)
+ if quiet == False:
+ self.report_extraction(name)
thumbnail = info['artwork_url']
if thumbnail is not None:
@@ -198,3 +201,41 @@ class SoundcloudSetIE(SoundcloudIE):
'id': info['id'],
'title': info['title'],
}
+
+
+class SoundcloudUserIE(SoundcloudIE):
+ _VALID_URL = r'https?://(www\.)?soundcloud.com/(?P<user>[^/]+)(/?(tracks/)?)?(\?.*)?$'
+ IE_NAME = u'soundcloud:user'
+
+ # it's in tests/test_playlists.py
+ _TEST = None
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ uploader = mobj.group('user')
+
+ url = 'http://soundcloud.com/%s/' % uploader
+ resolv_url = self._resolv_url(url)
+ user_json = self._download_webpage(resolv_url, uploader,
+ u'Downloading user info')
+ user = json.loads(user_json)
+
+ tracks = []
+ for i in itertools.count():
+ data = compat_urllib_parse.urlencode({'offset': i*50,
+ 'client_id': self._CLIENT_ID,
+ })
+ tracks_url = 'http://api.soundcloud.com/users/%s/tracks.json?' % user['id'] + data
+ response = self._download_webpage(tracks_url, uploader,
+ u'Downloading tracks page %s' % (i+1))
+ new_tracks = json.loads(response)
+ tracks.extend(self._extract_info_dict(track, quiet=True) for track in new_tracks)
+ if len(new_tracks) < 50:
+ break
+
+ return {
+ '_type': 'playlist',
+ 'id': compat_str(user['id']),
+ 'title': user['username'],
+ 'entries': tracks,
+ }
diff --git a/youtube_dl/extractor/southparkstudios.py b/youtube_dl/extractor/southparkstudios.py
new file mode 100644
index 000000000..b1e96b679
--- /dev/null
+++ b/youtube_dl/extractor/southparkstudios.py
@@ -0,0 +1,38 @@
+import re
+
+from .mtv import MTVIE, _media_xml_tag
+
+
+class SouthParkStudiosIE(MTVIE):
+ IE_NAME = u'southparkstudios.com'
+ _VALID_URL = r'https?://www\.southparkstudios\.com/(clips|full-episodes)/(?P<id>.+?)(\?|#|$)'
+
+ _FEED_URL = 'http://www.southparkstudios.com/feeds/video-player/mrss'
+
+ _TEST = {
+ u'url': u'http://www.southparkstudios.com/clips/104437/bat-daded#tab=featured',
+ u'file': u'a7bff6c2-ed00-11e0-aca6-0026b9414f30.mp4',
+ u'info_dict': {
+ u'title': u'Bat Daded',
+ u'description': u'Randy disqualifies South Park by getting into a fight with Bat Dad.',
+ },
+ }
+
+ # Overwrite MTVIE properties we don't want
+ _TESTS = []
+
+ def _get_thumbnail_url(self, uri, itemdoc):
+ search_path = '%s/%s' % (_media_xml_tag('group'), _media_xml_tag('thumbnail'))
+ thumb_node = itemdoc.find(search_path)
+ if thumb_node is None:
+ return None
+ else:
+ return thumb_node.attrib['url']
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ video_id = mobj.group('id')
+ webpage = self._download_webpage(url, video_id)
+ mgid = self._search_regex(r'swfobject.embedSWF\(".*?(mgid:.*?)"',
+ webpage, u'mgid')
+ return self._get_videos_info(mgid)
diff --git a/youtube_dl/extractor/subtitles.py b/youtube_dl/extractor/subtitles.py
index 97215f289..90de7de3a 100644
--- a/youtube_dl/extractor/subtitles.py
+++ b/youtube_dl/extractor/subtitles.py
@@ -10,8 +10,7 @@ class SubtitlesInfoExtractor(InfoExtractor):
@property
def _have_to_download_any_subtitles(self):
return any([self._downloader.params.get('writesubtitles', False),
- self._downloader.params.get('writeautomaticsub'),
- self._downloader.params.get('allsubtitles', False)])
+ self._downloader.params.get('writeautomaticsub')])
def _list_available_subtitles(self, video_id, webpage=None):
""" outputs the available subtitles for the video """
@@ -34,7 +33,7 @@ class SubtitlesInfoExtractor(InfoExtractor):
available_subs_list = {}
if self._downloader.params.get('writeautomaticsub', False):
available_subs_list.update(self._get_available_automatic_caption(video_id, video_webpage))
- if self._downloader.params.get('writesubtitles', False) or self._downloader.params.get('allsubtitles', False):
+ if self._downloader.params.get('writesubtitles', False):
available_subs_list.update(self._get_available_subtitles(video_id))
if not available_subs_list: # error, it didn't get the available subtitles
diff --git a/youtube_dl/extractor/trilulilu.py b/youtube_dl/extractor/trilulilu.py
index f278951ba..0bf028f61 100644
--- a/youtube_dl/extractor/trilulilu.py
+++ b/youtube_dl/extractor/trilulilu.py
@@ -52,6 +52,7 @@ class TriluliluIE(InfoExtractor):
{
'format': fnode.text,
'url': video_url_template % fnode.text,
+ 'ext': fnode.text.partition('-')[0]
}
for fnode in format_doc.findall('./formats/format')
@@ -67,7 +68,6 @@ class TriluliluIE(InfoExtractor):
}
# TODO: Remove when #980 has been merged
- info['url'] = formats[-1]['url']
- info['ext'] = formats[-1]['format'].partition('-')[0]
+ info.update(formats[-1])
return info
diff --git a/youtube_dl/extractor/vice.py b/youtube_dl/extractor/vice.py
new file mode 100644
index 000000000..6b93afa50
--- /dev/null
+++ b/youtube_dl/extractor/vice.py
@@ -0,0 +1,38 @@
+import re
+
+from .common import InfoExtractor
+from .ooyala import OoyalaIE
+from ..utils import ExtractorError
+
+
+class ViceIE(InfoExtractor):
+ _VALID_URL = r'http://www.vice.com/.*?/(?P<name>.+)'
+
+ _TEST = {
+ u'url': u'http://www.vice.com/Fringes/cowboy-capitalists-part-1',
+ u'file': u'43cW1mYzpia9IlestBjVpd23Yu3afAfp.mp4',
+ u'info_dict': {
+ u'title': u'VICE_COWBOYCAPITALISTS_PART01_v1_VICE_WM_1080p.mov',
+ },
+ u'params': {
+ # Requires ffmpeg (m3u8 manifest)
+ u'skip_download': True,
+ },
+ }
+
+ def _real_extract(self, url):
+ mobj = re.match(self._VALID_URL, url)
+ name = mobj.group('name')
+ webpage = self._download_webpage(url, name)
+ try:
+ ooyala_url = self._og_search_video_url(webpage)
+ except ExtractorError:
+ try:
+ embed_code = self._search_regex(
+ r'OO.Player.create\(\'ooyalaplayer\', \'(.+?)\'', webpage,
+ u'ooyala embed code')
+ ooyala_url = OoyalaIE._url_for_embed_code(embed_code)
+ except ExtractorError:
+ raise ExtractorError(u'The page doesn\'t contain a video', expected=True)
+ return self.url_result(ooyala_url, ie='Ooyala')
+
diff --git a/youtube_dl/extractor/xhamster.py b/youtube_dl/extractor/xhamster.py
index 88b8b6be0..361619694 100644
--- a/youtube_dl/extractor/xhamster.py
+++ b/youtube_dl/extractor/xhamster.py
@@ -11,8 +11,8 @@ from ..utils import (
class XHamsterIE(InfoExtractor):
"""Information Extractor for xHamster"""
- _VALID_URL = r'(?:http://)?(?:www.)?xhamster\.com/movies/(?P<id>[0-9]+)/.*\.html'
- _TEST = {
+ _VALID_URL = r'(?:http://)?(?:www\.)?xhamster\.com/movies/(?P<id>[0-9]+)/(?P<seo>.+?)\.html(?:\?.*)?'
+ _TESTS = [{
u'url': u'http://xhamster.com/movies/1509445/femaleagent_shy_beauty_takes_the_bait.html',
u'file': u'1509445.flv',
u'md5': u'9f48e0e8d58e3076bb236ff412ab62fa',
@@ -21,13 +21,24 @@ class XHamsterIE(InfoExtractor):
u"uploader_id": u"Ruseful2011",
u"title": u"FemaleAgent Shy beauty takes the bait"
}
- }
+ },
+ {
+ u'url': u'http://xhamster.com/movies/2221348/britney_spears_sexy_booty.html?hd',
+ u'file': u'2221348.flv',
+ u'md5': u'e767b9475de189320f691f49c679c4c7',
+ u'info_dict': {
+ u"upload_date": u"20130914",
+ u"uploader_id": u"jojo747400",
+ u"title": u"Britney Spears Sexy Booty"
+ }
+ }]
def _real_extract(self,url):
mobj = re.match(self._VALID_URL, url)
video_id = mobj.group('id')
- mrss_url = 'http://xhamster.com/movies/%s/.html' % video_id
+ seo = mobj.group('seo')
+ mrss_url = 'http://xhamster.com/movies/%s/%s.html?hd' % (video_id, seo)
webpage = self._download_webpage(mrss_url, video_id)
mobj = re.search(r'\'srv\': \'(?P<server>[^\']*)\',\s*\'file\': \'(?P<file>[^\']+)\',', webpage)
diff --git a/youtube_dl/extractor/youku.py b/youtube_dl/extractor/youku.py
index 996d38478..00fa2ccb5 100644
--- a/youtube_dl/extractor/youku.py
+++ b/youtube_dl/extractor/youku.py
@@ -66,6 +66,12 @@ class YoukuIE(InfoExtractor):
self.report_extraction(video_id)
try:
config = json.loads(jsondata)
+ error_code = config['data'][0].get('error_code')
+ if error_code:
+ # -8 means blocked outside China.
+ error = config['data'][0].get('error') # Chinese and English, separated by newline.
+ raise ExtractorError(error or u'Server reported error %i' % error_code,
+ expected=True)
video_title = config['data'][0]['title']
seed = config['data'][0]['seed']
@@ -89,6 +95,7 @@ class YoukuIE(InfoExtractor):
fileid = config['data'][0]['streamfileids'][format]
keys = [s['k'] for s in config['data'][0]['segs'][format]]
+ # segs is usually a dictionary, but an empty *list* if an error occured.
except (UnicodeDecodeError, ValueError, KeyError):
raise ExtractorError(u'Unable to extract info section')
diff --git a/youtube_dl/extractor/youtube.py b/youtube_dl/extractor/youtube.py
index f49665925..89c41efe5 100644
--- a/youtube_dl/extractor/youtube.py
+++ b/youtube_dl/extractor/youtube.py
@@ -1,15 +1,23 @@
# coding: utf-8
+import collections
+import errno
+import io
+import itertools
import json
-import netrc
+import os.path
import re
import socket
-import itertools
+import string
+import struct
+import traceback
import xml.etree.ElementTree
+import zlib
from .common import InfoExtractor, SearchInfoExtractor
from .subtitles import SubtitlesInfoExtractor
from ..utils import (
+ compat_chr,
compat_http_client,
compat_parse_qs,
compat_urllib_error,
@@ -23,6 +31,7 @@ from ..utils import (
unescapeHTML,
unified_strdate,
orderedSet,
+ write_json_file,
)
class YoutubeBaseInfoExtractor(InfoExtractor):
@@ -139,7 +148,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
(
(?:https?://)? # http(s):// (optional)
(?:(?:(?:(?:\w+\.)?youtube(?:-nocookie)?\.com/|
- tube\.majestyc\.net/) # the various hostnames, with wildcard subdomains
+ tube\.majestyc\.net/|
+ youtube\.googleapis\.com/) # the various hostnames, with wildcard subdomains
(?:.*?\#/)? # handle anchor (#/) redirect urls
(?: # the various things that can precede the ID:
(?:(?:v|embed|e)/) # v/ or embed/ or e/
@@ -351,7 +361,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
u"info_dict": {
u"upload_date": u"20120506",
u"title": u"Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]",
- u"description": u"md5:3e2666e0a55044490499ea45fe9037b7",
+ u"description": u"md5:bdac09887d209a4ed54b8f76b2bdaa8b",
u"uploader": u"Icona Pop",
u"uploader_id": u"IconaPop"
}
@@ -392,6 +402,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
if YoutubePlaylistIE.suitable(url): return False
return re.match(cls._VALID_URL, url, re.VERBOSE) is not None
+ def __init__(self, *args, **kwargs):
+ super(YoutubeIE, self).__init__(*args, **kwargs)
+ self._player_cache = {}
+
def report_video_webpage_download(self, video_id):
"""Report attempt to download video webpage."""
self.to_screen(u'%s: Downloading video webpage' % video_id)
@@ -412,11 +426,664 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
"""Indicate the download will use the RTMP protocol."""
self.to_screen(u'RTMP download detected')
- def _decrypt_signature(self, s):
+ def _extract_signature_function(self, video_id, player_url, slen):
+ id_m = re.match(r'.*-(?P<id>[a-zA-Z0-9_-]+)\.(?P<ext>[a-z]+)$',
+ player_url)
+ player_type = id_m.group('ext')
+ player_id = id_m.group('id')
+
+ # Read from filesystem cache
+ func_id = '%s_%s_%d' % (player_type, player_id, slen)
+ assert os.path.basename(func_id) == func_id
+ cache_dir = self._downloader.params.get('cachedir',
+ u'~/.youtube-dl/cache')
+
+ cache_enabled = cache_dir is not None
+ if cache_enabled:
+ cache_fn = os.path.join(os.path.expanduser(cache_dir),
+ u'youtube-sigfuncs',
+ func_id + '.json')
+ try:
+ with io.open(cache_fn, 'r', encoding='utf-8') as cachef:
+ cache_spec = json.load(cachef)
+ return lambda s: u''.join(s[i] for i in cache_spec)
+ except IOError:
+ pass # No cache available
+
+ if player_type == 'js':
+ code = self._download_webpage(
+ player_url, video_id,
+ note=u'Downloading %s player %s' % (player_type, player_id),
+ errnote=u'Download of %s failed' % player_url)
+ res = self._parse_sig_js(code)
+ elif player_type == 'swf':
+ urlh = self._request_webpage(
+ player_url, video_id,
+ note=u'Downloading %s player %s' % (player_type, player_id),
+ errnote=u'Download of %s failed' % player_url)
+ code = urlh.read()
+ res = self._parse_sig_swf(code)
+ else:
+ assert False, 'Invalid player type %r' % player_type
+
+ if cache_enabled:
+ try:
+ test_string = u''.join(map(compat_chr, range(slen)))
+ cache_res = res(test_string)
+ cache_spec = [ord(c) for c in cache_res]
+ try:
+ os.makedirs(os.path.dirname(cache_fn))
+ except OSError as ose:
+ if ose.errno != errno.EEXIST:
+ raise
+ write_json_file(cache_spec, cache_fn)
+ except Exception:
+ tb = traceback.format_exc()
+ self._downloader.report_warning(
+ u'Writing cache to %r failed: %s' % (cache_fn, tb))
+
+ return res
+
+ def _print_sig_code(self, func, slen):
+ def gen_sig_code(idxs):
+ def _genslice(start, end, step):
+ starts = u'' if start == 0 else str(start)
+ ends = (u':%d' % (end+step)) if end + step >= 0 else u':'
+ steps = u'' if step == 1 else (u':%d' % step)
+ return u's[%s%s%s]' % (starts, ends, steps)
+
+ step = None
+ start = '(Never used)' # Quelch pyflakes warnings - start will be
+ # set as soon as step is set
+ for i, prev in zip(idxs[1:], idxs[:-1]):
+ if step is not None:
+ if i - prev == step:
+ continue
+ yield _genslice(start, prev, step)
+ step = None
+ continue
+ if i - prev in [-1, 1]:
+ step = i - prev
+ start = prev
+ continue
+ else:
+ yield u's[%d]' % prev
+ if step is None:
+ yield u's[%d]' % i
+ else:
+ yield _genslice(start, i, step)
+
+ test_string = u''.join(map(compat_chr, range(slen)))
+ cache_res = func(test_string)
+ cache_spec = [ord(c) for c in cache_res]
+ expr_code = u' + '.join(gen_sig_code(cache_spec))
+ code = u'if len(s) == %d:\n return %s\n' % (slen, expr_code)
+ self.to_screen(u'Extracted signature function:\n' + code)
+
+ def _parse_sig_js(self, jscode):
+ funcname = self._search_regex(
+ r'signature=([a-zA-Z]+)', jscode,
+ u'Initial JS player signature function name')
+
+ functions = {}
+
+ def argidx(varname):
+ return string.lowercase.index(varname)
+
+ def interpret_statement(stmt, local_vars, allow_recursion=20):
+ if allow_recursion < 0:
+ raise ExtractorError(u'Recursion limit reached')
+
+ if stmt.startswith(u'var '):
+ stmt = stmt[len(u'var '):]
+ ass_m = re.match(r'^(?P<out>[a-z]+)(?:\[(?P<index>[^\]]+)\])?' +
+ r'=(?P<expr>.*)$', stmt)
+ if ass_m:
+ if ass_m.groupdict().get('index'):
+ def assign(val):
+ lvar = local_vars[ass_m.group('out')]
+ idx = interpret_expression(ass_m.group('index'),
+ local_vars, allow_recursion)
+ assert isinstance(idx, int)
+ lvar[idx] = val
+ return val
+ expr = ass_m.group('expr')
+ else:
+ def assign(val):
+ local_vars[ass_m.group('out')] = val
+ return val
+ expr = ass_m.group('expr')
+ elif stmt.startswith(u'return '):
+ assign = lambda v: v
+ expr = stmt[len(u'return '):]
+ else:
+ raise ExtractorError(
+ u'Cannot determine left side of statement in %r' % stmt)
+
+ v = interpret_expression(expr, local_vars, allow_recursion)
+ return assign(v)
+
+ def interpret_expression(expr, local_vars, allow_recursion):
+ if expr.isdigit():
+ return int(expr)
+
+ if expr.isalpha():
+ return local_vars[expr]
+
+ m = re.match(r'^(?P<in>[a-z]+)\.(?P<member>.*)$', expr)
+ if m:
+ member = m.group('member')
+ val = local_vars[m.group('in')]
+ if member == 'split("")':
+ return list(val)
+ if member == 'join("")':
+ return u''.join(val)
+ if member == 'length':
+ return len(val)
+ if member == 'reverse()':
+ return val[::-1]
+ slice_m = re.match(r'slice\((?P<idx>.*)\)', member)
+ if slice_m:
+ idx = interpret_expression(
+ slice_m.group('idx'), local_vars, allow_recursion-1)
+ return val[idx:]
+
+ m = re.match(
+ r'^(?P<in>[a-z]+)\[(?P<idx>.+)\]$', expr)
+ if m:
+ val = local_vars[m.group('in')]
+ idx = interpret_expression(m.group('idx'), local_vars,
+ allow_recursion-1)
+ return val[idx]
+
+ m = re.match(r'^(?P<a>.+?)(?P<op>[%])(?P<b>.+?)$', expr)
+ if m:
+ a = interpret_expression(m.group('a'),
+ local_vars, allow_recursion)
+ b = interpret_expression(m.group('b'),
+ local_vars, allow_recursion)
+ return a % b
+
+ m = re.match(
+ r'^(?P<func>[a-zA-Z]+)\((?P<args>[a-z0-9,]+)\)$', expr)
+ if m:
+ fname = m.group('func')
+ if fname not in functions:
+ functions[fname] = extract_function(fname)
+ argvals = [int(v) if v.isdigit() else local_vars[v]
+ for v in m.group('args').split(',')]
+ return functions[fname](argvals)
+ raise ExtractorError(u'Unsupported JS expression %r' % expr)
+
+ def extract_function(funcname):
+ func_m = re.search(
+ r'function ' + re.escape(funcname) +
+ r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
+ jscode)
+ argnames = func_m.group('args').split(',')
+
+ def resf(args):
+ local_vars = dict(zip(argnames, args))
+ for stmt in func_m.group('code').split(';'):
+ res = interpret_statement(stmt, local_vars)
+ return res
+ return resf
+
+ initial_function = extract_function(funcname)
+ return lambda s: initial_function([s])
+
+ def _parse_sig_swf(self, file_contents):
+ if file_contents[1:3] != b'WS':
+ raise ExtractorError(
+ u'Not an SWF file; header is %r' % file_contents[:3])
+ if file_contents[:1] == b'C':
+ content = zlib.decompress(file_contents[8:])
+ else:
+ raise NotImplementedError(u'Unsupported compression format %r' %
+ file_contents[:1])
+
+ def extract_tags(content):
+ pos = 0
+ while pos < len(content):
+ header16 = struct.unpack('<H', content[pos:pos+2])[0]
+ pos += 2
+ tag_code = header16 >> 6
+ tag_len = header16 & 0x3f
+ if tag_len == 0x3f:
+ tag_len = struct.unpack('<I', content[pos:pos+4])[0]
+ pos += 4
+ assert pos+tag_len <= len(content)
+ yield (tag_code, content[pos:pos+tag_len])
+ pos += tag_len
+
+ code_tag = next(tag
+ for tag_code, tag in extract_tags(content)
+ if tag_code == 82)
+ p = code_tag.index(b'\0', 4) + 1
+ code_reader = io.BytesIO(code_tag[p:])
+
+ # Parse ABC (AVM2 ByteCode)
+ def read_int(reader=None):
+ if reader is None:
+ reader = code_reader
+ res = 0
+ shift = 0
+ for _ in range(5):
+ buf = reader.read(1)
+ assert len(buf) == 1
+ b = struct.unpack('<B', buf)[0]
+ res = res | ((b & 0x7f) << shift)
+ if b & 0x80 == 0:
+ break
+ shift += 7
+ return res
+
+ def u30(reader=None):
+ res = read_int(reader)
+ assert res & 0xf0000000 == 0
+ return res
+ u32 = read_int
+
+ def s32(reader=None):
+ v = read_int(reader)
+ if v & 0x80000000 != 0:
+ v = - ((v ^ 0xffffffff) + 1)
+ return v
+
+ def read_string(reader=None):
+ if reader is None:
+ reader = code_reader
+ slen = u30(reader)
+ resb = reader.read(slen)
+ assert len(resb) == slen
+ return resb.decode('utf-8')
+
+ def read_bytes(count, reader=None):
+ if reader is None:
+ reader = code_reader
+ resb = reader.read(count)
+ assert len(resb) == count
+ return resb
+
+ def read_byte(reader=None):
+ resb = read_bytes(1, reader=reader)
+ res = struct.unpack('<B', resb)[0]
+ return res
+
+ # minor_version + major_version
+ read_bytes(2 + 2)
+
+ # Constant pool
+ int_count = u30()
+ for _c in range(1, int_count):
+ s32()
+ uint_count = u30()
+ for _c in range(1, uint_count):
+ u32()
+ double_count = u30()
+ read_bytes((double_count-1) * 8)
+ string_count = u30()
+ constant_strings = [u'']
+ for _c in range(1, string_count):
+ s = read_string()
+ constant_strings.append(s)
+ namespace_count = u30()
+ for _c in range(1, namespace_count):
+ read_bytes(1) # kind
+ u30() # name
+ ns_set_count = u30()
+ for _c in range(1, ns_set_count):
+ count = u30()
+ for _c2 in range(count):
+ u30()
+ multiname_count = u30()
+ MULTINAME_SIZES = {
+ 0x07: 2, # QName
+ 0x0d: 2, # QNameA
+ 0x0f: 1, # RTQName
+ 0x10: 1, # RTQNameA
+ 0x11: 0, # RTQNameL
+ 0x12: 0, # RTQNameLA
+ 0x09: 2, # Multiname
+ 0x0e: 2, # MultinameA
+ 0x1b: 1, # MultinameL
+ 0x1c: 1, # MultinameLA
+ }
+ multinames = [u'']
+ for _c in range(1, multiname_count):
+ kind = u30()
+ assert kind in MULTINAME_SIZES, u'Invalid multiname kind %r' % kind
+ if kind == 0x07:
+ u30() # namespace_idx
+ name_idx = u30()
+ multinames.append(constant_strings[name_idx])
+ else:
+ multinames.append('[MULTINAME kind: %d]' % kind)
+ for _c2 in range(MULTINAME_SIZES[kind]):
+ u30()
+
+ # Methods
+ method_count = u30()
+ MethodInfo = collections.namedtuple(
+ 'MethodInfo',
+ ['NEED_ARGUMENTS', 'NEED_REST'])
+ method_infos = []
+ for method_id in range(method_count):
+ param_count = u30()
+ u30() # return type
+ for _ in range(param_count):
+ u30() # param type
+ u30() # name index (always 0 for youtube)
+ flags = read_byte()
+ if flags & 0x08 != 0:
+ # Options present
+ option_count = u30()
+ for c in range(option_count):
+ u30() # val
+ read_bytes(1) # kind
+ if flags & 0x80 != 0:
+ # Param names present
+ for _ in range(param_count):
+ u30() # param name
+ mi = MethodInfo(flags & 0x01 != 0, flags & 0x04 != 0)
+ method_infos.append(mi)
+
+ # Metadata
+ metadata_count = u30()
+ for _c in range(metadata_count):
+ u30() # name
+ item_count = u30()
+ for _c2 in range(item_count):
+ u30() # key
+ u30() # value
+
+ def parse_traits_info():
+ trait_name_idx = u30()
+ kind_full = read_byte()
+ kind = kind_full & 0x0f
+ attrs = kind_full >> 4
+ methods = {}
+ if kind in [0x00, 0x06]: # Slot or Const
+ u30() # Slot id
+ u30() # type_name_idx
+ vindex = u30()
+ if vindex != 0:
+ read_byte() # vkind
+ elif kind in [0x01, 0x02, 0x03]: # Method / Getter / Setter
+ u30() # disp_id
+ method_idx = u30()
+ methods[multinames[trait_name_idx]] = method_idx
+ elif kind == 0x04: # Class
+ u30() # slot_id
+ u30() # classi
+ elif kind == 0x05: # Function
+ u30() # slot_id
+ function_idx = u30()
+ methods[function_idx] = multinames[trait_name_idx]
+ else:
+ raise ExtractorError(u'Unsupported trait kind %d' % kind)
+
+ if attrs & 0x4 != 0: # Metadata present
+ metadata_count = u30()
+ for _c3 in range(metadata_count):
+ u30() # metadata index
+
+ return methods
+
+ # Classes
+ TARGET_CLASSNAME = u'SignatureDecipher'
+ searched_idx = multinames.index(TARGET_CLASSNAME)
+ searched_class_id = None
+ class_count = u30()
+ for class_id in range(class_count):
+ name_idx = u30()
+ if name_idx == searched_idx:
+ # We found the class we're looking for!
+ searched_class_id = class_id
+ u30() # super_name idx
+ flags = read_byte()
+ if flags & 0x08 != 0: # Protected namespace is present
+ u30() # protected_ns_idx
+ intrf_count = u30()
+ for _c2 in range(intrf_count):
+ u30()
+ u30() # iinit
+ trait_count = u30()
+ for _c2 in range(trait_count):
+ parse_traits_info()
+
+ if searched_class_id is None:
+ raise ExtractorError(u'Target class %r not found' %
+ TARGET_CLASSNAME)
+
+ method_names = {}
+ method_idxs = {}
+ for class_id in range(class_count):
+ u30() # cinit
+ trait_count = u30()
+ for _c2 in range(trait_count):
+ trait_methods = parse_traits_info()
+ if class_id == searched_class_id:
+ method_names.update(trait_methods.items())
+ method_idxs.update(dict(
+ (idx, name)
+ for name, idx in trait_methods.items()))
+
+ # Scripts
+ script_count = u30()
+ for _c in range(script_count):
+ u30() # init
+ trait_count = u30()
+ for _c2 in range(trait_count):
+ parse_traits_info()
+
+ # Method bodies
+ method_body_count = u30()
+ Method = collections.namedtuple('Method', ['code', 'local_count'])
+ methods = {}
+ for _c in range(method_body_count):
+ method_idx = u30()
+ u30() # max_stack
+ local_count = u30()
+ u30() # init_scope_depth
+ u30() # max_scope_depth
+ code_length = u30()
+ code = read_bytes(code_length)
+ if method_idx in method_idxs:
+ m = Method(code, local_count)
+ methods[method_idxs[method_idx]] = m
+ exception_count = u30()
+ for _c2 in range(exception_count):
+ u30() # from
+ u30() # to
+ u30() # target
+ u30() # exc_type
+ u30() # var_name
+ trait_count = u30()
+ for _c2 in range(trait_count):
+ parse_traits_info()
+
+ assert p + code_reader.tell() == len(code_tag)
+ assert len(methods) == len(method_idxs)
+
+ method_pyfunctions = {}
+
+ def extract_function(func_name):
+ if func_name in method_pyfunctions:
+ return method_pyfunctions[func_name]
+ if func_name not in methods:
+ raise ExtractorError(u'Cannot find function %r' % func_name)
+ m = methods[func_name]
+
+ def resfunc(args):
+ registers = ['(this)'] + list(args) + [None] * m.local_count
+ stack = []
+ coder = io.BytesIO(m.code)
+ while True:
+ opcode = struct.unpack('!B', coder.read(1))[0]
+ if opcode == 36: # pushbyte
+ v = struct.unpack('!B', coder.read(1))[0]
+ stack.append(v)
+ elif opcode == 44: # pushstring
+ idx = u30(coder)
+ stack.append(constant_strings[idx])
+ elif opcode == 48: # pushscope
+ # We don't implement the scope register, so we'll just
+ # ignore the popped value
+ stack.pop()
+ elif opcode == 70: # callproperty
+ index = u30(coder)
+ mname = multinames[index]
+ arg_count = u30(coder)
+ args = list(reversed(
+ [stack.pop() for _ in range(arg_count)]))
+ obj = stack.pop()
+ if mname == u'split':
+ assert len(args) == 1
+ assert isinstance(args[0], compat_str)
+ assert isinstance(obj, compat_str)
+ if args[0] == u'':
+ res = list(obj)
+ else:
+ res = obj.split(args[0])
+ stack.append(res)
+ elif mname == u'slice':
+ assert len(args) == 1
+ assert isinstance(args[0], int)
+ assert isinstance(obj, list)
+ res = obj[args[0]:]
+ stack.append(res)
+ elif mname == u'join':
+ assert len(args) == 1
+ assert isinstance(args[0], compat_str)
+ assert isinstance(obj, list)
+ res = args[0].join(obj)
+ stack.append(res)
+ elif mname in method_pyfunctions:
+ stack.append(method_pyfunctions[mname](args))
+ else:
+ raise NotImplementedError(
+ u'Unsupported property %r on %r'
+ % (mname, obj))
+ elif opcode == 72: # returnvalue
+ res = stack.pop()
+ return res
+ elif opcode == 79: # callpropvoid
+ index = u30(coder)
+ mname = multinames[index]
+ arg_count = u30(coder)
+ args = list(reversed(
+ [stack.pop() for _ in range(arg_count)]))
+ obj = stack.pop()
+ if mname == u'reverse':
+ assert isinstance(obj, list)
+ obj.reverse()
+ else:
+ raise NotImplementedError(
+ u'Unsupported (void) property %r on %r'
+ % (mname, obj))
+ elif opcode == 93: # findpropstrict
+ index = u30(coder)
+ mname = multinames[index]
+ res = extract_function(mname)
+ stack.append(res)
+ elif opcode == 97: # setproperty
+ index = u30(coder)
+ value = stack.pop()
+ idx = stack.pop()
+ obj = stack.pop()
+ assert isinstance(obj, list)
+ assert isinstance(idx, int)
+ obj[idx] = value
+ elif opcode == 98: # getlocal
+ index = u30(coder)
+ stack.append(registers[index])
+ elif opcode == 99: # setlocal
+ index = u30(coder)
+ value = stack.pop()
+ registers[index] = value
+ elif opcode == 102: # getproperty
+ index = u30(coder)
+ pname = multinames[index]
+ if pname == u'length':
+ obj = stack.pop()
+ assert isinstance(obj, list)
+ stack.append(len(obj))
+ else: # Assume attribute access
+ idx = stack.pop()
+ assert isinstance(idx, int)
+ obj = stack.pop()
+ assert isinstance(obj, list)
+ stack.append(obj[idx])
+ elif opcode == 128: # coerce
+ u30(coder)
+ elif opcode == 133: # coerce_s
+ assert isinstance(stack[-1], (type(None), compat_str))
+ elif opcode == 164: # modulo
+ value2 = stack.pop()
+ value1 = stack.pop()
+ res = value1 % value2
+ stack.append(res)
+ elif opcode == 208: # getlocal_0
+ stack.append(registers[0])
+ elif opcode == 209: # getlocal_1
+ stack.append(registers[1])
+ elif opcode == 210: # getlocal_2
+ stack.append(registers[2])
+ elif opcode == 211: # getlocal_3
+ stack.append(registers[3])
+ elif opcode == 214: # setlocal_2
+ registers[2] = stack.pop()
+ elif opcode == 215: # setlocal_3
+ registers[3] = stack.pop()
+ else:
+ raise NotImplementedError(
+ u'Unsupported opcode %d' % opcode)
+
+ method_pyfunctions[func_name] = resfunc
+ return resfunc
+
+ initial_function = extract_function(u'decipher')
+ return lambda s: initial_function([s])
+
+ def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
"""Turn the encrypted s field into a working signature"""
- if len(s) == 92:
+ if player_url is not None:
+ try:
+ if player_url not in self._player_cache:
+ func = self._extract_signature_function(
+ video_id, player_url, len(s)
+ )
+ self._player_cache[player_url] = func
+ func = self._player_cache[player_url]
+ if self._downloader.params.get('youtube_print_sig_code'):
+ self._print_sig_code(func, len(s))
+ return func(s)
+ except Exception:
+ tb = traceback.format_exc()
+ self._downloader.report_warning(
+ u'Automatic signature extraction failed: ' + tb)
+
+ self._downloader.report_warning(
+ u'Warning: Falling back to static signature algorithm')
+
+ return self._static_decrypt_signature(
+ s, video_id, player_url, age_gate)
+
+ def _static_decrypt_signature(self, s, video_id, player_url, age_gate):
+ if age_gate:
+ # The videos with age protection use another player, so the
+ # algorithms can be different.
+ if len(s) == 86:
+ return s[2:63] + s[82] + s[64:82] + s[63]
+
+ if len(s) == 93:
+ return s[86:29:-1] + s[88] + s[28:5:-1]
+ elif len(s) == 92:
return s[25] + s[3:25] + s[0] + s[26:42] + s[79] + s[43:79] + s[91] + s[80:83]
+ elif len(s) == 91:
+ return s[84:27:-1] + s[86] + s[26:5:-1]
elif len(s) == 90:
return s[25] + s[3:25] + s[2] + s[26:40] + s[77] + s[41:77] + s[89] + s[78:81]
elif len(s) == 89:
@@ -426,13 +1093,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
elif len(s) == 87:
return s[6:27] + s[4] + s[28:39] + s[27] + s[40:59] + s[2] + s[60:]
elif len(s) == 86:
- return s[5:34] + s[0] + s[35:38] + s[3] + s[39:45] + s[38] + s[46:53] + s[73] + s[54:73] + s[85] + s[74:85] + s[53]
+ return s[80:72:-1] + s[16] + s[71:39:-1] + s[72] + s[38:16:-1] + s[82] + s[15::-1]
elif len(s) == 85:
- return s[40] + s[82:43:-1] + s[22] + s[42:40:-1] + s[83] + s[39:22:-1] + s[0] + s[21:2:-1]
+ return s[3:11] + s[0] + s[12:55] + s[84] + s[56:84]
elif len(s) == 84:
- return s[81:36:-1] + s[0] + s[35:2:-1]
+ return s[78:70:-1] + s[14] + s[69:37:-1] + s[70] + s[36:14:-1] + s[80] + s[:14][::-1]
elif len(s) == 83:
- return s[81:64:-1] + s[82] + s[63:52:-1] + s[45] + s[51:45:-1] + s[1] + s[44:1:-1] + s[0]
+ return s[80:63:-1] + s[0] + s[62:0:-1] + s[63]
elif len(s) == 82:
return s[80:73:-1] + s[81] + s[72:54:-1] + s[2] + s[53:43:-1] + s[0] + s[42:2:-1] + s[43] + s[1] + s[54]
elif len(s) == 81:
@@ -445,15 +1112,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
else:
raise ExtractorError(u'Unable to decrypt signature, key length %d not supported; retrying might work' % (len(s)))
- def _decrypt_signature_age_gate(self, s):
- # The videos with age protection use another player, so the algorithms
- # can be different.
- if len(s) == 86:
- return s[2:63] + s[82] + s[64:82] + s[63]
- else:
- # Fallback to the other algortihms
- return self._decrypt_signature(s)
-
def _get_available_subtitles(self, video_id):
try:
sub_list = self._download_webpage(
@@ -626,7 +1284,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
video_webpage = video_webpage_bytes.decode('utf-8', 'ignore')
# Attempt to extract SWF player URL
- mobj = re.search(r'swfConfig.*?"(http:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
+ mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
if mobj is not None:
player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
else:
@@ -779,24 +1437,34 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
if 'sig' in url_data:
url += '&signature=' + url_data['sig'][0]
elif 's' in url_data:
+ encrypted_sig = url_data['s'][0]
if self._downloader.params.get('verbose'):
- s = url_data['s'][0]
if age_gate:
- player_version = self._search_regex(r'ad3-(.+?)\.swf',
- video_info['ad3_module'][0] if 'ad3_module' in video_info else 'NOT FOUND',
- 'flash player', fatal=False)
- player = 'flash player %s' % player_version
+ if player_url is None:
+ player_version = 'unknown'
+ else:
+ player_version = self._search_regex(
+ r'-(.+)\.swf$', player_url,
+ u'flash player', fatal=False)
+ player_desc = 'flash player %s' % player_version
else:
- player = u'html5 player %s' % self._search_regex(r'html5player-(.+?)\.js', video_webpage,
+ player_version = self._search_regex(
+ r'html5player-(.+?)\.js', video_webpage,
'html5 player', fatal=False)
- parts_sizes = u'.'.join(compat_str(len(part)) for part in s.split('.'))
+ player_desc = u'html5 player %s' % player_version
+
+ parts_sizes = u'.'.join(compat_str(len(part)) for part in encrypted_sig.split('.'))
self.to_screen(u'encrypted signature length %d (%s), itag %s, %s' %
- (len(s), parts_sizes, url_data['itag'][0], player))
- encrypted_sig = url_data['s'][0]
- if age_gate:
- signature = self._decrypt_signature_age_gate(encrypted_sig)
- else:
- signature = self._decrypt_signature(encrypted_sig)
+ (len(encrypted_sig), parts_sizes, url_data['itag'][0], player_desc))
+
+ if not age_gate:
+ jsplayer_url_json = self._search_regex(
+ r'"assets":.+?"js":\s*("[^"]+")',
+ video_webpage, u'JS player URL')
+ player_url = json.loads(jsplayer_url_json)
+
+ signature = self._decrypt_signature(
+ encrypted_sig, video_id, player_url, age_gate)
url += '&signature=' + signature
if 'ratebypass' not in url:
url += '&ratebypass=yes'
@@ -1007,6 +1675,9 @@ class YoutubeUserIE(InfoExtractor):
response = json.loads(page)
except ValueError as err:
raise ExtractorError(u'Invalid JSON in API response: ' + compat_str(err))
+ if 'entry' not in response['feed']:
+ # Number of videos is a multiple of self._MAX_RESULTS
+ break
# Extract video identifiers
ids_in_page = []