logo

youtube-dl

[mirror] Download/Watch videos from video hostersgit clone https://hacktivis.me/git/mirror/youtube-dl.git

hotstar.py (9449B)


  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import hashlib
  4. import hmac
  5. import json
  6. import re
  7. import time
  8. import uuid
  9. from .common import InfoExtractor
  10. from ..compat import (
  11. compat_HTTPError,
  12. compat_str,
  13. )
  14. from ..utils import (
  15. determine_ext,
  16. ExtractorError,
  17. int_or_none,
  18. str_or_none,
  19. try_get,
  20. url_or_none,
  21. )
  22. class HotStarBaseIE(InfoExtractor):
  23. _AKAMAI_ENCRYPTION_KEY = b'\x05\xfc\x1a\x01\xca\xc9\x4b\xc4\x12\xfc\x53\x12\x07\x75\xf9\xee'
  24. def _call_api_impl(self, path, video_id, headers, query, data=None):
  25. st = int(time.time())
  26. exp = st + 6000
  27. auth = 'st=%d~exp=%d~acl=/*' % (st, exp)
  28. auth += '~hmac=' + hmac.new(self._AKAMAI_ENCRYPTION_KEY, auth.encode(), hashlib.sha256).hexdigest()
  29. h = {'hotstarauth': auth}
  30. h.update(headers)
  31. return self._download_json(
  32. 'https://api.hotstar.com/' + path,
  33. video_id, headers=h, query=query, data=data)
  34. def _call_api(self, path, video_id, query_name='contentId'):
  35. response = self._call_api_impl(path, video_id, {
  36. 'x-country-code': 'IN',
  37. 'x-platform-code': 'JIO',
  38. }, {
  39. query_name: video_id,
  40. 'tas': 10000,
  41. })
  42. if response['statusCode'] != 'OK':
  43. raise ExtractorError(
  44. response['body']['message'], expected=True)
  45. return response['body']['results']
  46. def _call_api_v2(self, path, video_id, headers, query=None, data=None):
  47. h = {'X-Request-Id': compat_str(uuid.uuid4())}
  48. h.update(headers)
  49. try:
  50. return self._call_api_impl(
  51. path, video_id, h, query, data)
  52. except ExtractorError as e:
  53. if isinstance(e.cause, compat_HTTPError):
  54. if e.cause.code == 402:
  55. self.raise_login_required()
  56. message = self._parse_json(e.cause.read().decode(), video_id)['message']
  57. if message in ('Content not available in region', 'Country is not supported'):
  58. raise self.raise_geo_restricted(message)
  59. raise ExtractorError(message)
  60. raise e
  61. class HotStarIE(HotStarBaseIE):
  62. IE_NAME = 'hotstar'
  63. _VALID_URL = r'https?://(?:www\.)?hotstar\.com/(?:.+[/-])?(?P<id>\d{10})'
  64. _TESTS = [{
  65. # contentData
  66. 'url': 'https://www.hotstar.com/can-you-not-spread-rumours/1000076273',
  67. 'info_dict': {
  68. 'id': '1000076273',
  69. 'ext': 'mp4',
  70. 'title': 'Can You Not Spread Rumours?',
  71. 'description': 'md5:c957d8868e9bc793ccb813691cc4c434',
  72. 'timestamp': 1447248600,
  73. 'upload_date': '20151111',
  74. 'duration': 381,
  75. },
  76. 'params': {
  77. # m3u8 download
  78. 'skip_download': True,
  79. }
  80. }, {
  81. # contentDetail
  82. 'url': 'https://www.hotstar.com/movies/radha-gopalam/1000057157',
  83. 'only_matching': True,
  84. }, {
  85. 'url': 'http://www.hotstar.com/sports/cricket/rajitha-sizzles-on-debut-with-329/2001477583',
  86. 'only_matching': True,
  87. }, {
  88. 'url': 'http://www.hotstar.com/1000000515',
  89. 'only_matching': True,
  90. }, {
  91. # only available via api v2
  92. 'url': 'https://www.hotstar.com/tv/ek-bhram-sarvagun-sampanna/s-2116/janhvi-targets-suman/1000234847',
  93. 'only_matching': True,
  94. }, {
  95. 'url': 'https://www.hotstar.com/in/tv/start-music/1260005217/cooks-vs-comalis/1100039717',
  96. 'only_matching': True,
  97. }]
  98. _GEO_BYPASS = False
  99. _DEVICE_ID = None
  100. _USER_TOKEN = None
  101. def _real_extract(self, url):
  102. video_id = self._match_id(url)
  103. webpage = self._download_webpage(url, video_id)
  104. app_state = self._parse_json(self._search_regex(
  105. r'<script>window\.APP_STATE\s*=\s*({.+?})</script>',
  106. webpage, 'app state'), video_id)
  107. video_data = {}
  108. getters = list(
  109. lambda x, k=k: x['initialState']['content%s' % k]['content']
  110. for k in ('Data', 'Detail')
  111. )
  112. for v in app_state.values():
  113. content = try_get(v, getters, dict)
  114. if content and content.get('contentId') == video_id:
  115. video_data = content
  116. break
  117. title = video_data['title']
  118. if video_data.get('drmProtected'):
  119. raise ExtractorError('This video is DRM protected.', expected=True)
  120. headers = {'Referer': url}
  121. formats = []
  122. geo_restricted = False
  123. if not self._USER_TOKEN:
  124. self._DEVICE_ID = compat_str(uuid.uuid4())
  125. self._USER_TOKEN = self._call_api_v2('um/v3/users', video_id, {
  126. 'X-HS-Platform': 'PCTV',
  127. 'Content-Type': 'application/json',
  128. }, data=json.dumps({
  129. 'device_ids': [{
  130. 'id': self._DEVICE_ID,
  131. 'type': 'device_id',
  132. }],
  133. }).encode())['user_identity']
  134. playback_sets = self._call_api_v2(
  135. 'play/v2/playback/content/' + video_id, video_id, {
  136. 'X-HS-Platform': 'web',
  137. 'X-HS-AppVersion': '6.99.1',
  138. 'X-HS-UserToken': self._USER_TOKEN,
  139. }, query={
  140. 'device-id': self._DEVICE_ID,
  141. 'desired-config': 'encryption:plain',
  142. 'os-name': 'Windows',
  143. 'os-version': '10',
  144. })['data']['playBackSets']
  145. for playback_set in playback_sets:
  146. if not isinstance(playback_set, dict):
  147. continue
  148. format_url = url_or_none(playback_set.get('playbackUrl'))
  149. if not format_url:
  150. continue
  151. format_url = re.sub(
  152. r'(?<=//staragvod)(\d)', r'web\1', format_url)
  153. tags = str_or_none(playback_set.get('tagsCombination')) or ''
  154. if tags and 'encryption:plain' not in tags:
  155. continue
  156. ext = determine_ext(format_url)
  157. try:
  158. if 'package:hls' in tags or ext == 'm3u8':
  159. formats.extend(self._extract_m3u8_formats(
  160. format_url, video_id, 'mp4',
  161. entry_protocol='m3u8_native',
  162. m3u8_id='hls', headers=headers))
  163. elif 'package:dash' in tags or ext == 'mpd':
  164. formats.extend(self._extract_mpd_formats(
  165. format_url, video_id, mpd_id='dash', headers=headers))
  166. elif ext == 'f4m':
  167. # produce broken files
  168. pass
  169. else:
  170. formats.append({
  171. 'url': format_url,
  172. 'width': int_or_none(playback_set.get('width')),
  173. 'height': int_or_none(playback_set.get('height')),
  174. })
  175. except ExtractorError as e:
  176. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
  177. geo_restricted = True
  178. continue
  179. if not formats and geo_restricted:
  180. self.raise_geo_restricted(countries=['IN'])
  181. self._sort_formats(formats)
  182. for f in formats:
  183. f.setdefault('http_headers', {}).update(headers)
  184. image = try_get(video_data, lambda x: x['image']['h'], compat_str)
  185. return {
  186. 'id': video_id,
  187. 'title': title,
  188. 'thumbnail': 'https://img1.hotstarext.com/image/upload/' + image if image else None,
  189. 'description': video_data.get('description'),
  190. 'duration': int_or_none(video_data.get('duration')),
  191. 'timestamp': int_or_none(video_data.get('broadcastDate') or video_data.get('startDate')),
  192. 'formats': formats,
  193. 'channel': video_data.get('channelName'),
  194. 'channel_id': str_or_none(video_data.get('channelId')),
  195. 'series': video_data.get('showName'),
  196. 'season': video_data.get('seasonName'),
  197. 'season_number': int_or_none(video_data.get('seasonNo')),
  198. 'season_id': str_or_none(video_data.get('seasonId')),
  199. 'episode': title,
  200. 'episode_number': int_or_none(video_data.get('episodeNo')),
  201. }
  202. class HotStarPlaylistIE(HotStarBaseIE):
  203. IE_NAME = 'hotstar:playlist'
  204. _VALID_URL = r'https?://(?:www\.)?hotstar\.com/(?:[a-z]{2}/)?tv/[^/]+/s-\w+/list/[^/]+/t-(?P<id>\w+)'
  205. _TESTS = [{
  206. 'url': 'https://www.hotstar.com/tv/savdhaan-india/s-26/list/popular-clips/t-3_2_26',
  207. 'info_dict': {
  208. 'id': '3_2_26',
  209. },
  210. 'playlist_mincount': 20,
  211. }, {
  212. 'url': 'https://www.hotstar.com/tv/savdhaan-india/s-26/list/extras/t-2480',
  213. 'only_matching': True,
  214. }, {
  215. 'url': 'https://www.hotstar.com/us/tv/masterchef-india/s-830/list/episodes/t-1_2_830',
  216. 'only_matching': True,
  217. }]
  218. def _real_extract(self, url):
  219. playlist_id = self._match_id(url)
  220. collection = self._call_api('o/v1/tray/find', playlist_id, 'uqId')
  221. entries = [
  222. self.url_result(
  223. 'https://www.hotstar.com/%s' % video['contentId'],
  224. ie=HotStarIE.ie_key(), video_id=video['contentId'])
  225. for video in collection['assets']['items']
  226. if video.get('contentId')]
  227. return self.playlist_result(entries, playlist_id)