aboutsummaryrefslogtreecommitdiff
path: root/youtube_dl/extractor/turner.py
diff options
context:
space:
mode:
Diffstat (limited to 'youtube_dl/extractor/turner.py')
-rw-r--r--youtube_dl/extractor/turner.py155
1 files changed, 118 insertions, 37 deletions
diff --git a/youtube_dl/extractor/turner.py b/youtube_dl/extractor/turner.py
index 1c0be9fc6..81229a54b 100644
--- a/youtube_dl/extractor/turner.py
+++ b/youtube_dl/extractor/turner.py
@@ -6,41 +6,77 @@ import re
from .adobepass import AdobePassIE
from ..compat import compat_str
from ..utils import (
+ fix_xml_ampersands,
xpath_text,
int_or_none,
determine_ext,
+ float_or_none,
parse_duration,
xpath_attr,
update_url_query,
ExtractorError,
+ strip_or_none,
+ url_or_none,
)
class TurnerBaseIE(AdobePassIE):
+ _AKAMAI_SPE_TOKEN_CACHE = {}
+
def _extract_timestamp(self, video_data):
return int_or_none(xpath_attr(video_data, 'dateCreated', 'uts'))
- def _extract_cvp_info(self, data_src, video_id, path_data={}, ap_data={}):
- video_data = self._download_xml(data_src, video_id)
+ def _add_akamai_spe_token(self, tokenizer_src, video_url, content_id, ap_data, custom_tokenizer_query=None):
+ secure_path = self._search_regex(r'https?://[^/]+(.+/)', video_url, 'secure path') + '*'
+ token = self._AKAMAI_SPE_TOKEN_CACHE.get(secure_path)
+ if not token:
+ query = {
+ 'path': secure_path,
+ }
+ if custom_tokenizer_query:
+ query.update(custom_tokenizer_query)
+ else:
+ query['videoId'] = content_id
+ if ap_data.get('auth_required'):
+ query['accessToken'] = self._extract_mvpd_auth(ap_data['url'], content_id, ap_data['site_name'], ap_data['site_name'])
+ auth = self._download_xml(
+ tokenizer_src, content_id, query=query)
+ error_msg = xpath_text(auth, 'error/msg')
+ if error_msg:
+ raise ExtractorError(error_msg, expected=True)
+ token = xpath_text(auth, 'token')
+ if not token:
+ return video_url
+ self._AKAMAI_SPE_TOKEN_CACHE[secure_path] = token
+ return video_url + '?hdnea=' + token
+
+ def _extract_cvp_info(self, data_src, video_id, path_data={}, ap_data={}, fatal=False):
+ video_data = self._download_xml(
+ data_src, video_id,
+ transform_source=lambda s: fix_xml_ampersands(s).strip(),
+ fatal=fatal)
+ if not video_data:
+ return {}
video_id = video_data.attrib['id']
title = xpath_text(video_data, 'headline', fatal=True)
content_id = xpath_text(video_data, 'contentId') or video_id
# rtmp_src = xpath_text(video_data, 'akamai/src')
# if rtmp_src:
- # splited_rtmp_src = rtmp_src.split(',')
- # if len(splited_rtmp_src) == 2:
- # rtmp_src = splited_rtmp_src[1]
+ # split_rtmp_src = rtmp_src.split(',')
+ # if len(split_rtmp_src) == 2:
+ # rtmp_src = split_rtmp_src[1]
# aifp = xpath_text(video_data, 'akamai/aifp', default='')
- tokens = {}
urls = []
formats = []
+ thumbnails = []
+ subtitles = {}
rex = re.compile(
r'(?P<width>[0-9]+)x(?P<height>[0-9]+)(?:_(?P<bitrate>[0-9]+))?')
# Possible formats locations: files/file, files/groupFiles/files
# and maybe others
for video_file in video_data.findall('.//file'):
- video_url = video_file.text.strip()
+ video_url = url_or_none(video_file.text.strip())
if not video_url:
continue
ext = determine_ext(video_url)
@@ -66,26 +102,10 @@ class TurnerBaseIE(AdobePassIE):
secure_path_data = path_data.get('secure')
if not secure_path_data:
continue
- video_url = secure_path_data['media_src'] + video_url
- secure_path = self._search_regex(r'https?://[^/]+(.+/)', video_url, 'secure path') + '*'
- token = tokens.get(secure_path)
- if not token:
- query = {
- 'path': secure_path,
- 'videoId': content_id,
- }
- if ap_data.get('auth_required'):
- query['accessToken'] = self._extract_mvpd_auth(ap_data['url'], video_id, ap_data['site_name'], ap_data['site_name'])
- auth = self._download_xml(
- secure_path_data['tokenizer_src'], video_id, query=query)
- error_msg = xpath_text(auth, 'error/msg')
- if error_msg:
- raise ExtractorError(error_msg, expected=True)
- token = xpath_text(auth, 'token')
- if not token:
- continue
- tokens[secure_path] = token
- video_url = video_url + '?hdnea=' + token
+ video_url = self._add_akamai_spe_token(
+ secure_path_data['tokenizer_src'],
+ secure_path_data['media_src'] + video_url,
+ content_id, ap_data)
elif not re.match('https?://', video_url):
base_path_data = path_data.get(ext, path_data.get('default', {}))
media_src = base_path_data.get('media_src')
@@ -96,9 +116,28 @@ class TurnerBaseIE(AdobePassIE):
continue
urls.append(video_url)
format_id = video_file.get('bitrate')
- if ext == 'smil':
+ if ext in ('scc', 'srt', 'vtt'):
+ subtitles.setdefault('en', []).append({
+ 'ext': ext,
+ 'url': video_url,
+ })
+ elif ext == 'png':
+ thumbnails.append({
+ 'id': format_id,
+ 'url': video_url,
+ })
+ elif ext == 'smil':
formats.extend(self._extract_smil_formats(
video_url, video_id, fatal=False))
+ elif re.match(r'https?://[^/]+\.akamaihd\.net/[iz]/', video_url):
+ formats.extend(self._extract_akamai_formats(
+ video_url, video_id, {
+ 'hds': path_data.get('f4m', {}).get('host'),
+ # nba.cdn.turner.com, ht.cdn.turner.com, ht2.cdn.turner.com
+ # ht3.cdn.turner.com, i.cdn.turner.com, s.cdn.turner.com
+ # ssl.cdn.turner.com
+ 'http': 'pmd.cdn.turner.com',
+ }))
elif ext == 'm3u8':
m3u8_formats = self._extract_m3u8_formats(
video_url, video_id, 'mp4',
@@ -117,7 +156,7 @@ class TurnerBaseIE(AdobePassIE):
'url': video_url,
'ext': ext,
}
- mobj = rex.search(format_id + video_url)
+ mobj = rex.search(video_url)
if mobj:
f.update({
'width': int(mobj.group('width')),
@@ -140,11 +179,10 @@ class TurnerBaseIE(AdobePassIE):
formats.append(f)
self._sort_formats(formats)
- subtitles = {}
for source in video_data.findall('closedCaptions/source'):
for track in source.findall('track'):
- track_url = track.get('url')
- if not isinstance(track_url, compat_str) or track_url.endswith('/big'):
+ track_url = url_or_none(track.get('url'))
+ if not track_url or track_url.endswith('/big'):
continue
lang = track.get('lang') or track.get('label') or 'en'
subtitles.setdefault(lang, []).append({
@@ -156,24 +194,67 @@ class TurnerBaseIE(AdobePassIE):
}.get(source.get('format'))
})
- thumbnails = [{
- 'id': image.get('cut'),
+ thumbnails.extend({
+ 'id': image.get('cut') or image.get('name'),
'url': image.text,
'width': int_or_none(image.get('width')),
'height': int_or_none(image.get('height')),
- } for image in video_data.findall('images/image')]
+ } for image in video_data.findall('images/image'))
+
+ is_live = xpath_text(video_data, 'isLive') == 'true'
return {
'id': video_id,
- 'title': title,
+ 'title': self._live_title(title) if is_live else title,
'formats': formats,
'subtitles': subtitles,
'thumbnails': thumbnails,
- 'description': xpath_text(video_data, 'description'),
+ 'thumbnail': xpath_text(video_data, 'poster'),
+ 'description': strip_or_none(xpath_text(video_data, 'description')),
'duration': parse_duration(xpath_text(video_data, 'length') or xpath_text(video_data, 'trt')),
'timestamp': self._extract_timestamp(video_data),
'upload_date': xpath_attr(video_data, 'metas', 'version'),
'series': xpath_text(video_data, 'showTitle'),
'season_number': int_or_none(xpath_text(video_data, 'seasonNumber')),
'episode_number': int_or_none(xpath_text(video_data, 'episodeNumber')),
+ 'is_live': is_live,
+ }
+
+ def _extract_ngtv_info(self, media_id, tokenizer_query, ap_data=None):
+ streams_data = self._download_json(
+ 'http://medium.ngtv.io/media/%s/tv' % media_id,
+ media_id)['media']['tv']
+ duration = None
+ chapters = []
+ formats = []
+ for supported_type in ('unprotected', 'bulkaes'):
+ stream_data = streams_data.get(supported_type, {})
+ m3u8_url = stream_data.get('secureUrl') or stream_data.get('url')
+ if not m3u8_url:
+ continue
+ if stream_data.get('playlistProtection') == 'spe':
+ m3u8_url = self._add_akamai_spe_token(
+ 'http://token.ngtv.io/token/token_spe',
+ m3u8_url, media_id, ap_data or {}, tokenizer_query)
+ formats.extend(self._extract_m3u8_formats(
+ m3u8_url, media_id, 'mp4', m3u8_id='hls', fatal=False))
+
+ duration = float_or_none(stream_data.get('totalRuntime'))
+
+ if not chapters:
+ for chapter in stream_data.get('contentSegments', []):
+ start_time = float_or_none(chapter.get('start'))
+ chapter_duration = float_or_none(chapter.get('duration'))
+ if start_time is None or chapter_duration is None:
+ continue
+ chapters.append({
+ 'start_time': start_time,
+ 'end_time': start_time + chapter_duration,
+ })
+ self._sort_formats(formats)
+
+ return {
+ 'formats': formats,
+ 'chapters': chapters,
+ 'duration': duration,
}