vvvvid.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import functools
  2. import re
  3. from .common import InfoExtractor
  4. from .youtube import YoutubeIE
  5. from ..utils import (
  6. ExtractorError,
  7. int_or_none,
  8. str_or_none,
  9. )
  10. class VVVVIDIE(InfoExtractor):
  11. _VALID_URL_BASE = r'https?://(?:www\.)?vvvvid\.it/(?:#!)?(?:show|anime|film|series)/'
  12. _VALID_URL = rf'{_VALID_URL_BASE}(?P<show_id>\d+)/[^/]+/(?P<season_id>\d+)/(?P<id>[0-9]+)'
  13. _TESTS = [{
  14. # video_type == 'video/vvvvid'
  15. 'url': 'https://www.vvvvid.it/show/498/the-power-of-computing/518/505692/playstation-vr-cambiera-il-nostro-modo-di-giocare',
  16. 'info_dict': {
  17. 'id': '505692',
  18. 'ext': 'mp4',
  19. 'title': 'Playstation VR cambierà il nostro modo di giocare',
  20. 'duration': 93,
  21. 'series': 'The Power of Computing',
  22. 'season_id': '518',
  23. 'episode': 'Playstation VR cambierà il nostro modo di giocare',
  24. 'episode_id': '4747',
  25. 'view_count': int,
  26. 'like_count': int,
  27. 'repost_count': int,
  28. 'thumbnail': 'https://static.vvvvid.it/img/zoomin/28CA2409-E663-34F0-2B02E72356556EA3_500k.jpg',
  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. 'info_dict': {
  37. 'id': '482493',
  38. 'ext': 'mp4',
  39. 'title': 'Episodio 01',
  40. },
  41. 'params': {
  42. 'skip_download': True,
  43. },
  44. 'skip': 'Every video/rcs is not working even in real website',
  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': '@EMOTIONLabelChannel',
  56. 'uploader': 'EMOTION Label Channel',
  57. 'episode_id': '3115',
  58. 'view_count': int,
  59. 'like_count': int,
  60. 'repost_count': int,
  61. 'availability': str,
  62. 'categories': list,
  63. 'age_limit': 0,
  64. 'channel': 'EMOTION Label Channel',
  65. 'channel_follower_count': int,
  66. 'channel_id': 'UCQ5URCSs1f5Cz9rh-cDGxNQ',
  67. 'channel_url': 'https://www.youtube.com/channel/UCQ5URCSs1f5Cz9rh-cDGxNQ',
  68. 'comment_count': int,
  69. 'duration': 133,
  70. 'episode': 'Trailer',
  71. 'heatmap': list,
  72. 'live_status': 'not_live',
  73. 'playable_in_embed': True,
  74. 'season_id': '406',
  75. 'series': 'One-Punch Man',
  76. 'tags': list,
  77. 'uploader_url': 'https://www.youtube.com/@EMOTIONLabelChannel',
  78. 'thumbnail': 'https://i.ytimg.com/vi/RzmFKUDOUgw/maxresdefault.jpg',
  79. },
  80. 'params': {
  81. 'skip_download': True,
  82. },
  83. }, {
  84. # video_type == 'video/dash'
  85. 'url': 'https://www.vvvvid.it/show/844/le-bizzarre-avventure-di-jojo-vento-aureo/938/527551/golden-wind',
  86. 'info_dict': {
  87. 'id': '527551',
  88. 'ext': 'mp4',
  89. 'title': 'Golden Wind',
  90. 'duration': 1430,
  91. 'series': 'Le bizzarre avventure di Jojo - Vento Aureo',
  92. 'season_id': '938',
  93. 'episode': 'Golden Wind',
  94. 'episode_number': 1,
  95. 'episode_id': '9089',
  96. 'view_count': int,
  97. 'like_count': int,
  98. 'repost_count': int,
  99. 'thumbnail': 'https://static.vvvvid.it/img/thumbs/Dynit/Jojo/Jojo_S05Ep01-t.jpg',
  100. 'season': 'Season 5',
  101. 'season_number': 5,
  102. },
  103. 'params': {
  104. 'skip_download': True,
  105. 'format': 'mp4',
  106. },
  107. }, {
  108. 'url': 'https://www.vvvvid.it/show/434/perche-dovrei-guardarlo-di-dario-moccia/437/489048',
  109. 'only_matching': True,
  110. }]
  111. _conn_id = None
  112. @functools.cached_property
  113. def _headers(self):
  114. return {
  115. **self.geo_verification_headers(),
  116. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.50 Safari/537.37',
  117. }
  118. def _real_initialize(self):
  119. self._conn_id = self._download_json(
  120. 'https://www.vvvvid.it/user/login',
  121. None, headers=self._headers)['data']['conn_id']
  122. def _download_info(self, show_id, path, video_id, fatal=True, query=None):
  123. q = {
  124. 'conn_id': self._conn_id,
  125. }
  126. if query:
  127. q.update(query)
  128. response = self._download_json(
  129. f'https://www.vvvvid.it/vvvvid/ondemand/{show_id}/{path}',
  130. video_id, headers=self._headers, query=q, fatal=fatal)
  131. if not (response or fatal):
  132. return
  133. if response.get('result') == 'error':
  134. raise ExtractorError('{} said: {}'.format(
  135. self.IE_NAME, response['message']), expected=True)
  136. return response['data']
  137. def _extract_common_video_info(self, video_data):
  138. return {
  139. 'thumbnail': video_data.get('thumbnail'),
  140. 'episode_id': str_or_none(video_data.get('id')),
  141. }
  142. def _real_extract(self, url):
  143. show_id, season_id, video_id = self._match_valid_url(url).groups()
  144. response = self._download_info(
  145. show_id, f'season/{season_id}',
  146. video_id, query={'video_id': video_id})
  147. vid = int(video_id)
  148. video_data = next(filter(
  149. lambda episode: episode.get('video_id') == vid, response))
  150. title = video_data['title']
  151. formats = []
  152. # vvvvid embed_info decryption algorithm is reverse engineered from function $ds(h) at vvvvid.js
  153. def ds(h):
  154. g = 'MNOPIJKL89+/4567UVWXQRSTEFGHABCDcdefYZabstuvopqr0123wxyzklmnghij'
  155. def f(m):
  156. l = []
  157. o = 0
  158. b = False
  159. m_len = len(m)
  160. while ((not b) and o < m_len):
  161. n = m[o] << 2
  162. o += 1
  163. k = -1
  164. j = -1
  165. if o < m_len:
  166. n += m[o] >> 4
  167. o += 1
  168. if o < m_len:
  169. k = (m[o - 1] << 4) & 255
  170. k += m[o] >> 2
  171. o += 1
  172. if o < m_len:
  173. j = (m[o - 1] << 6) & 255
  174. j += m[o]
  175. o += 1
  176. else:
  177. b = True
  178. else:
  179. b = True
  180. else:
  181. b = True
  182. l.append(n)
  183. if k != -1:
  184. l.append(k)
  185. if j != -1:
  186. l.append(j)
  187. return l
  188. c = []
  189. for e in h:
  190. c.append(g.index(e))
  191. c_len = len(c)
  192. for e in range(c_len * 2 - 1, -1, -1):
  193. a = c[e % c_len] ^ c[(e + 1) % c_len]
  194. c[e % c_len] = a
  195. c = f(c)
  196. d = ''
  197. for e in c:
  198. d += chr(e)
  199. return d
  200. info = {}
  201. def metadata_from_url(r_url):
  202. if not info and r_url:
  203. mobj = re.search(r'_(?:S(\d+))?Ep(\d+)', r_url)
  204. if mobj:
  205. info['episode_number'] = int(mobj.group(2))
  206. season_number = mobj.group(1)
  207. if season_number:
  208. info['season_number'] = int(season_number)
  209. video_type = video_data.get('video_type')
  210. is_youtube = False
  211. for quality in ('', '_sd'):
  212. embed_code = video_data.get('embed_info' + quality)
  213. if not embed_code:
  214. continue
  215. embed_code = ds(embed_code)
  216. if video_type == 'video/kenc':
  217. embed_code = re.sub(r'https?(://[^/]+)/z/', r'https\1/i/', embed_code).replace('/manifest.f4m', '/master.m3u8')
  218. kenc = self._download_json(
  219. 'https://www.vvvvid.it/kenc', video_id, query={
  220. 'action': 'kt',
  221. 'conn_id': self._conn_id,
  222. 'url': embed_code,
  223. }, fatal=False) or {}
  224. kenc_message = kenc.get('message')
  225. if kenc_message:
  226. embed_code += '?' + ds(kenc_message)
  227. formats.extend(self._extract_m3u8_formats(
  228. embed_code, video_id, 'mp4', m3u8_id='hls', fatal=False))
  229. elif video_type == 'video/rcs':
  230. formats.extend(self._extract_akamai_formats(embed_code, video_id))
  231. elif video_type == 'video/youtube':
  232. info.update({
  233. '_type': 'url_transparent',
  234. 'ie_key': YoutubeIE.ie_key(),
  235. 'url': embed_code,
  236. })
  237. is_youtube = True
  238. break
  239. elif video_type == 'video/dash':
  240. formats.extend(self._extract_m3u8_formats(
  241. embed_code, video_id, 'mp4', m3u8_id='hls', fatal=False))
  242. else:
  243. formats.extend(self._extract_wowza_formats(
  244. f'http://sb.top-ix.org/videomg/_definst_/mp4:{embed_code}/playlist.m3u8', video_id, skip_protocols=['f4m']))
  245. metadata_from_url(embed_code)
  246. if not is_youtube:
  247. info['formats'] = formats
  248. metadata_from_url(video_data.get('thumbnail'))
  249. info.update(self._extract_common_video_info(video_data))
  250. info.update({
  251. 'id': video_id,
  252. 'title': title,
  253. 'duration': int_or_none(video_data.get('length')),
  254. 'series': video_data.get('show_title'),
  255. 'season_id': season_id,
  256. 'episode': title,
  257. 'view_count': int_or_none(video_data.get('views')),
  258. 'like_count': int_or_none(video_data.get('video_likes')),
  259. 'repost_count': int_or_none(video_data.get('video_shares')),
  260. })
  261. return info
  262. class VVVVIDShowIE(VVVVIDIE): # XXX: Do not subclass from concrete IE
  263. _VALID_URL = rf'(?P<base_url>{VVVVIDIE._VALID_URL_BASE}(?P<id>\d+)(?:/(?P<show_title>[^/?&#]+))?)/?(?:[?#&]|$)'
  264. _TESTS = [{
  265. 'url': 'https://www.vvvvid.it/show/156/psyco-pass',
  266. 'info_dict': {
  267. 'id': '156',
  268. 'title': 'Psycho-Pass',
  269. 'description': 'md5:94d572c0bd85894b193b8aebc9a3a806',
  270. },
  271. 'playlist_count': 46,
  272. }, {
  273. 'url': 'https://www.vvvvid.it/show/156',
  274. 'only_matching': True,
  275. }]
  276. def _real_extract(self, url):
  277. base_url, show_id, show_title = self._match_valid_url(url).groups()
  278. seasons = self._download_info(
  279. show_id, 'seasons/', show_title)
  280. show_info = self._download_info(
  281. show_id, 'info/', show_title, fatal=False)
  282. if not show_title:
  283. base_url += '/title'
  284. entries = []
  285. for season in (seasons or []):
  286. episodes = season.get('episodes') or []
  287. playlist_title = season.get('name') or show_info.get('title')
  288. for episode in episodes:
  289. if episode.get('playable') is False:
  290. continue
  291. season_id = str_or_none(episode.get('season_id'))
  292. video_id = str_or_none(episode.get('video_id'))
  293. if not (season_id and video_id):
  294. continue
  295. info = self._extract_common_video_info(episode)
  296. info.update({
  297. '_type': 'url_transparent',
  298. 'ie_key': VVVVIDIE.ie_key(),
  299. 'url': '/'.join([base_url, season_id, video_id]),
  300. 'title': episode.get('title'),
  301. 'description': episode.get('description'),
  302. 'season_id': season_id,
  303. 'playlist_title': playlist_title,
  304. })
  305. entries.append(info)
  306. return self.playlist_result(
  307. entries, show_id, show_info.get('title'), show_info.get('description'))