aboutsummaryrefslogtreecommitdiff
path: root/youtube_dl/extractor
diff options
context:
space:
mode:
Diffstat (limited to 'youtube_dl/extractor')
-rw-r--r--youtube_dl/extractor/__init__.py8
-rw-r--r--youtube_dl/extractor/ctsnews.py93
-rw-r--r--youtube_dl/extractor/generic.py24
-rw-r--r--youtube_dl/extractor/ivi.py27
-rw-r--r--youtube_dl/extractor/nextmedia.py163
-rw-r--r--youtube_dl/extractor/srmediathek.py2
-rw-r--r--youtube_dl/extractor/viddler.py63
-rw-r--r--youtube_dl/extractor/xuite.py142
8 files changed, 493 insertions, 29 deletions
diff --git a/youtube_dl/extractor/__init__.py b/youtube_dl/extractor/__init__.py
index 873ae69d3..d3f51d8c4 100644
--- a/youtube_dl/extractor/__init__.py
+++ b/youtube_dl/extractor/__init__.py
@@ -82,6 +82,7 @@ from .crunchyroll import (
CrunchyrollShowPlaylistIE
)
from .cspan import CSpanIE
+from .ctsnews import CtsNewsIE
from .dailymotion import (
DailymotionIE,
DailymotionPlaylistIE,
@@ -285,6 +286,12 @@ from .netzkino import NetzkinoIE
from .nerdcubed import NerdCubedFeedIE
from .newgrounds import NewgroundsIE
from .newstube import NewstubeIE
+from .nextmedia import (
+ NextMediaIE,
+ NextMediaActionNewsIE,
+ AppleDailyRealtimeNewsIE,
+ AppleDailyAnimationNewsIE
+)
from .nfb import NFBIE
from .nfl import NFLIE
from .nhl import NHLIE, NHLVideocenterIE
@@ -547,6 +554,7 @@ from .xminus import XMinusIE
from .xnxx import XNXXIE
from .xvideos import XVideosIE
from .xtube import XTubeUserIE, XTubeIE
+from .xuite import XuiteIE
from .xxxymovies import XXXYMoviesIE
from .yahoo import (
YahooIE,
diff --git a/youtube_dl/extractor/ctsnews.py b/youtube_dl/extractor/ctsnews.py
new file mode 100644
index 000000000..0226f8036
--- /dev/null
+++ b/youtube_dl/extractor/ctsnews.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .common import InfoExtractor
+from ..utils import parse_iso8601, ExtractorError
+
+
+class CtsNewsIE(InfoExtractor):
+ # https connection failed (Connection reset)
+ _VALID_URL = r'http://news\.cts\.com\.tw/[a-z]+/[a-z]+/\d+/(?P<id>\d+)\.html'
+ _TESTS = [{
+ 'url': 'http://news.cts.com.tw/cts/international/201501/201501291578109.html',
+ 'md5': 'a9875cb790252b08431186d741beaabe',
+ 'info_dict': {
+ 'id': '201501291578109',
+ 'ext': 'mp4',
+ 'title': '以色列.真主黨交火 3人死亡',
+ 'description': 'md5:95e9b295c898b7ff294f09d450178d7d',
+ 'timestamp': 1422528540,
+ 'upload_date': '20150129',
+ }
+ }, {
+ # News count not appear on page but still available in database
+ 'url': 'http://news.cts.com.tw/cts/international/201309/201309031304098.html',
+ 'md5': '3aee7e0df7cdff94e43581f54c22619e',
+ 'info_dict': {
+ 'id': '201309031304098',
+ 'ext': 'mp4',
+ 'title': '韓國31歲童顏男 貌如十多歲小孩',
+ 'description': 'md5:f183feeba3752b683827aab71adad584',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'timestamp': 1378205880,
+ 'upload_date': '20130903',
+ }
+ }, {
+ # With Youtube embedded video
+ 'url': 'http://news.cts.com.tw/cts/money/201501/201501291578003.html',
+ 'md5': '1d842c771dc94c8c3bca5af2cc1db9c5',
+ 'add_ie': ['Youtube'],
+ 'info_dict': {
+ 'id': 'OVbfO7d0_hQ',
+ 'ext': 'mp4',
+ 'title': 'iPhone6熱銷 蘋果財報亮眼',
+ 'description': 'md5:f395d4f485487bb0f992ed2c4b07aa7d',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'upload_date': '20150128',
+ 'uploader_id': 'TBSCTS',
+ 'uploader': '中華電視公司',
+ }
+ }]
+
+ def _real_extract(self, url):
+ news_id = self._match_id(url)
+ page = self._download_webpage(url, news_id)
+
+ if self._search_regex(r'(CTSPlayer2)', page, 'CTSPlayer2 identifier', default=None):
+ feed_url = self._html_search_regex(
+ r'(http://news\.cts\.com\.tw/action/mp4feed\.php\?news_id=\d+)',
+ page, 'feed url')
+ video_url = self._download_webpage(
+ feed_url, news_id, note='Fetching feed')
+ else:
+ self.to_screen('Not CTSPlayer video, trying Youtube...')
+ youtube_url = self._search_regex(
+ r'src="(//www\.youtube\.com/embed/[^"]+)"', page, 'youtube url',
+ default=None)
+ if not youtube_url:
+ raise ExtractorError('The news includes no videos!', expected=True)
+
+ return {
+ '_type': 'url',
+ 'url': youtube_url,
+ 'ie_key': 'Youtube',
+ }
+
+ description = self._html_search_meta('description', page)
+ title = self._html_search_meta('title', page)
+ thumbnail = self._html_search_meta('image', page)
+
+ datetime_str = self._html_search_regex(
+ r'(\d{4}/\d{2}/\d{2} \d{2}:\d{2})', page, 'date and time')
+ # Transform into ISO 8601 format with timezone info
+ datetime_str = datetime_str.replace('/', '-') + ':00+0800'
+ timestamp = parse_iso8601(datetime_str, delimiter=' ')
+
+ return {
+ 'id': news_id,
+ 'url': video_url,
+ 'title': title,
+ 'description': description,
+ 'thumbnail': thumbnail,
+ 'timestamp': timestamp,
+ }
diff --git a/youtube_dl/extractor/generic.py b/youtube_dl/extractor/generic.py
index ad16b8330..41884ed7a 100644
--- a/youtube_dl/extractor/generic.py
+++ b/youtube_dl/extractor/generic.py
@@ -498,6 +498,19 @@ class GenericIE(InfoExtractor):
'uploader': 'www.abc.net.au',
'title': 'Game of Thrones with dice - Dungeons and Dragons fantasy role-playing game gets new life - 19/01/2015',
}
+ },
+ # embedded viddler video
+ {
+ 'url': 'http://deadspin.com/i-cant-stop-watching-john-wall-chop-the-nuggets-with-th-1681801597',
+ 'info_dict': {
+ 'id': '4d03aad9',
+ 'ext': 'mp4',
+ 'uploader': 'deadspin',
+ 'title': 'WALL-TO-GORTAT',
+ 'timestamp': 1422285291,
+ 'upload_date': '20150126',
+ },
+ 'add_ie': ['Viddler'],
}
]
@@ -860,9 +873,16 @@ class GenericIE(InfoExtractor):
if mobj is not None:
return self.url_result(mobj.group('url'))
+ # Look for embedded Viddler player
+ mobj = re.search(
+ r'<(?:iframe[^>]+?src|param[^>]+?value)=(["\'])(?P<url>(?:https?:)?//(?:www\.)?viddler\.com/(?:embed|player)/.+?)\1',
+ webpage)
+ if mobj is not None:
+ return self.url_result(mobj.group('url'))
+
# Look for Ooyala videos
- mobj = (re.search(r'player.ooyala.com/[^"?]+\?[^"]*?(?:embedCode|ec)=(?P<ec>[^"&]+)', webpage) or
- re.search(r'OO.Player.create\([\'"].*?[\'"],\s*[\'"](?P<ec>.{32})[\'"]', webpage))
+ mobj = (re.search(r'player\.ooyala\.com/[^"?]+\?[^"]*?(?:embedCode|ec)=(?P<ec>[^"&]+)', webpage) or
+ re.search(r'OO\.Player\.create\([\'"].*?[\'"],\s*[\'"](?P<ec>.{32})[\'"]', webpage))
if mobj is not None:
return OoyalaIE._build_url_result(mobj.group('ec'))
diff --git a/youtube_dl/extractor/ivi.py b/youtube_dl/extractor/ivi.py
index 7a400323d..e82594444 100644
--- a/youtube_dl/extractor/ivi.py
+++ b/youtube_dl/extractor/ivi.py
@@ -16,7 +16,7 @@ from ..utils import (
class IviIE(InfoExtractor):
IE_DESC = 'ivi.ru'
IE_NAME = 'ivi'
- _VALID_URL = r'https?://(?:www\.)?ivi\.ru/(?:watch/(?:[^/]+/)?|video/player\?.*?videoId=)(?P<videoid>\d+)'
+ _VALID_URL = r'https?://(?:www\.)?ivi\.ru/(?:watch/(?:[^/]+/)?|video/player\?.*?videoId=)(?P<id>\d+)'
_TESTS = [
# Single movie
@@ -63,29 +63,34 @@ class IviIE(InfoExtractor):
return int(m.group('commentcount')) if m is not None else 0
def _real_extract(self, url):
- mobj = re.match(self._VALID_URL, url)
- video_id = mobj.group('videoid')
+ video_id = self._match_id(url)
api_url = 'http://api.digitalaccess.ru/api/json/'
- data = {'method': 'da.content.get',
- 'params': [video_id, {'site': 's183',
- 'referrer': 'http://www.ivi.ru/watch/%s' % video_id,
- 'contentid': video_id
- }
- ]
+ data = {
+ 'method': 'da.content.get',
+ 'params': [
+ video_id, {
+ 'site': 's183',
+ 'referrer': 'http://www.ivi.ru/watch/%s' % video_id,
+ 'contentid': video_id
}
+ ]
+ }
request = compat_urllib_request.Request(api_url, json.dumps(data))
- video_json_page = self._download_webpage(request, video_id, 'Downloading video JSON')
+ video_json_page = self._download_webpage(
+ request, video_id, 'Downloading video JSON')
video_json = json.loads(video_json_page)
if 'error' in video_json:
error = video_json['error']
if error['origin'] == 'NoRedisValidData':
raise ExtractorError('Video %s does not exist' % video_id, expected=True)
- raise ExtractorError('Unable to download video %s: %s' % (video_id, error['message']), expected=True)
+ raise ExtractorError(
+ 'Unable to download video %s: %s' % (video_id, error['message']),
+ expected=True)
result = video_json['result']
diff --git a/youtube_dl/extractor/nextmedia.py b/youtube_dl/extractor/nextmedia.py
new file mode 100644
index 000000000..02dba4ef6
--- /dev/null
+++ b/youtube_dl/extractor/nextmedia.py
@@ -0,0 +1,163 @@
+# coding: utf-8
+from __future__ import unicode_literals
+
+from .common import InfoExtractor
+from ..utils import parse_iso8601
+
+
+class NextMediaIE(InfoExtractor):
+ _VALID_URL = r'http://hk.apple.nextmedia.com/[^/]+/[^/]+/(?P<date>\d+)/(?P<id>\d+)'
+ _TESTS = [{
+ 'url': 'http://hk.apple.nextmedia.com/realtime/news/20141108/53109199',
+ 'md5': 'dff9fad7009311c421176d1ac90bfe4f',
+ 'info_dict': {
+ 'id': '53109199',
+ 'ext': 'mp4',
+ 'title': '【佔領金鐘】50外國領事議員撐場 讚學生勇敢香港有希望',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'description': 'md5:28222b9912b6665a21011b034c70fcc7',
+ 'timestamp': 1415456273,
+ 'upload_date': '20141108',
+ }
+ }]
+
+ _URL_PATTERN = r'\{ url: \'(.+)\' \}'
+
+ def _real_extract(self, url):
+ news_id = self._match_id(url)
+ page = self._download_webpage(url, news_id)
+ return self._extract_from_nextmedia_page(news_id, url, page)
+
+ def _extract_from_nextmedia_page(self, news_id, url, page):
+ title = self._fetch_title(page)
+ video_url = self._search_regex(self._URL_PATTERN, page, 'video url')
+
+ attrs = {
+ 'id': news_id,
+ 'title': title,
+ 'url': video_url, # ext can be inferred from url
+ 'thumbnail': self._fetch_thumbnail(page),
+ 'description': self._fetch_description(page),
+ }
+
+ timestamp = self._fetch_timestamp(page)
+ if timestamp:
+ attrs['timestamp'] = timestamp
+ else:
+ attrs['upload_date'] = self._fetch_upload_date(url)
+
+ return attrs
+
+ def _fetch_title(self, page):
+ return self._og_search_title(page)
+
+ def _fetch_thumbnail(self, page):
+ return self._og_search_thumbnail(page)
+
+ def _fetch_timestamp(self, page):
+ dateCreated = self._search_regex('"dateCreated":"([^"]+)"', page, 'created time')
+ return parse_iso8601(dateCreated)
+
+ def _fetch_upload_date(self, url):
+ return self._search_regex(self._VALID_URL, url, 'upload date', group='date')
+
+ def _fetch_description(self, page):
+ return self._og_search_property('description', page)
+
+
+class NextMediaActionNewsIE(NextMediaIE):
+ _VALID_URL = r'http://hk.dv.nextmedia.com/actionnews/[^/]+/(?P<date>\d+)/(?P<id>\d+)/\d+'
+ _TESTS = [{
+ 'url': 'http://hk.dv.nextmedia.com/actionnews/hit/20150121/19009428/20061460',
+ 'md5': '05fce8ffeed7a5e00665d4b7cf0f9201',
+ 'info_dict': {
+ 'id': '19009428',
+ 'ext': 'mp4',
+ 'title': '【壹週刊】細10年男友偷食 50歲邵美琪再失戀',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'description': 'md5:cd802fad1f40fd9ea178c1e2af02d659',
+ 'timestamp': 1421791200,
+ 'upload_date': '20150120',
+ }
+ }]
+
+ def _real_extract(self, url):
+ news_id = self._match_id(url)
+ actionnews_page = self._download_webpage(url, news_id)
+ article_url = self._og_search_url(actionnews_page)
+ article_page = self._download_webpage(article_url, news_id)
+ return self._extract_from_nextmedia_page(news_id, url, article_page)
+
+
+class AppleDailyRealtimeNewsIE(NextMediaIE):
+ _VALID_URL = r'http://(www|ent).appledaily.com.tw/(realtimenews|enews)/[^/]+/[^/]+/(?P<date>\d+)/(?P<id>\d+)(/.*)?'
+ _TESTS = [{
+ 'url': 'http://ent.appledaily.com.tw/enews/article/entertainment/20150128/36354694',
+ 'md5': 'a843ab23d150977cc55ef94f1e2c1e4d',
+ 'info_dict': {
+ 'id': '36354694',
+ 'ext': 'mp4',
+ 'title': '周亭羽走過摩鐵陰霾2男陪吃 九把刀孤寒看醫生',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'description': 'md5:b23787119933404ce515c6356a8c355c',
+ 'upload_date': '20150128',
+ }
+ }, {
+ 'url': 'http://www.appledaily.com.tw/realtimenews/article/strange/20150128/550549/%E4%B8%8D%E6%BB%BF%E8%A2%AB%E8%B8%A9%E8%85%B3%E3%80%80%E5%B1%B1%E6%9D%B1%E5%85%A9%E5%A4%A7%E5%AA%BD%E4%B8%80%E8%B7%AF%E6%89%93%E4%B8%8B%E8%BB%8A',
+ 'md5': '86b4e9132d158279c7883822d94ccc49',
+ 'info_dict': {
+ 'id': '550549',
+ 'ext': 'mp4',
+ 'title': '不滿被踩腳 山東兩大媽一路打下車',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'description': 'md5:2648aaf6fc4f401f6de35a91d111aa1d',
+ 'upload_date': '20150128',
+ }
+ }]
+
+ _URL_PATTERN = r'\{url: \'(.+)\'\}'
+
+ def _fetch_title(self, page):
+ return self._html_search_regex(r'<h1 id="h1">([^<>]+)</h1>', page, 'news title')
+
+ def _fetch_thumbnail(self, page):
+ return self._html_search_regex(r"setInitialImage\(\'([^']+)'\)", page, 'video thumbnail', fatal=False)
+
+ def _fetch_timestamp(self, page):
+ return None
+
+
+class AppleDailyAnimationNewsIE(AppleDailyRealtimeNewsIE):
+ _VALID_URL = 'http://www.appledaily.com.tw/animation/[^/]+/[^/]+/(?P<date>\d+)/(?P<id>\d+)(/.*)?'
+ _TESTS = [{
+ 'url': 'http://www.appledaily.com.tw/animation/realtimenews/new/20150128/5003671',
+ 'md5': '03df296d95dedc2d5886debbb80cb43f',
+ 'info_dict': {
+ 'id': '5003671',
+ 'ext': 'mp4',
+ 'title': '20正妹熱舞 《刀龍傳說Online》火辣上市',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'description': 'md5:23c0aac567dc08c9c16a3161a2c2e3cd',
+ 'upload_date': '20150128',
+ }
+ }, {
+ # No thumbnail
+ 'url': 'http://www.appledaily.com.tw/animation/realtimenews/new/20150128/5003673/',
+ 'md5': 'b06182cd386ea7bc6115ec7ff0f72aeb',
+ 'info_dict': {
+ 'id': '5003673',
+ 'ext': 'mp4',
+ 'title': '半夜尿尿 好像會看到___',
+ 'description': 'md5:61d2da7fe117fede148706cdb85ac066',
+ 'upload_date': '20150128',
+ },
+ 'expected_warnings': [
+ 'video thumbnail',
+ ]
+ }]
+
+ def _fetch_title(self, page):
+ return self._html_search_meta('description', page, 'news title')
+
+ def _fetch_description(self, page):
+ return self._html_search_meta('description', page, 'news description')
diff --git a/youtube_dl/extractor/srmediathek.py b/youtube_dl/extractor/srmediathek.py
index 666a7dcc8..5d583c720 100644
--- a/youtube_dl/extractor/srmediathek.py
+++ b/youtube_dl/extractor/srmediathek.py
@@ -8,7 +8,7 @@ from ..utils import js_to_json
class SRMediathekIE(InfoExtractor):
- IE_DESC = 'Süddeutscher Rundfunk'
+ IE_DESC = 'Saarländischer Rundfunk'
_VALID_URL = r'https?://sr-mediathek\.sr-online\.de/index\.php\?.*?&id=(?P<id>[0-9]+)'
_TEST = {
diff --git a/youtube_dl/extractor/viddler.py b/youtube_dl/extractor/viddler.py
index 0faa729c6..8516a2940 100644
--- a/youtube_dl/extractor/viddler.py
+++ b/youtube_dl/extractor/viddler.py
@@ -5,27 +5,58 @@ from ..utils import (
float_or_none,
int_or_none,
)
+from ..compat import (
+ compat_urllib_request
+)
class ViddlerIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?viddler\.com/(?:v|embed|player)/(?P<id>[a-z0-9]+)'
- _TEST = {
- "url": "http://www.viddler.com/v/43903784",
+ _TESTS = [{
+ 'url': 'http://www.viddler.com/v/43903784',
'md5': 'ae43ad7cb59431ce043f0ff7fa13cbf4',
'info_dict': {
'id': '43903784',
'ext': 'mp4',
- "title": "Video Made Easy",
- 'description': 'You don\'t need to be a professional to make high-quality video content. Viddler provides some quick and easy tips on how to produce great video content with limited resources. ',
- "uploader": "viddler",
+ 'title': 'Video Made Easy',
+ 'description': 'md5:6a697ebd844ff3093bd2e82c37b409cd',
+ 'uploader': 'viddler',
'timestamp': 1335371429,
'upload_date': '20120425',
- "duration": 100.89,
+ 'duration': 100.89,
'thumbnail': 're:^https?://.*\.jpg$',
'view_count': int,
+ 'comment_count': int,
'categories': ['video content', 'high quality video', 'video made easy', 'how to produce video with limited resources', 'viddler'],
}
- }
+ }, {
+ 'url': 'http://www.viddler.com/v/4d03aad9/',
+ 'md5': 'faa71fbf70c0bee7ab93076fd007f4b0',
+ 'info_dict': {
+ 'id': '4d03aad9',
+ 'ext': 'mp4',
+ 'title': 'WALL-TO-GORTAT',
+ 'upload_date': '20150126',
+ 'uploader': 'deadspin',
+ 'timestamp': 1422285291,
+ 'view_count': int,
+ 'comment_count': int,
+ }
+ }, {
+ 'url': 'http://www.viddler.com/player/221ebbbd/0/',
+ 'md5': '0defa2bd0ea613d14a6e9bd1db6be326',
+ 'info_dict': {
+ 'id': '221ebbbd',
+ 'ext': 'mp4',
+ 'title': 'LETeens-Grammar-snack-third-conditional',
+ 'description': ' ',
+ 'upload_date': '20140929',
+ 'uploader': 'BCLETeens',
+ 'timestamp': 1411997190,
+ 'view_count': int,
+ 'comment_count': int,
+ }
+ }]
def _real_extract(self, url):
video_id = self._match_id(url)
@@ -33,14 +64,17 @@ class ViddlerIE(InfoExtractor):
json_url = (
'http://api.viddler.com/api/v2/viddler.videos.getPlaybackDetails.json?video_id=%s&key=v0vhrt7bg2xq1vyxhkct' %
video_id)
- data = self._download_json(json_url, video_id)['video']
+ headers = {'Referer': 'http://static.cdn-ec.viddler.com/js/arpeggio/v2/embed.html'}
+ request = compat_urllib_request.Request(json_url, None, headers)
+ data = self._download_json(request, video_id)['video']
formats = []
for filed in data['files']:
if filed.get('status', 'ready') != 'ready':
continue
+ format_id = filed.get('profile_id') or filed['profile_name']
f = {
- 'format_id': filed['profile_id'],
+ 'format_id': format_id,
'format_note': filed['profile_name'],
'url': self._proto_relative_url(filed['url']),
'width': int_or_none(filed.get('width')),
@@ -53,16 +87,15 @@ class ViddlerIE(InfoExtractor):
if filed.get('cdn_url'):
f = f.copy()
- f['url'] = self._proto_relative_url(filed['cdn_url'])
- f['format_id'] = filed['profile_id'] + '-cdn'
+ f['url'] = self._proto_relative_url(filed['cdn_url'], 'http:')
+ f['format_id'] = format_id + '-cdn'
f['source_preference'] = 1
formats.append(f)
if filed.get('html5_video_source'):
f = f.copy()
- f['url'] = self._proto_relative_url(
- filed['html5_video_source'])
- f['format_id'] = filed['profile_id'] + '-html5'
+ f['url'] = self._proto_relative_url(filed['html5_video_source'])
+ f['format_id'] = format_id + '-html5'
f['source_preference'] = 0
formats.append(f)
self._sort_formats(formats)
@@ -71,7 +104,6 @@ class ViddlerIE(InfoExtractor):
t.get('text') for t in data.get('tags', []) if 'text' in t]
return {
- '_type': 'video',
'id': video_id,
'title': data['title'],
'formats': formats,
@@ -81,5 +113,6 @@ class ViddlerIE(InfoExtractor):
'uploader': data.get('author'),
'duration': float_or_none(data.get('length')),
'view_count': int_or_none(data.get('view_count')),
+ 'comment_count': int_or_none(data.get('comment_count')),
'categories': categories,
}
diff --git a/youtube_dl/extractor/xuite.py b/youtube_dl/extractor/xuite.py
new file mode 100644
index 000000000..4971965f9
--- /dev/null
+++ b/youtube_dl/extractor/xuite.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import base64
+
+from .common import InfoExtractor
+from ..compat import compat_urllib_parse_unquote
+from ..utils import (
+ ExtractorError,
+ parse_iso8601,
+ parse_duration,
+)
+
+
+class XuiteIE(InfoExtractor):
+ _REGEX_BASE64 = r'(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?'
+ _VALID_URL = r'https?://vlog\.xuite\.net/(?:play|embed)/(?P<id>%s)' % _REGEX_BASE64
+ _TESTS = [{
+ # Audio
+ 'url': 'http://vlog.xuite.net/play/RGkzc1ZULTM4NjA5MTQuZmx2',
+ 'md5': '63a42c705772aa53fd4c1a0027f86adf',
+ 'info_dict': {
+ 'id': '3860914',
+ 'ext': 'mp3',
+ 'title': '孤單南半球-歐德陽',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'duration': 247.246,
+ 'timestamp': 1314932940,
+ 'upload_date': '20110902',
+ 'uploader': '阿能',
+ 'uploader_id': '15973816',
+ 'categories': ['個人短片'],
+ },
+ }, {
+ # Video with only one format
+ 'url': 'http://vlog.xuite.net/play/TkRZNjhULTM0NDE2MjkuZmx2',
+ 'md5': 'c45737fc8ac5dc8ac2f92ecbcecf505e',
+ 'info_dict': {
+ 'id': '3441629',
+ 'ext': 'mp4',
+ 'title': '孫燕姿 - 眼淚成詩',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'duration': 217.399,
+ 'timestamp': 1299383640,
+ 'upload_date': '20110306',
+ 'uploader': 'Valen',
+ 'uploader_id': '10400126',
+ 'categories': ['影視娛樂'],
+ },
+ }, {
+ # Video with two formats
+ 'url': 'http://vlog.xuite.net/play/bWo1N1pLLTIxMzAxMTcwLmZsdg==',
+ 'md5': '1166e0f461efe55b62e26a2d2a68e6de',
+ 'info_dict': {
+ 'id': '21301170',
+ 'ext': 'mp4',
+ 'title': '暗殺教室 02',
+ 'description': '字幕:【極影字幕社】',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'duration': 1384.907,
+ 'timestamp': 1421481240,
+ 'upload_date': '20150117',
+ 'uploader': '我只是想認真點',
+ 'uploader_id': '242127761',
+ 'categories': ['電玩動漫'],
+ },
+ }, {
+ 'url': 'http://vlog.xuite.net/play/S1dDUjdyLTMyOTc3NjcuZmx2/%E5%AD%AB%E7%87%95%E5%A7%BF-%E7%9C%BC%E6%B7%9A%E6%88%90%E8%A9%A9',
+ 'only_matching': True,
+ }]
+
+ def _extract_flv_config(self, media_id):
+ base64_media_id = base64.b64encode(media_id.encode('utf-8')).decode('utf-8')
+ flv_config = self._download_xml(
+ 'http://vlog.xuite.net/flash/player?media=%s' % base64_media_id,
+ 'flv config')
+ prop_dict = {}
+ for prop in flv_config.findall('./property'):
+ prop_id = base64.b64decode(prop.attrib['id']).decode('utf-8')
+ # CDATA may be empty in flv config
+ if not prop.text:
+ continue
+ encoded_content = base64.b64decode(prop.text).decode('utf-8')
+ prop_dict[prop_id] = compat_urllib_parse_unquote(encoded_content)
+ return prop_dict
+
+ def _real_extract(self, url):
+ video_id = self._match_id(url)
+
+ webpage = self._download_webpage(url, video_id)
+
+ error_msg = self._search_regex(
+ r'<div id="error-message-content">([^<]+)',
+ webpage, 'error message', default=None)
+ if error_msg:
+ raise ExtractorError(
+ '%s returned error: %s' % (self.IE_NAME, error_msg),
+ expected=True)
+
+ video_id = self._html_search_regex(
+ r'data-mediaid="(\d+)"', webpage, 'media id')
+ flv_config = self._extract_flv_config(video_id)
+
+ FORMATS = {
+ 'audio': 'mp3',
+ 'video': 'mp4',
+ }
+
+ formats = []
+ for format_tag in ('src', 'hq_src'):
+ video_url = flv_config.get(format_tag)
+ if not video_url:
+ continue
+ format_id = self._search_regex(
+ r'\bq=(.+?)\b', video_url, 'format id', default=format_tag)
+ formats.append({
+ 'url': video_url,
+ 'ext': FORMATS.get(flv_config['type'], 'mp4'),
+ 'format_id': format_id,
+ 'height': int(format_id) if format_id.isnumeric() else None,
+ })
+ self._sort_formats(formats)
+
+ timestamp = flv_config.get('publish_datetime')
+ if timestamp:
+ timestamp = parse_iso8601(timestamp + ' +0800', ' ')
+
+ category = flv_config.get('category')
+ categories = [category] if category else []
+
+ return {
+ 'id': video_id,
+ 'title': flv_config['title'],
+ 'description': flv_config.get('description'),
+ 'thumbnail': flv_config.get('thumb'),
+ 'timestamp': timestamp,
+ 'uploader': flv_config.get('author_name'),
+ 'uploader_id': flv_config.get('author_id'),
+ 'duration': parse_duration(flv_config.get('duration')),
+ 'categories': categories,
+ 'formats': formats,
+ }