logo

youtube-dl

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

adn.py (11515B)


  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import base64
  4. import binascii
  5. import json
  6. import os
  7. import random
  8. from .common import InfoExtractor
  9. from ..aes import aes_cbc_decrypt
  10. from ..compat import (
  11. compat_HTTPError,
  12. compat_b64decode,
  13. compat_ord,
  14. )
  15. from ..utils import (
  16. bytes_to_intlist,
  17. bytes_to_long,
  18. ExtractorError,
  19. float_or_none,
  20. int_or_none,
  21. intlist_to_bytes,
  22. long_to_bytes,
  23. pkcs1pad,
  24. strip_or_none,
  25. try_get,
  26. unified_strdate,
  27. urlencode_postdata,
  28. )
  29. class ADNIE(InfoExtractor):
  30. IE_DESC = 'Animation Digital Network'
  31. _VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.fr/video/[^/]+/(?P<id>\d+)'
  32. _TESTS = [{
  33. 'url': 'https://animationdigitalnetwork.fr/video/fruits-basket/9841-episode-1-a-ce-soir',
  34. 'md5': '1c9ef066ceb302c86f80c2b371615261',
  35. 'info_dict': {
  36. 'id': '9841',
  37. 'ext': 'mp4',
  38. 'title': 'Fruits Basket - Episode 1',
  39. 'description': 'md5:14be2f72c3c96809b0ca424b0097d336',
  40. 'series': 'Fruits Basket',
  41. 'duration': 1437,
  42. 'release_date': '20190405',
  43. 'comment_count': int,
  44. 'average_rating': float,
  45. 'season_number': 1,
  46. 'episode': 'À ce soir !',
  47. 'episode_number': 1,
  48. },
  49. 'skip': 'Only available in region (FR, ...)',
  50. }, {
  51. 'url': 'http://animedigitalnetwork.fr/video/blue-exorcist-kyoto-saga/7778-episode-1-debut-des-hostilites',
  52. 'only_matching': True,
  53. }]
  54. _NETRC_MACHINE = 'animationdigitalnetwork'
  55. _BASE = 'animationdigitalnetwork.fr'
  56. _API_BASE_URL = 'https://gw.api.' + _BASE + '/'
  57. _PLAYER_BASE_URL = _API_BASE_URL + 'player/'
  58. _HEADERS = {}
  59. _LOGIN_ERR_MESSAGE = 'Unable to log in'
  60. _RSA_KEY = (0x9B42B08905199A5CCE2026274399CA560ECB209EE9878A708B1C0812E1BB8CB5D1FB7441861147C1A1F2F3A0476DD63A9CAC20D3E983613346850AA6CB38F16DC7D720FD7D86FC6E5B3D5BBC72E14CD0BF9E869F2CEA2CCAD648F1DCE38F1FF916CEFB2D339B64AA0264372344BC775E265E8A852F88144AB0BD9AA06C1A4ABB, 65537)
  61. _POS_ALIGN_MAP = {
  62. 'start': 1,
  63. 'end': 3,
  64. }
  65. _LINE_ALIGN_MAP = {
  66. 'middle': 8,
  67. 'end': 4,
  68. }
  69. @staticmethod
  70. def _ass_subtitles_timecode(seconds):
  71. return '%01d:%02d:%02d.%02d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 100)
  72. def _get_subtitles(self, sub_url, video_id):
  73. if not sub_url:
  74. return None
  75. enc_subtitles = self._download_webpage(
  76. sub_url, video_id, 'Downloading subtitles location', fatal=False) or '{}'
  77. subtitle_location = (self._parse_json(enc_subtitles, video_id, fatal=False) or {}).get('location')
  78. if subtitle_location:
  79. enc_subtitles = self._download_webpage(
  80. subtitle_location, video_id, 'Downloading subtitles data',
  81. fatal=False, headers={'Origin': 'https://' + self._BASE})
  82. if not enc_subtitles:
  83. return None
  84. # http://animationdigitalnetwork.fr/components/com_vodvideo/videojs/adn-vjs.min.js
  85. dec_subtitles = intlist_to_bytes(aes_cbc_decrypt(
  86. bytes_to_intlist(compat_b64decode(enc_subtitles[24:])),
  87. bytes_to_intlist(binascii.unhexlify(self._K + '7fac1178830cfe0c')),
  88. bytes_to_intlist(compat_b64decode(enc_subtitles[:24]))
  89. ))
  90. subtitles_json = self._parse_json(
  91. dec_subtitles[:-compat_ord(dec_subtitles[-1])].decode(),
  92. None, fatal=False)
  93. if not subtitles_json:
  94. return None
  95. subtitles = {}
  96. for sub_lang, sub in subtitles_json.items():
  97. ssa = '''[Script Info]
  98. ScriptType:V4.00
  99. [V4 Styles]
  100. Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,TertiaryColour,BackColour,Bold,Italic,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,AlphaLevel,Encoding
  101. Style: Default,Arial,18,16777215,16777215,16777215,0,-1,0,1,1,0,2,20,20,20,0,0
  102. [Events]
  103. Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
  104. for current in sub:
  105. start, end, text, line_align, position_align = (
  106. float_or_none(current.get('startTime')),
  107. float_or_none(current.get('endTime')),
  108. current.get('text'), current.get('lineAlign'),
  109. current.get('positionAlign'))
  110. if start is None or end is None or text is None:
  111. continue
  112. alignment = self._POS_ALIGN_MAP.get(position_align, 2) + self._LINE_ALIGN_MAP.get(line_align, 0)
  113. ssa += os.linesep + 'Dialogue: Marked=0,%s,%s,Default,,0,0,0,,%s%s' % (
  114. self._ass_subtitles_timecode(start),
  115. self._ass_subtitles_timecode(end),
  116. '{\\a%d}' % alignment if alignment != 2 else '',
  117. text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}'))
  118. if sub_lang == 'vostf':
  119. sub_lang = 'fr'
  120. subtitles.setdefault(sub_lang, []).extend([{
  121. 'ext': 'json',
  122. 'data': json.dumps(sub),
  123. }, {
  124. 'ext': 'ssa',
  125. 'data': ssa,
  126. }])
  127. return subtitles
  128. def _real_initialize(self):
  129. username, password = self._get_login_info()
  130. if not username:
  131. return
  132. try:
  133. url = self._API_BASE_URL + 'authentication/login'
  134. access_token = (self._download_json(
  135. url, None, 'Logging in', self._LOGIN_ERR_MESSAGE, fatal=False,
  136. data=urlencode_postdata({
  137. 'password': password,
  138. 'rememberMe': False,
  139. 'source': 'Web',
  140. 'username': username,
  141. })) or {}).get('accessToken')
  142. if access_token:
  143. self._HEADERS = {'authorization': 'Bearer ' + access_token}
  144. except ExtractorError as e:
  145. message = None
  146. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
  147. resp = self._parse_json(
  148. self._webpage_read_content(e.cause, url, username),
  149. username, fatal=False) or {}
  150. message = resp.get('message') or resp.get('code')
  151. self.report_warning(message or self._LOGIN_ERR_MESSAGE)
  152. def _real_extract(self, url):
  153. video_id = self._match_id(url)
  154. video_base_url = self._PLAYER_BASE_URL + 'video/%s/' % video_id
  155. player = self._download_json(
  156. video_base_url + 'configuration', video_id,
  157. 'Downloading player config JSON metadata',
  158. headers=self._HEADERS)['player']
  159. options = player['options']
  160. user = options['user']
  161. if not user.get('hasAccess'):
  162. self.raise_login_required()
  163. token = self._download_json(
  164. user.get('refreshTokenUrl') or (self._PLAYER_BASE_URL + 'refresh/token'),
  165. video_id, 'Downloading access token', headers={
  166. 'x-player-refresh-token': user['refreshToken']
  167. }, data=b'')['token']
  168. links_url = try_get(options, lambda x: x['video']['url']) or (video_base_url + 'link')
  169. self._K = ''.join([random.choice('0123456789abcdef') for _ in range(16)])
  170. message = bytes_to_intlist(json.dumps({
  171. 'k': self._K,
  172. 't': token,
  173. }))
  174. # Sometimes authentication fails for no good reason, retry with
  175. # a different random padding
  176. links_data = None
  177. for _ in range(3):
  178. padded_message = intlist_to_bytes(pkcs1pad(message, 128))
  179. n, e = self._RSA_KEY
  180. encrypted_message = long_to_bytes(pow(bytes_to_long(padded_message), e, n))
  181. authorization = base64.b64encode(encrypted_message).decode()
  182. try:
  183. links_data = self._download_json(
  184. links_url, video_id, 'Downloading links JSON metadata', headers={
  185. 'X-Player-Token': authorization
  186. }, query={
  187. 'freeWithAds': 'true',
  188. 'adaptive': 'false',
  189. 'withMetadata': 'true',
  190. 'source': 'Web'
  191. })
  192. break
  193. except ExtractorError as e:
  194. if not isinstance(e.cause, compat_HTTPError):
  195. raise e
  196. if e.cause.code == 401:
  197. # This usually goes away with a different random pkcs1pad, so retry
  198. continue
  199. error = self._parse_json(
  200. self._webpage_read_content(e.cause, links_url, video_id),
  201. video_id, fatal=False) or {}
  202. message = error.get('message')
  203. if e.cause.code == 403 and error.get('code') == 'player-bad-geolocation-country':
  204. self.raise_geo_restricted(msg=message)
  205. raise ExtractorError(message)
  206. else:
  207. raise ExtractorError('Giving up retrying')
  208. links = links_data.get('links') or {}
  209. metas = links_data.get('metadata') or {}
  210. sub_url = (links.get('subtitles') or {}).get('all')
  211. video_info = links_data.get('video') or {}
  212. title = metas['title']
  213. formats = []
  214. for format_id, qualities in (links.get('streaming') or {}).items():
  215. if not isinstance(qualities, dict):
  216. continue
  217. for quality, load_balancer_url in qualities.items():
  218. load_balancer_data = self._download_json(
  219. load_balancer_url, video_id,
  220. 'Downloading %s %s JSON metadata' % (format_id, quality),
  221. fatal=False) or {}
  222. m3u8_url = load_balancer_data.get('location')
  223. if not m3u8_url:
  224. continue
  225. m3u8_formats = self._extract_m3u8_formats(
  226. m3u8_url, video_id, 'mp4', 'm3u8_native',
  227. m3u8_id=format_id, fatal=False)
  228. if format_id == 'vf':
  229. for f in m3u8_formats:
  230. f['language'] = 'fr'
  231. formats.extend(m3u8_formats)
  232. self._sort_formats(formats)
  233. video = (self._download_json(
  234. self._API_BASE_URL + 'video/%s' % video_id, video_id,
  235. 'Downloading additional video metadata', fatal=False) or {}).get('video') or {}
  236. show = video.get('show') or {}
  237. return {
  238. 'id': video_id,
  239. 'title': title,
  240. 'description': strip_or_none(metas.get('summary') or video.get('summary')),
  241. 'thumbnail': video_info.get('image') or player.get('image'),
  242. 'formats': formats,
  243. 'subtitles': self.extract_subtitles(sub_url, video_id),
  244. 'episode': metas.get('subtitle') or video.get('name'),
  245. 'episode_number': int_or_none(video.get('shortNumber')),
  246. 'series': show.get('title'),
  247. 'season_number': int_or_none(video.get('season')),
  248. 'duration': int_or_none(video_info.get('duration') or video.get('duration')),
  249. 'release_date': unified_strdate(video.get('releaseDate')),
  250. 'average_rating': float_or_none(video.get('rating') or metas.get('rating')),
  251. 'comment_count': int_or_none(video.get('commentsCount')),
  252. }