sonyliv.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import datetime
  2. import json
  3. import math
  4. import random
  5. import time
  6. import uuid
  7. from .common import InfoExtractor
  8. from ..compat import compat_HTTPError
  9. from ..utils import (
  10. ExtractorError,
  11. int_or_none,
  12. try_get,
  13. )
  14. class SonyLIVIE(InfoExtractor):
  15. _VALID_URL = r'''(?x)
  16. (?:
  17. sonyliv:|
  18. https?://(?:www\.)?sonyliv\.com/(?:s(?:how|port)s/[^/]+|movies|clip|trailer|music-videos)/[^/?#&]+-
  19. )
  20. (?P<id>\d+)
  21. '''
  22. _TESTS = [{
  23. 'url': 'https://www.sonyliv.com/shows/bachelors-delight-1700000113/achaari-cheese-toast-1000022678?watch=true',
  24. 'info_dict': {
  25. 'title': 'Achaari Cheese Toast',
  26. 'id': '1000022678',
  27. 'ext': 'mp4',
  28. 'upload_date': '20200411',
  29. 'description': 'md5:3957fa31d9309bf336ceb3f37ad5b7cb',
  30. 'timestamp': 1586632091,
  31. 'duration': 185,
  32. 'season_number': 1,
  33. 'series': 'Bachelors Delight',
  34. 'episode_number': 1,
  35. 'release_year': 2016,
  36. },
  37. 'params': {
  38. 'skip_download': True,
  39. },
  40. }, {
  41. 'url': 'https://www.sonyliv.com/movies/tahalka-1000050121?watch=true',
  42. 'only_matching': True,
  43. }, {
  44. 'url': 'https://www.sonyliv.com/clip/jigarbaaz-1000098925',
  45. 'only_matching': True,
  46. }, {
  47. 'url': 'https://www.sonyliv.com/trailer/sandwiched-forever-1000100286?watch=true',
  48. 'only_matching': True,
  49. }, {
  50. 'url': 'https://www.sonyliv.com/sports/india-tour-of-australia-2020-21-1700000286/cricket-hls-day-3-1st-test-aus-vs-ind-19-dec-2020-1000100959?watch=true',
  51. 'only_matching': True,
  52. }, {
  53. 'url': 'https://www.sonyliv.com/music-videos/yeh-un-dinon-ki-baat-hai-1000018779',
  54. 'only_matching': True,
  55. }]
  56. _GEO_COUNTRIES = ['IN']
  57. _HEADERS = {}
  58. _LOGIN_HINT = 'Use "--username <mobile_number>" to login using OTP or "--username token --password <auth_token>" to login using auth token.'
  59. _NETRC_MACHINE = 'sonyliv'
  60. def _get_device_id(self):
  61. e = int(time.time() * 1000)
  62. t = list('xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx')
  63. for i, c in enumerate(t):
  64. n = int((e + 16 * random.random()) % 16) | 0
  65. e = math.floor(e / 16)
  66. if c == 'x':
  67. t[i] = str(n)
  68. elif c == 'y':
  69. t[i] = '{:x}'.format(3 & n | 8)
  70. return ''.join(t) + '-' + str(int(time.time() * 1000))
  71. def _perform_login(self, username, password):
  72. self._HEADERS['device_id'] = self._get_device_id()
  73. self._HEADERS['content-type'] = 'application/json'
  74. if username.lower() == 'token' and len(password) > 1198:
  75. self._HEADERS['authorization'] = password
  76. elif len(username) != 10 or not username.isdigit():
  77. raise ExtractorError(f'Invalid username/password; {self._LOGIN_HINT}')
  78. self.report_login()
  79. otp_request_json = self._download_json(
  80. 'https://apiv2.sonyliv.com/AGL/1.6/A/ENG/WEB/IN/HR/CREATEOTP-V2',
  81. None, note='Sending OTP', headers=self._HEADERS, data=json.dumps({
  82. 'mobileNumber': username,
  83. 'channelPartnerID': 'MSMIND',
  84. 'country': 'IN',
  85. 'timestamp': datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%MZ'),
  86. 'otpSize': 6,
  87. 'loginType': 'REGISTERORSIGNIN',
  88. 'isMobileMandatory': True,
  89. }).encode())
  90. if otp_request_json['resultCode'] == 'KO':
  91. raise ExtractorError(otp_request_json['message'], expected=True)
  92. otp_verify_json = self._download_json(
  93. 'https://apiv2.sonyliv.com/AGL/2.0/A/ENG/WEB/IN/HR/CONFIRMOTP-V2',
  94. None, note='Verifying OTP', headers=self._HEADERS, data=json.dumps({
  95. 'channelPartnerID': 'MSMIND',
  96. 'mobileNumber': username,
  97. 'country': 'IN',
  98. 'otp': self._get_tfa_info('OTP'),
  99. 'dmaId': 'IN',
  100. 'ageConfirmation': True,
  101. 'timestamp': datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%MZ'),
  102. 'isMobileMandatory': True,
  103. }).encode())
  104. if otp_verify_json['resultCode'] == 'KO':
  105. raise ExtractorError(otp_request_json['message'], expected=True)
  106. self._HEADERS['authorization'] = otp_verify_json['resultObj']['accessToken']
  107. def _call_api(self, version, path, video_id):
  108. try:
  109. return self._download_json(
  110. 'https://apiv2.sonyliv.com/AGL/%s/A/ENG/WEB/%s' % (version, path),
  111. video_id, headers=self._HEADERS)['resultObj']
  112. except ExtractorError as e:
  113. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 406 and self._parse_json(
  114. e.cause.read().decode(), video_id)['message'] == 'Please subscribe to watch this content':
  115. self.raise_login_required(self._LOGIN_HINT, method=None)
  116. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
  117. message = self._parse_json(
  118. e.cause.read().decode(), video_id)['message']
  119. if message == 'Geoblocked Country':
  120. self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
  121. raise ExtractorError(message)
  122. raise
  123. def _initialize_pre_login(self):
  124. self._HEADERS['security_token'] = self._call_api('1.4', 'ALL/GETTOKEN', None)
  125. def _real_extract(self, url):
  126. video_id = self._match_id(url)
  127. content = self._call_api(
  128. '1.5', 'IN/CONTENT/VIDEOURL/VOD/' + video_id, video_id)
  129. if not self.get_param('allow_unplayable_formats') and content.get('isEncrypted'):
  130. self.report_drm(video_id)
  131. dash_url = content['videoURL']
  132. headers = {
  133. 'x-playback-session-id': '%s-%d' % (uuid.uuid4().hex, time.time() * 1000)
  134. }
  135. formats = self._extract_mpd_formats(
  136. dash_url, video_id, mpd_id='dash', headers=headers, fatal=False)
  137. formats.extend(self._extract_m3u8_formats(
  138. dash_url.replace('.mpd', '.m3u8').replace('/DASH/', '/HLS/'),
  139. video_id, 'mp4', m3u8_id='hls', headers=headers, fatal=False))
  140. for f in formats:
  141. f.setdefault('http_headers', {}).update(headers)
  142. metadata = self._call_api(
  143. '1.6', 'IN/DETAIL/' + video_id, video_id)['containers'][0]['metadata']
  144. title = metadata['episodeTitle']
  145. subtitles = {}
  146. for sub in content.get('subtitle', []):
  147. sub_url = sub.get('subtitleUrl')
  148. if not sub_url:
  149. continue
  150. subtitles.setdefault(sub.get('subtitleLanguageName', 'ENG'), []).append({
  151. 'url': sub_url,
  152. })
  153. return {
  154. 'id': video_id,
  155. 'title': title,
  156. 'formats': formats,
  157. 'thumbnail': content.get('posterURL'),
  158. 'description': metadata.get('longDescription') or metadata.get('shortDescription'),
  159. 'timestamp': int_or_none(metadata.get('creationDate'), 1000),
  160. 'duration': int_or_none(metadata.get('duration')),
  161. 'season_number': int_or_none(metadata.get('season')),
  162. 'series': metadata.get('title'),
  163. 'episode_number': int_or_none(metadata.get('episodeNumber')),
  164. 'release_year': int_or_none(metadata.get('year')),
  165. 'subtitles': subtitles,
  166. }
  167. class SonyLIVSeriesIE(InfoExtractor):
  168. _VALID_URL = r'https?://(?:www\.)?sonyliv\.com/shows/[^/?#&]+-(?P<id>\d{10})$'
  169. _TESTS = [{
  170. 'url': 'https://www.sonyliv.com/shows/adaalat-1700000091',
  171. 'playlist_mincount': 456,
  172. 'info_dict': {
  173. 'id': '1700000091',
  174. },
  175. }]
  176. _API_SHOW_URL = "https://apiv2.sonyliv.com/AGL/1.9/R/ENG/WEB/IN/DL/DETAIL/{}?kids_safe=false&from=0&to=49"
  177. _API_EPISODES_URL = "https://apiv2.sonyliv.com/AGL/1.4/R/ENG/WEB/IN/CONTENT/DETAIL/BUNDLE/{}?from=0&to=1000&orderBy=episodeNumber&sortOrder=asc"
  178. _API_SECURITY_URL = 'https://apiv2.sonyliv.com/AGL/1.4/A/ENG/WEB/ALL/GETTOKEN'
  179. def _entries(self, show_id):
  180. headers = {
  181. 'Accept': 'application/json, text/plain, */*',
  182. 'Referer': 'https://www.sonyliv.com',
  183. }
  184. headers['security_token'] = self._download_json(
  185. self._API_SECURITY_URL, video_id=show_id, headers=headers,
  186. note='Downloading security token')['resultObj']
  187. seasons = try_get(
  188. self._download_json(self._API_SHOW_URL.format(show_id), video_id=show_id, headers=headers),
  189. lambda x: x['resultObj']['containers'][0]['containers'], list)
  190. for season in seasons or []:
  191. season_id = season['id']
  192. episodes = try_get(
  193. self._download_json(self._API_EPISODES_URL.format(season_id), video_id=season_id, headers=headers),
  194. lambda x: x['resultObj']['containers'][0]['containers'], list)
  195. for episode in episodes or []:
  196. video_id = episode.get('id')
  197. yield self.url_result('sonyliv:%s' % video_id, ie=SonyLIVIE.ie_key(), video_id=video_id)
  198. def _real_extract(self, url):
  199. show_id = self._match_id(url)
  200. return self.playlist_result(self._entries(show_id), playlist_id=show_id)