You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
13 KiB

  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import re
  4. from .common import InfoExtractor
  5. from ..compat import compat_HTTPError
  6. from ..utils import (
  7. determine_ext,
  8. float_or_none,
  9. int_or_none,
  10. unsmuggle_url,
  11. ExtractorError,
  12. )
  13. class LimelightBaseIE(InfoExtractor):
  14. _PLAYLIST_SERVICE_URL = 'http://production-ps.lvp.llnw.net/r/PlaylistService/%s/%s/%s'
  15. _API_URL = 'http://api.video.limelight.com/rest/organizations/%s/%s/%s/%s.json'
  16. def _call_playlist_service(self, item_id, method, fatal=True, referer=None):
  17. headers = {}
  18. if referer:
  19. headers['Referer'] = referer
  20. try:
  21. return self._download_json(
  22. self._PLAYLIST_SERVICE_URL % (self._PLAYLIST_SERVICE_PATH, item_id, method),
  23. item_id, 'Downloading PlaylistService %s JSON' % method, fatal=fatal, headers=headers)
  24. except ExtractorError as e:
  25. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
  26. error = self._parse_json(e.cause.read().decode(), item_id)['detail']['contentAccessPermission']
  27. if error == 'CountryDisabled':
  28. self.raise_geo_restricted()
  29. raise ExtractorError(error, expected=True)
  30. raise
  31. def _call_api(self, organization_id, item_id, method):
  32. return self._download_json(
  33. self._API_URL % (organization_id, self._API_PATH, item_id, method),
  34. item_id, 'Downloading API %s JSON' % method)
  35. def _extract(self, item_id, pc_method, mobile_method, meta_method, referer=None):
  36. pc = self._call_playlist_service(item_id, pc_method, referer=referer)
  37. metadata = self._call_api(pc['orgId'], item_id, meta_method)
  38. mobile = self._call_playlist_service(item_id, mobile_method, fatal=False, referer=referer)
  39. return pc, mobile, metadata
  40. def _extract_info(self, streams, mobile_urls, properties):
  41. video_id = properties['media_id']
  42. formats = []
  43. urls = []
  44. for stream in streams:
  45. stream_url = stream.get('url')
  46. if not stream_url or stream.get('drmProtected') or stream_url in urls:
  47. continue
  48. urls.append(stream_url)
  49. ext = determine_ext(stream_url)
  50. if ext == 'f4m':
  51. formats.extend(self._extract_f4m_formats(
  52. stream_url, video_id, f4m_id='hds', fatal=False))
  53. else:
  54. fmt = {
  55. 'url': stream_url,
  56. 'abr': float_or_none(stream.get('audioBitRate')),
  57. 'fps': float_or_none(stream.get('videoFrameRate')),
  58. 'ext': ext,
  59. }
  60. width = int_or_none(stream.get('videoWidthInPixels'))
  61. height = int_or_none(stream.get('videoHeightInPixels'))
  62. vbr = float_or_none(stream.get('videoBitRate'))
  63. if width or height or vbr:
  64. fmt.update({
  65. 'width': width,
  66. 'height': height,
  67. 'vbr': vbr,
  68. })
  69. else:
  70. fmt['vcodec'] = 'none'
  71. rtmp = re.search(r'^(?P<url>rtmpe?://(?P<host>[^/]+)/(?P<app>.+))/(?P<playpath>mp[34]:.+)$', stream_url)
  72. if rtmp:
  73. format_id = 'rtmp'
  74. if stream.get('videoBitRate'):
  75. format_id += '-%d' % int_or_none(stream['videoBitRate'])
  76. http_format_id = format_id.replace('rtmp', 'http')
  77. CDN_HOSTS = (
  78. ('delvenetworks.com', 'cpl.delvenetworks.com'),
  79. ('video.llnw.net', 's2.content.video.llnw.net'),
  80. )
  81. for cdn_host, http_host in CDN_HOSTS:
  82. if cdn_host not in rtmp.group('host').lower():
  83. continue
  84. http_url = 'http://%s/%s' % (http_host, rtmp.group('playpath')[4:])
  85. urls.append(http_url)
  86. if self._is_valid_url(http_url, video_id, http_format_id):
  87. http_fmt = fmt.copy()
  88. http_fmt.update({
  89. 'url': http_url,
  90. 'format_id': http_format_id,
  91. })
  92. formats.append(http_fmt)
  93. break
  94. fmt.update({
  95. 'url': rtmp.group('url'),
  96. 'play_path': rtmp.group('playpath'),
  97. 'app': rtmp.group('app'),
  98. 'ext': 'flv',
  99. 'format_id': format_id,
  100. })
  101. formats.append(fmt)
  102. for mobile_url in mobile_urls:
  103. media_url = mobile_url.get('mobileUrl')
  104. format_id = mobile_url.get('targetMediaPlatform')
  105. if not media_url or format_id in ('Widevine', 'SmoothStreaming') or media_url in urls:
  106. continue
  107. urls.append(media_url)
  108. ext = determine_ext(media_url)
  109. if ext == 'm3u8':
  110. formats.extend(self._extract_m3u8_formats(
  111. media_url, video_id, 'mp4', 'm3u8_native',
  112. m3u8_id=format_id, fatal=False))
  113. elif ext == 'f4m':
  114. formats.extend(self._extract_f4m_formats(
  115. stream_url, video_id, f4m_id=format_id, fatal=False))
  116. else:
  117. formats.append({
  118. 'url': media_url,
  119. 'format_id': format_id,
  120. 'preference': -1,
  121. 'ext': ext,
  122. })
  123. self._sort_formats(formats)
  124. title = properties['title']
  125. description = properties.get('description')
  126. timestamp = int_or_none(properties.get('publish_date') or properties.get('create_date'))
  127. duration = float_or_none(properties.get('duration_in_milliseconds'), 1000)
  128. filesize = int_or_none(properties.get('total_storage_in_bytes'))
  129. categories = [properties.get('category')]
  130. tags = properties.get('tags', [])
  131. thumbnails = [{
  132. 'url': thumbnail['url'],
  133. 'width': int_or_none(thumbnail.get('width')),
  134. 'height': int_or_none(thumbnail.get('height')),
  135. } for thumbnail in properties.get('thumbnails', []) if thumbnail.get('url')]
  136. subtitles = {}
  137. for caption in properties.get('captions', []):
  138. lang = caption.get('language_code')
  139. subtitles_url = caption.get('url')
  140. if lang and subtitles_url:
  141. subtitles.setdefault(lang, []).append({
  142. 'url': subtitles_url,
  143. })
  144. closed_captions_url = properties.get('closed_captions_url')
  145. if closed_captions_url:
  146. subtitles.setdefault('en', []).append({
  147. 'url': closed_captions_url,
  148. 'ext': 'ttml',
  149. })
  150. return {
  151. 'id': video_id,
  152. 'title': title,
  153. 'description': description,
  154. 'formats': formats,
  155. 'timestamp': timestamp,
  156. 'duration': duration,
  157. 'filesize': filesize,
  158. 'categories': categories,
  159. 'tags': tags,
  160. 'thumbnails': thumbnails,
  161. 'subtitles': subtitles,
  162. }
  163. class LimelightMediaIE(LimelightBaseIE):
  164. IE_NAME = 'limelight'
  165. _VALID_URL = r'''(?x)
  166. (?:
  167. limelight:media:|
  168. https?://
  169. (?:
  170. link\.videoplatform\.limelight\.com/media/|
  171. assets\.delvenetworks\.com/player/loader\.swf
  172. )
  173. \?.*?\bmediaId=
  174. )
  175. (?P<id>[a-z0-9]{32})
  176. '''
  177. _TESTS = [{
  178. 'url': 'http://link.videoplatform.limelight.com/media/?mediaId=3ffd040b522b4485b6d84effc750cd86',
  179. 'info_dict': {
  180. 'id': '3ffd040b522b4485b6d84effc750cd86',
  181. 'ext': 'mp4',
  182. 'title': 'HaP and the HB Prince Trailer',
  183. 'description': 'md5:8005b944181778e313d95c1237ddb640',
  184. 'thumbnail': r're:^https?://.*\.jpeg$',
  185. 'duration': 144.23,
  186. 'timestamp': 1244136834,
  187. 'upload_date': '20090604',
  188. },
  189. 'params': {
  190. # m3u8 download
  191. 'skip_download': True,
  192. },
  193. }, {
  194. # video with subtitles
  195. 'url': 'limelight:media:a3e00274d4564ec4a9b29b9466432335',
  196. 'md5': '2fa3bad9ac321e23860ca23bc2c69e3d',
  197. 'info_dict': {
  198. 'id': 'a3e00274d4564ec4a9b29b9466432335',
  199. 'ext': 'mp4',
  200. 'title': '3Play Media Overview Video',
  201. 'thumbnail': r're:^https?://.*\.jpeg$',
  202. 'duration': 78.101,
  203. 'timestamp': 1338929955,
  204. 'upload_date': '20120605',
  205. 'subtitles': 'mincount:9',
  206. },
  207. }, {
  208. 'url': 'https://assets.delvenetworks.com/player/loader.swf?mediaId=8018a574f08d416e95ceaccae4ba0452',
  209. 'only_matching': True,
  210. }]
  211. _PLAYLIST_SERVICE_PATH = 'media'
  212. _API_PATH = 'media'
  213. def _real_extract(self, url):
  214. url, smuggled_data = unsmuggle_url(url, {})
  215. video_id = self._match_id(url)
  216. self._initialize_geo_bypass(smuggled_data.get('geo_countries'))
  217. pc, mobile, metadata = self._extract(
  218. video_id, 'getPlaylistByMediaId',
  219. 'getMobilePlaylistByMediaId', 'properties',
  220. smuggled_data.get('source_url'))
  221. return self._extract_info(
  222. pc['playlistItems'][0].get('streams', []),
  223. mobile['mediaList'][0].get('mobileUrls', []) if mobile else [],
  224. metadata)
  225. class LimelightChannelIE(LimelightBaseIE):
  226. IE_NAME = 'limelight:channel'
  227. _VALID_URL = r'''(?x)
  228. (?:
  229. limelight:channel:|
  230. https?://
  231. (?:
  232. link\.videoplatform\.limelight\.com/media/|
  233. assets\.delvenetworks\.com/player/loader\.swf
  234. )
  235. \?.*?\bchannelId=
  236. )
  237. (?P<id>[a-z0-9]{32})
  238. '''
  239. _TESTS = [{
  240. 'url': 'http://link.videoplatform.limelight.com/media/?channelId=ab6a524c379342f9b23642917020c082',
  241. 'info_dict': {
  242. 'id': 'ab6a524c379342f9b23642917020c082',
  243. 'title': 'Javascript Sample Code',
  244. },
  245. 'playlist_mincount': 3,
  246. }, {
  247. 'url': 'http://assets.delvenetworks.com/player/loader.swf?channelId=ab6a524c379342f9b23642917020c082',
  248. 'only_matching': True,
  249. }]
  250. _PLAYLIST_SERVICE_PATH = 'channel'
  251. _API_PATH = 'channels'
  252. def _real_extract(self, url):
  253. url, smuggled_data = unsmuggle_url(url, {})
  254. channel_id = self._match_id(url)
  255. pc, mobile, medias = self._extract(
  256. channel_id, 'getPlaylistByChannelId',
  257. 'getMobilePlaylistWithNItemsByChannelId?begin=0&count=-1',
  258. 'media', smuggled_data.get('source_url'))
  259. entries = [
  260. self._extract_info(
  261. pc['playlistItems'][i].get('streams', []),
  262. mobile['mediaList'][i].get('mobileUrls', []) if mobile else [],
  263. medias['media_list'][i])
  264. for i in range(len(medias['media_list']))]
  265. return self.playlist_result(entries, channel_id, pc['title'])
  266. class LimelightChannelListIE(LimelightBaseIE):
  267. IE_NAME = 'limelight:channel_list'
  268. _VALID_URL = r'''(?x)
  269. (?:
  270. limelight:channel_list:|
  271. https?://
  272. (?:
  273. link\.videoplatform\.limelight\.com/media/|
  274. assets\.delvenetworks\.com/player/loader\.swf
  275. )
  276. \?.*?\bchannelListId=
  277. )
  278. (?P<id>[a-z0-9]{32})
  279. '''
  280. _TESTS = [{
  281. 'url': 'http://link.videoplatform.limelight.com/media/?channelListId=301b117890c4465c8179ede21fd92e2b',
  282. 'info_dict': {
  283. 'id': '301b117890c4465c8179ede21fd92e2b',
  284. 'title': 'Website - Hero Player',
  285. },
  286. 'playlist_mincount': 2,
  287. }, {
  288. 'url': 'https://assets.delvenetworks.com/player/loader.swf?channelListId=301b117890c4465c8179ede21fd92e2b',
  289. 'only_matching': True,
  290. }]
  291. _PLAYLIST_SERVICE_PATH = 'channel_list'
  292. def _real_extract(self, url):
  293. channel_list_id = self._match_id(url)
  294. channel_list = self._call_playlist_service(channel_list_id, 'getMobileChannelListById')
  295. entries = [
  296. self.url_result('limelight:channel:%s' % channel['id'], 'LimelightChannel')
  297. for channel in channel_list['channelList']]
  298. return self.playlist_result(entries, channel_list_id, channel_list['title'])