logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

hotstar.py (18125B)


  1. import hashlib
  2. import hmac
  3. import json
  4. import re
  5. import time
  6. import uuid
  7. from .common import InfoExtractor
  8. from ..networking.exceptions import HTTPError
  9. from ..utils import (
  10. ExtractorError,
  11. determine_ext,
  12. int_or_none,
  13. join_nonempty,
  14. str_or_none,
  15. traverse_obj,
  16. url_or_none,
  17. )
  18. class HotStarBaseIE(InfoExtractor):
  19. _BASE_URL = 'https://www.hotstar.com'
  20. _API_URL = 'https://api.hotstar.com'
  21. _AKAMAI_ENCRYPTION_KEY = b'\x05\xfc\x1a\x01\xca\xc9\x4b\xc4\x12\xfc\x53\x12\x07\x75\xf9\xee'
  22. def _call_api_v1(self, path, *args, **kwargs):
  23. return self._download_json(
  24. f'{self._API_URL}/o/v1/{path}', *args, **kwargs,
  25. headers={'x-country-code': 'IN', 'x-platform-code': 'PCTV'})
  26. def _call_api_impl(self, path, video_id, query, st=None, cookies=None):
  27. st = int_or_none(st) or int(time.time())
  28. exp = st + 6000
  29. auth = f'st={st}~exp={exp}~acl=/*'
  30. auth += '~hmac=' + hmac.new(self._AKAMAI_ENCRYPTION_KEY, auth.encode(), hashlib.sha256).hexdigest()
  31. if cookies and cookies.get('userUP'):
  32. token = cookies.get('userUP').value
  33. else:
  34. token = self._download_json(
  35. f'{self._API_URL}/um/v3/users',
  36. video_id, note='Downloading token',
  37. data=json.dumps({'device_ids': [{'id': str(uuid.uuid4()), 'type': 'device_id'}]}).encode(),
  38. headers={
  39. 'hotstarauth': auth,
  40. 'x-hs-platform': 'PCTV', # or 'web'
  41. 'Content-Type': 'application/json',
  42. })['user_identity']
  43. response = self._download_json(
  44. f'{self._API_URL}/{path}', video_id, query=query,
  45. headers={
  46. 'hotstarauth': auth,
  47. 'x-hs-appversion': '6.72.2',
  48. 'x-hs-platform': 'web',
  49. 'x-hs-usertoken': token,
  50. })
  51. if response['message'] != "Playback URL's fetched successfully":
  52. raise ExtractorError(
  53. response['message'], expected=True)
  54. return response['data']
  55. def _call_api_v2(self, path, video_id, st=None, cookies=None):
  56. return self._call_api_impl(
  57. f'{path}/content/{video_id}', video_id, st=st, cookies=cookies, query={
  58. 'desired-config': 'audio_channel:stereo|container:fmp4|dynamic_range:hdr|encryption:plain|ladder:tv|package:dash|resolution:fhd|subs-tag:HotstarVIP|video_codec:h265',
  59. 'device-id': cookies.get('device_id').value if cookies.get('device_id') else str(uuid.uuid4()),
  60. 'os-name': 'Windows',
  61. 'os-version': '10',
  62. })
  63. def _playlist_entries(self, path, item_id, root=None, **kwargs):
  64. results = self._call_api_v1(path, item_id, **kwargs)['body']['results']
  65. for video in traverse_obj(results, (('assets', None), 'items', ...)):
  66. if video.get('contentId'):
  67. yield self.url_result(
  68. HotStarIE._video_url(video['contentId'], root=root), HotStarIE, video['contentId'])
  69. class HotStarIE(HotStarBaseIE):
  70. IE_NAME = 'hotstar'
  71. _VALID_URL = r'''(?x)
  72. https?://(?:www\.)?hotstar\.com(?:/in)?/(?!in/)
  73. (?:
  74. (?P<type>movies|sports|clips|episode|(?P<tv>tv|shows))/
  75. (?(tv)(?:[^/?#]+/){2}|[^?#]*)
  76. )?
  77. [^/?#]+/
  78. (?P<id>\d{10})
  79. '''
  80. _TESTS = [{
  81. 'url': 'https://www.hotstar.com/can-you-not-spread-rumours/1000076273',
  82. 'info_dict': {
  83. 'id': '1000076273',
  84. 'ext': 'mp4',
  85. 'title': 'Can You Not Spread Rumours?',
  86. 'description': 'md5:c957d8868e9bc793ccb813691cc4c434',
  87. 'timestamp': 1447248600,
  88. 'upload_date': '20151111',
  89. 'duration': 381,
  90. 'episode': 'Can You Not Spread Rumours?',
  91. },
  92. 'params': {'skip_download': 'm3u8'},
  93. }, {
  94. 'url': 'https://www.hotstar.com/tv/ek-bhram-sarvagun-sampanna/s-2116/janhvi-targets-suman/1000234847',
  95. 'info_dict': {
  96. 'id': '1000234847',
  97. 'ext': 'mp4',
  98. 'title': 'Janhvi Targets Suman',
  99. 'description': 'md5:78a85509348910bd1ca31be898c5796b',
  100. 'timestamp': 1556670600,
  101. 'upload_date': '20190501',
  102. 'duration': 1219,
  103. 'channel': 'StarPlus',
  104. 'channel_id': '3',
  105. 'series': 'Ek Bhram - Sarvagun Sampanna',
  106. 'season': 'Chapter 1',
  107. 'season_number': 1,
  108. 'season_id': '6771',
  109. 'episode': 'Janhvi Targets Suman',
  110. 'episode_number': 8,
  111. },
  112. }, {
  113. 'url': 'https://www.hotstar.com/in/shows/anupama/1260022017/anupama-anuj-share-a-moment/1000282843',
  114. 'info_dict': {
  115. 'id': '1000282843',
  116. 'ext': 'mp4',
  117. 'title': 'Anupama, Anuj Share a Moment',
  118. 'season': 'Chapter 1',
  119. 'description': 'md5:8d74ed2248423b8b06d5c8add4d7a0c0',
  120. 'timestamp': 1678149000,
  121. 'channel': 'StarPlus',
  122. 'series': 'Anupama',
  123. 'season_number': 1,
  124. 'season_id': '7399',
  125. 'upload_date': '20230307',
  126. 'episode': 'Anupama, Anuj Share a Moment',
  127. 'episode_number': 853,
  128. 'duration': 1272,
  129. 'channel_id': '3',
  130. },
  131. 'skip': 'HTTP Error 504: Gateway Time-out', # XXX: Investigate 504 errors on some episodes
  132. }, {
  133. 'url': 'https://www.hotstar.com/in/shows/kana-kaanum-kaalangal/1260097087/back-to-school/1260097320',
  134. 'info_dict': {
  135. 'id': '1260097320',
  136. 'ext': 'mp4',
  137. 'title': 'Back To School',
  138. 'season': 'Chapter 1',
  139. 'description': 'md5:b0d6a4c8a650681491e7405496fc7e13',
  140. 'timestamp': 1650564000,
  141. 'channel': 'Hotstar Specials',
  142. 'series': 'Kana Kaanum Kaalangal',
  143. 'season_number': 1,
  144. 'season_id': '9441',
  145. 'upload_date': '20220421',
  146. 'episode': 'Back To School',
  147. 'episode_number': 1,
  148. 'duration': 1810,
  149. 'channel_id': '54',
  150. },
  151. }, {
  152. 'url': 'https://www.hotstar.com/in/clips/e3-sairat-kahani-pyaar-ki/1000262286',
  153. 'info_dict': {
  154. 'id': '1000262286',
  155. 'ext': 'mp4',
  156. 'title': 'E3 - SaiRat, Kahani Pyaar Ki',
  157. 'description': 'md5:e3b4b3203bc0c5396fe7d0e4948a6385',
  158. 'episode': 'E3 - SaiRat, Kahani Pyaar Ki',
  159. 'upload_date': '20210606',
  160. 'timestamp': 1622943900,
  161. 'duration': 5395,
  162. },
  163. }, {
  164. 'url': 'https://www.hotstar.com/in/movies/premam/1000091195',
  165. 'info_dict': {
  166. 'id': '1000091195',
  167. 'ext': 'mp4',
  168. 'title': 'Premam',
  169. 'release_year': 2015,
  170. 'description': 'md5:d833c654e4187b5e34757eafb5b72d7f',
  171. 'timestamp': 1462149000,
  172. 'upload_date': '20160502',
  173. 'episode': 'Premam',
  174. 'duration': 8994,
  175. },
  176. }, {
  177. 'url': 'https://www.hotstar.com/movies/radha-gopalam/1000057157',
  178. 'only_matching': True,
  179. }, {
  180. 'url': 'https://www.hotstar.com/in/sports/cricket/follow-the-blues-2021/recap-eng-fight-back-on-day-2/1260066104',
  181. 'only_matching': True,
  182. }, {
  183. 'url': 'https://www.hotstar.com/in/sports/football/most-costly-pl-transfers-ft-grealish/1260065956',
  184. 'only_matching': True,
  185. }]
  186. _GEO_BYPASS = False
  187. _TYPE = {
  188. 'movies': 'movie',
  189. 'sports': 'match',
  190. 'episode': 'episode',
  191. 'tv': 'episode',
  192. 'shows': 'episode',
  193. 'clips': 'content',
  194. None: 'content',
  195. }
  196. _IGNORE_MAP = {
  197. 'res': 'resolution',
  198. 'vcodec': 'video_codec',
  199. 'dr': 'dynamic_range',
  200. }
  201. _TAG_FIELDS = {
  202. 'language': 'language',
  203. 'acodec': 'audio_codec',
  204. 'vcodec': 'video_codec',
  205. }
  206. @classmethod
  207. def _video_url(cls, video_id, video_type=None, *, slug='ignore_me', root=None):
  208. assert None in (video_type, root)
  209. if not root:
  210. root = join_nonempty(cls._BASE_URL, video_type, delim='/')
  211. return f'{root}/{slug}/{video_id}'
  212. def _real_extract(self, url):
  213. video_id, video_type = self._match_valid_url(url).group('id', 'type')
  214. video_type = self._TYPE.get(video_type, video_type)
  215. cookies = self._get_cookies(url) # Cookies before any request
  216. video_data = traverse_obj(
  217. self._call_api_v1(
  218. f'{video_type}/detail', video_id, fatal=False, query={'tas': 10000, 'contentId': video_id}),
  219. ('body', 'results', 'item', {dict})) or {}
  220. if not self.get_param('allow_unplayable_formats') and video_data.get('drmProtected'):
  221. self.report_drm(video_id)
  222. # See https://github.com/yt-dlp/yt-dlp/issues/396
  223. st = self._download_webpage_handle(f'{self._BASE_URL}/in', video_id)[1].headers.get('x-origin-date')
  224. geo_restricted = False
  225. formats, subs = [], {}
  226. headers = {'Referer': f'{self._BASE_URL}/in'}
  227. # change to v2 in the future
  228. playback_sets = self._call_api_v2('play/v1/playback', video_id, st=st, cookies=cookies)['playBackSets']
  229. for playback_set in playback_sets:
  230. if not isinstance(playback_set, dict):
  231. continue
  232. tags = str_or_none(playback_set.get('tagsCombination')) or ''
  233. if any(f'{prefix}:{ignore}' in tags
  234. for key, prefix in self._IGNORE_MAP.items()
  235. for ignore in self._configuration_arg(key)):
  236. continue
  237. format_url = url_or_none(playback_set.get('playbackUrl'))
  238. if not format_url:
  239. continue
  240. format_url = re.sub(r'(?<=//staragvod)(\d)', r'web\1', format_url)
  241. ext = determine_ext(format_url)
  242. current_formats, current_subs = [], {}
  243. try:
  244. if 'package:hls' in tags or ext == 'm3u8':
  245. current_formats, current_subs = self._extract_m3u8_formats_and_subtitles(
  246. format_url, video_id, ext='mp4', headers=headers)
  247. elif 'package:dash' in tags or ext == 'mpd':
  248. current_formats, current_subs = self._extract_mpd_formats_and_subtitles(
  249. format_url, video_id, headers=headers)
  250. elif ext == 'f4m':
  251. pass # XXX: produce broken files
  252. else:
  253. current_formats = [{
  254. 'url': format_url,
  255. 'width': int_or_none(playback_set.get('width')),
  256. 'height': int_or_none(playback_set.get('height')),
  257. }]
  258. except ExtractorError as e:
  259. if isinstance(e.cause, HTTPError) and e.cause.status == 403:
  260. geo_restricted = True
  261. continue
  262. tag_dict = dict((*t.split(':', 1), None)[:2] for t in tags.split(';'))
  263. if tag_dict.get('encryption') not in ('plain', None):
  264. for f in current_formats:
  265. f['has_drm'] = True
  266. for f in current_formats:
  267. for k, v in self._TAG_FIELDS.items():
  268. if not f.get(k):
  269. f[k] = tag_dict.get(v)
  270. if f.get('vcodec') != 'none' and not f.get('dynamic_range'):
  271. f['dynamic_range'] = tag_dict.get('dynamic_range')
  272. if f.get('acodec') != 'none' and not f.get('audio_channels'):
  273. f['audio_channels'] = {
  274. 'stereo': 2,
  275. 'dolby51': 6,
  276. }.get(tag_dict.get('audio_channel'))
  277. f['format_note'] = join_nonempty(
  278. tag_dict.get('ladder'),
  279. tag_dict.get('audio_channel') if f.get('acodec') != 'none' else None,
  280. f.get('format_note'),
  281. delim=', ')
  282. formats.extend(current_formats)
  283. subs = self._merge_subtitles(subs, current_subs)
  284. if not formats and geo_restricted:
  285. self.raise_geo_restricted(countries=['IN'], metadata_available=True)
  286. self._remove_duplicate_formats(formats)
  287. for f in formats:
  288. f.setdefault('http_headers', {}).update(headers)
  289. return {
  290. 'id': video_id,
  291. 'title': video_data.get('title'),
  292. 'description': video_data.get('description'),
  293. 'duration': int_or_none(video_data.get('duration')),
  294. 'timestamp': int_or_none(traverse_obj(video_data, 'broadcastDate', 'startDate')),
  295. 'release_year': int_or_none(video_data.get('year')),
  296. 'formats': formats,
  297. 'subtitles': subs,
  298. 'channel': video_data.get('channelName'),
  299. 'channel_id': str_or_none(video_data.get('channelId')),
  300. 'series': video_data.get('showName'),
  301. 'season': video_data.get('seasonName'),
  302. 'season_number': int_or_none(video_data.get('seasonNo')),
  303. 'season_id': str_or_none(video_data.get('seasonId')),
  304. 'episode': video_data.get('title'),
  305. 'episode_number': int_or_none(video_data.get('episodeNo')),
  306. }
  307. class HotStarPrefixIE(InfoExtractor):
  308. """ The "hotstar:" prefix is no longer in use, but this is kept for backward compatibility """
  309. IE_DESC = False
  310. _VALID_URL = r'hotstar:(?:(?P<type>\w+):)?(?P<id>\d+)$'
  311. _TESTS = [{
  312. 'url': 'hotstar:1000076273',
  313. 'only_matching': True,
  314. }, {
  315. 'url': 'hotstar:movies:1260009879',
  316. 'info_dict': {
  317. 'id': '1260009879',
  318. 'ext': 'mp4',
  319. 'title': 'Nuvvu Naaku Nachav',
  320. 'description': 'md5:d43701b1314e6f8233ce33523c043b7d',
  321. 'timestamp': 1567525674,
  322. 'upload_date': '20190903',
  323. 'duration': 10787,
  324. 'episode': 'Nuvvu Naaku Nachav',
  325. },
  326. }, {
  327. 'url': 'hotstar:episode:1000234847',
  328. 'only_matching': True,
  329. }, {
  330. # contentData
  331. 'url': 'hotstar:sports:1260065956',
  332. 'only_matching': True,
  333. }, {
  334. # contentData
  335. 'url': 'hotstar:sports:1260066104',
  336. 'only_matching': True,
  337. }]
  338. def _real_extract(self, url):
  339. video_id, video_type = self._match_valid_url(url).group('id', 'type')
  340. return self.url_result(HotStarIE._video_url(video_id, video_type), HotStarIE, video_id)
  341. class HotStarPlaylistIE(HotStarBaseIE):
  342. IE_NAME = 'hotstar:playlist'
  343. _VALID_URL = r'https?://(?:www\.)?hotstar\.com(?:/in)?/(?:tv|shows)(?:/[^/]+){2}/list/[^/]+/t-(?P<id>\w+)'
  344. _TESTS = [{
  345. 'url': 'https://www.hotstar.com/tv/savdhaan-india/s-26/list/popular-clips/t-3_2_26',
  346. 'info_dict': {
  347. 'id': '3_2_26',
  348. },
  349. 'playlist_mincount': 20,
  350. }, {
  351. 'url': 'https://www.hotstar.com/shows/savdhaan-india/s-26/list/popular-clips/t-3_2_26',
  352. 'only_matching': True,
  353. }, {
  354. 'url': 'https://www.hotstar.com/tv/savdhaan-india/s-26/list/extras/t-2480',
  355. 'only_matching': True,
  356. }, {
  357. 'url': 'https://www.hotstar.com/in/tv/karthika-deepam/15457/list/popular-clips/t-3_2_1272',
  358. 'only_matching': True,
  359. }]
  360. def _real_extract(self, url):
  361. id_ = self._match_id(url)
  362. return self.playlist_result(
  363. self._playlist_entries('tray/find', id_, query={'tas': 10000, 'uqId': id_}), id_)
  364. class HotStarSeasonIE(HotStarBaseIE):
  365. IE_NAME = 'hotstar:season'
  366. _VALID_URL = r'(?P<url>https?://(?:www\.)?hotstar\.com(?:/in)?/(?:tv|shows)/[^/]+/\w+)/seasons/[^/]+/ss-(?P<id>\w+)'
  367. _TESTS = [{
  368. 'url': 'https://www.hotstar.com/tv/radhakrishn/1260000646/seasons/season-2/ss-8028',
  369. 'info_dict': {
  370. 'id': '8028',
  371. },
  372. 'playlist_mincount': 35,
  373. }, {
  374. 'url': 'https://www.hotstar.com/in/tv/ishqbaaz/9567/seasons/season-2/ss-4357',
  375. 'info_dict': {
  376. 'id': '4357',
  377. },
  378. 'playlist_mincount': 30,
  379. }, {
  380. 'url': 'https://www.hotstar.com/in/tv/bigg-boss/14714/seasons/season-4/ss-8208/',
  381. 'info_dict': {
  382. 'id': '8208',
  383. },
  384. 'playlist_mincount': 19,
  385. }, {
  386. 'url': 'https://www.hotstar.com/in/shows/bigg-boss/14714/seasons/season-4/ss-8208/',
  387. 'only_matching': True,
  388. }]
  389. def _real_extract(self, url):
  390. url, season_id = self._match_valid_url(url).groups()
  391. return self.playlist_result(self._playlist_entries(
  392. 'season/asset', season_id, url, query={'tao': 0, 'tas': 0, 'size': 10000, 'id': season_id}), season_id)
  393. class HotStarSeriesIE(HotStarBaseIE):
  394. IE_NAME = 'hotstar:series'
  395. _VALID_URL = r'(?P<url>https?://(?:www\.)?hotstar\.com(?:/in)?/(?:tv|shows)/[^/]+/(?P<id>\d+))/?(?:[#?]|$)'
  396. _TESTS = [{
  397. 'url': 'https://www.hotstar.com/in/tv/radhakrishn/1260000646',
  398. 'info_dict': {
  399. 'id': '1260000646',
  400. },
  401. 'playlist_mincount': 690,
  402. }, {
  403. 'url': 'https://www.hotstar.com/tv/dancee-/1260050431',
  404. 'info_dict': {
  405. 'id': '1260050431',
  406. },
  407. 'playlist_mincount': 43,
  408. }, {
  409. 'url': 'https://www.hotstar.com/in/tv/mahabharat/435/',
  410. 'info_dict': {
  411. 'id': '435',
  412. },
  413. 'playlist_mincount': 267,
  414. }, {
  415. 'url': 'https://www.hotstar.com/in/shows/anupama/1260022017/',
  416. 'info_dict': {
  417. 'id': '1260022017',
  418. },
  419. 'playlist_mincount': 940,
  420. }]
  421. def _real_extract(self, url):
  422. url, series_id = self._match_valid_url(url).groups()
  423. id_ = self._call_api_v1(
  424. 'show/detail', series_id, query={'contentId': series_id})['body']['results']['item']['id']
  425. return self.playlist_result(self._playlist_entries(
  426. 'tray/g/1/items', series_id, url, query={'tao': 0, 'tas': 10000, 'etid': 0, 'eid': id_}), series_id)