vvvvid.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import re
  2. from .common import InfoExtractor
  3. from .youtube import YoutubeIE
  4. from ..utils import (
  5. ExtractorError,
  6. int_or_none,
  7. str_or_none,
  8. )
  9. class VVVVIDIE(InfoExtractor):
  10. _VALID_URL_BASE = r'https?://(?:www\.)?vvvvid\.it/(?:#!)?(?:show|anime|film|series)/'
  11. _VALID_URL = r'%s(?P<show_id>\d+)/[^/]+/(?P<season_id>\d+)/(?P<id>[0-9]+)' % _VALID_URL_BASE
  12. _TESTS = [{
  13. # video_type == 'video/vvvvid'
  14. 'url': 'https://www.vvvvid.it/#!show/434/perche-dovrei-guardarlo-di-dario-moccia/437/489048/ping-pong',
  15. 'md5': 'b8d3cecc2e981adc3835adf07f6df91b',
  16. 'info_dict': {
  17. 'id': '489048',
  18. 'ext': 'mp4',
  19. 'title': 'Ping Pong',
  20. 'duration': 239,
  21. 'series': '"Perché dovrei guardarlo?" di Dario Moccia',
  22. 'season_id': '437',
  23. 'episode': 'Ping Pong',
  24. 'episode_number': 1,
  25. 'episode_id': '3334',
  26. 'view_count': int,
  27. 'like_count': int,
  28. 'repost_count': int,
  29. },
  30. 'params': {
  31. 'skip_download': True,
  32. },
  33. }, {
  34. # video_type == 'video/rcs'
  35. 'url': 'https://www.vvvvid.it/#!show/376/death-note-live-action/377/482493/episodio-01',
  36. 'md5': '33e0edfba720ad73a8782157fdebc648',
  37. 'info_dict': {
  38. 'id': '482493',
  39. 'ext': 'mp4',
  40. 'title': 'Episodio 01',
  41. },
  42. 'params': {
  43. 'skip_download': True,
  44. },
  45. }, {
  46. # video_type == 'video/youtube'
  47. 'url': 'https://www.vvvvid.it/show/404/one-punch-man/406/486683/trailer',
  48. 'md5': '33e0edfba720ad73a8782157fdebc648',
  49. 'info_dict': {
  50. 'id': 'RzmFKUDOUgw',
  51. 'ext': 'mp4',
  52. 'title': 'Trailer',
  53. 'upload_date': '20150906',
  54. 'description': 'md5:a5e802558d35247fee285875328c0b80',
  55. 'uploader_id': 'BandaiVisual',
  56. 'uploader': 'BANDAI NAMCO Arts Channel',
  57. },
  58. 'params': {
  59. 'skip_download': True,
  60. },
  61. }, {
  62. # video_type == 'video/dash'
  63. 'url': 'https://www.vvvvid.it/show/683/made-in-abyss/1542/693786/nanachi',
  64. 'info_dict': {
  65. 'id': '693786',
  66. 'ext': 'mp4',
  67. 'title': 'Nanachi',
  68. },
  69. 'params': {
  70. 'skip_download': True,
  71. 'format': 'mp4',
  72. },
  73. }, {
  74. 'url': 'https://www.vvvvid.it/show/434/perche-dovrei-guardarlo-di-dario-moccia/437/489048',
  75. 'only_matching': True
  76. }]
  77. _conn_id = None
  78. def _real_initialize(self):
  79. self._conn_id = self._download_json(
  80. 'https://www.vvvvid.it/user/login',
  81. None, headers=self.geo_verification_headers())['data']['conn_id']
  82. def _download_info(self, show_id, path, video_id, fatal=True, query=None):
  83. q = {
  84. 'conn_id': self._conn_id,
  85. }
  86. if query:
  87. q.update(query)
  88. response = self._download_json(
  89. 'https://www.vvvvid.it/vvvvid/ondemand/%s/%s' % (show_id, path),
  90. video_id, headers=self.geo_verification_headers(), query=q, fatal=fatal)
  91. if not (response or fatal):
  92. return
  93. if response.get('result') == 'error':
  94. raise ExtractorError('%s said: %s' % (
  95. self.IE_NAME, response['message']), expected=True)
  96. return response['data']
  97. def _extract_common_video_info(self, video_data):
  98. return {
  99. 'thumbnail': video_data.get('thumbnail'),
  100. 'episode_id': str_or_none(video_data.get('id')),
  101. }
  102. def _real_extract(self, url):
  103. show_id, season_id, video_id = self._match_valid_url(url).groups()
  104. response = self._download_info(
  105. show_id, 'season/%s' % season_id,
  106. video_id, query={'video_id': video_id})
  107. vid = int(video_id)
  108. video_data = list(filter(
  109. lambda episode: episode.get('video_id') == vid, response))[0]
  110. title = video_data['title']
  111. formats = []
  112. # vvvvid embed_info decryption algorithm is reverse engineered from function $ds(h) at vvvvid.js
  113. def ds(h):
  114. g = "MNOPIJKL89+/4567UVWXQRSTEFGHABCDcdefYZabstuvopqr0123wxyzklmnghij"
  115. def f(m):
  116. l = []
  117. o = 0
  118. b = False
  119. m_len = len(m)
  120. while ((not b) and o < m_len):
  121. n = m[o] << 2
  122. o += 1
  123. k = -1
  124. j = -1
  125. if o < m_len:
  126. n += m[o] >> 4
  127. o += 1
  128. if o < m_len:
  129. k = (m[o - 1] << 4) & 255
  130. k += m[o] >> 2
  131. o += 1
  132. if o < m_len:
  133. j = (m[o - 1] << 6) & 255
  134. j += m[o]
  135. o += 1
  136. else:
  137. b = True
  138. else:
  139. b = True
  140. else:
  141. b = True
  142. l.append(n)
  143. if k != -1:
  144. l.append(k)
  145. if j != -1:
  146. l.append(j)
  147. return l
  148. c = []
  149. for e in h:
  150. c.append(g.index(e))
  151. c_len = len(c)
  152. for e in range(c_len * 2 - 1, -1, -1):
  153. a = c[e % c_len] ^ c[(e + 1) % c_len]
  154. c[e % c_len] = a
  155. c = f(c)
  156. d = ''
  157. for e in c:
  158. d += chr(e)
  159. return d
  160. info = {}
  161. def metadata_from_url(r_url):
  162. if not info and r_url:
  163. mobj = re.search(r'_(?:S(\d+))?Ep(\d+)', r_url)
  164. if mobj:
  165. info['episode_number'] = int(mobj.group(2))
  166. season_number = mobj.group(1)
  167. if season_number:
  168. info['season_number'] = int(season_number)
  169. video_type = video_data.get('video_type')
  170. is_youtube = False
  171. for quality in ('', '_sd'):
  172. embed_code = video_data.get('embed_info' + quality)
  173. if not embed_code:
  174. continue
  175. embed_code = ds(embed_code)
  176. if video_type == 'video/kenc':
  177. embed_code = re.sub(r'https?(://[^/]+)/z/', r'https\1/i/', embed_code).replace('/manifest.f4m', '/master.m3u8')
  178. kenc = self._download_json(
  179. 'https://www.vvvvid.it/kenc', video_id, query={
  180. 'action': 'kt',
  181. 'conn_id': self._conn_id,
  182. 'url': embed_code,
  183. }, fatal=False) or {}
  184. kenc_message = kenc.get('message')
  185. if kenc_message:
  186. embed_code += '?' + ds(kenc_message)
  187. formats.extend(self._extract_m3u8_formats(
  188. embed_code, video_id, 'mp4', m3u8_id='hls', fatal=False))
  189. elif video_type == 'video/rcs':
  190. formats.extend(self._extract_akamai_formats(embed_code, video_id))
  191. elif video_type == 'video/youtube':
  192. info.update({
  193. '_type': 'url_transparent',
  194. 'ie_key': YoutubeIE.ie_key(),
  195. 'url': embed_code,
  196. })
  197. is_youtube = True
  198. break
  199. elif video_type == 'video/dash':
  200. formats.extend(self._extract_m3u8_formats(
  201. embed_code, video_id, 'mp4', m3u8_id='hls', fatal=False))
  202. else:
  203. formats.extend(self._extract_wowza_formats(
  204. 'http://sb.top-ix.org/videomg/_definst_/mp4:%s/playlist.m3u8' % embed_code, video_id))
  205. metadata_from_url(embed_code)
  206. if not is_youtube:
  207. info['formats'] = formats
  208. metadata_from_url(video_data.get('thumbnail'))
  209. info.update(self._extract_common_video_info(video_data))
  210. info.update({
  211. 'id': video_id,
  212. 'title': title,
  213. 'duration': int_or_none(video_data.get('length')),
  214. 'series': video_data.get('show_title'),
  215. 'season_id': season_id,
  216. 'episode': title,
  217. 'view_count': int_or_none(video_data.get('views')),
  218. 'like_count': int_or_none(video_data.get('video_likes')),
  219. 'repost_count': int_or_none(video_data.get('video_shares')),
  220. })
  221. return info
  222. class VVVVIDShowIE(VVVVIDIE): # XXX: Do not subclass from concrete IE
  223. _VALID_URL = r'(?P<base_url>%s(?P<id>\d+)(?:/(?P<show_title>[^/?&#]+))?)/?(?:[?#&]|$)' % VVVVIDIE._VALID_URL_BASE
  224. _TESTS = [{
  225. 'url': 'https://www.vvvvid.it/show/156/psyco-pass',
  226. 'info_dict': {
  227. 'id': '156',
  228. 'title': 'Psycho-Pass',
  229. 'description': 'md5:94d572c0bd85894b193b8aebc9a3a806',
  230. },
  231. 'playlist_count': 46,
  232. }, {
  233. 'url': 'https://www.vvvvid.it/show/156',
  234. 'only_matching': True,
  235. }]
  236. def _real_extract(self, url):
  237. base_url, show_id, show_title = self._match_valid_url(url).groups()
  238. seasons = self._download_info(
  239. show_id, 'seasons/', show_title)
  240. show_info = self._download_info(
  241. show_id, 'info/', show_title, fatal=False)
  242. if not show_title:
  243. base_url += "/title"
  244. entries = []
  245. for season in (seasons or []):
  246. episodes = season.get('episodes') or []
  247. playlist_title = season.get('name') or show_info.get('title')
  248. for episode in episodes:
  249. if episode.get('playable') is False:
  250. continue
  251. season_id = str_or_none(episode.get('season_id'))
  252. video_id = str_or_none(episode.get('video_id'))
  253. if not (season_id and video_id):
  254. continue
  255. info = self._extract_common_video_info(episode)
  256. info.update({
  257. '_type': 'url_transparent',
  258. 'ie_key': VVVVIDIE.ie_key(),
  259. 'url': '/'.join([base_url, season_id, video_id]),
  260. 'title': episode.get('title'),
  261. 'description': episode.get('description'),
  262. 'season_id': season_id,
  263. 'playlist_title': playlist_title,
  264. })
  265. entries.append(info)
  266. return self.playlist_result(
  267. entries, show_id, show_info.get('title'), show_info.get('description'))