yandexvideo.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import itertools
  2. from .common import InfoExtractor
  3. from ..utils import (
  4. determine_ext,
  5. extract_attributes,
  6. int_or_none,
  7. lowercase_escape,
  8. parse_qs,
  9. traverse_obj,
  10. try_get,
  11. url_or_none,
  12. )
  13. class YandexVideoIE(InfoExtractor):
  14. _VALID_URL = r'''(?x)
  15. https?://
  16. (?:
  17. yandex\.ru(?:/(?:portal/(?:video|efir)|efir))?/?\?.*?stream_id=|
  18. frontend\.vh\.yandex\.ru/player/
  19. )
  20. (?P<id>(?:[\da-f]{32}|[\w-]{12}))
  21. '''
  22. _TESTS = [{
  23. 'url': 'https://yandex.ru/portal/video?stream_id=4dbb36ec4e0526d58f9f2dc8f0ecf374',
  24. 'info_dict': {
  25. 'id': '4dbb36ec4e0526d58f9f2dc8f0ecf374',
  26. 'ext': 'mp4',
  27. 'title': 'Русский Вудсток - главный рок-фест в истории СССР / вДудь',
  28. 'description': 'md5:7d6b8d4bc4a3b9a56499916c1ea5b5fa',
  29. 'thumbnail': r're:^https?://',
  30. 'timestamp': 1549972939,
  31. 'duration': 5575,
  32. 'age_limit': 18,
  33. 'upload_date': '20190212',
  34. 'view_count': int,
  35. 'like_count': int,
  36. 'dislike_count': int,
  37. },
  38. 'params': {'skip_download': 'm3u8'},
  39. }, {
  40. 'url': 'https://yandex.ru/portal/efir?stream_id=4dbb262b4fe5cf15a215de4f34eee34d&from=morda',
  41. 'only_matching': True,
  42. }, {
  43. 'url': 'https://yandex.ru/?stream_id=4dbb262b4fe5cf15a215de4f34eee34d',
  44. 'only_matching': True,
  45. }, {
  46. 'url': 'https://frontend.vh.yandex.ru/player/4dbb262b4fe5cf15a215de4f34eee34d?from=morda',
  47. 'only_matching': True,
  48. }, {
  49. # vod-episode, series episode
  50. 'url': 'https://yandex.ru/portal/video?stream_id=45b11db6e4b68797919c93751a938cee',
  51. 'only_matching': True,
  52. }, {
  53. # episode, sports
  54. 'url': 'https://yandex.ru/?stream_channel=1538487871&stream_id=4132a07f71fb0396be93d74b3477131d',
  55. 'only_matching': True,
  56. }, {
  57. # DASH with DRM
  58. 'url': 'https://yandex.ru/portal/video?from=morda&stream_id=485a92d94518d73a9d0ff778e13505f8',
  59. 'only_matching': True,
  60. }, {
  61. 'url': 'https://yandex.ru/efir?stream_active=watching&stream_id=v7a2dZ-v5mSI&from_block=efir_newtab',
  62. 'only_matching': True,
  63. }]
  64. def _real_extract(self, url):
  65. video_id = self._match_id(url)
  66. player = try_get((self._download_json(
  67. 'https://frontend.vh.yandex.ru/graphql', video_id, data=('''{
  68. player(content_id: "%s") {
  69. computed_title
  70. content_url
  71. description
  72. dislikes
  73. duration
  74. likes
  75. program_title
  76. release_date
  77. release_date_ut
  78. release_year
  79. restriction_age
  80. season
  81. start_time
  82. streams
  83. thumbnail
  84. title
  85. views_count
  86. }
  87. }''' % video_id).encode(), fatal=False)), lambda x: x['player']['content']) # noqa: UP031
  88. if not player or player.get('error'):
  89. player = self._download_json(
  90. f'https://frontend.vh.yandex.ru/v23/player/{video_id}.json',
  91. video_id, query={
  92. 'stream_options': 'hires',
  93. 'disable_trackings': 1,
  94. })
  95. content = player['content']
  96. title = content.get('title') or content['computed_title']
  97. formats = []
  98. streams = content.get('streams') or []
  99. streams.append({'url': content.get('content_url')})
  100. for stream in streams:
  101. content_url = url_or_none(stream.get('url'))
  102. if not content_url:
  103. continue
  104. ext = determine_ext(content_url)
  105. if ext == 'ismc':
  106. continue
  107. elif ext == 'm3u8':
  108. formats.extend(self._extract_m3u8_formats(
  109. content_url, video_id, 'mp4',
  110. 'm3u8_native', m3u8_id='hls', fatal=False))
  111. elif ext == 'mpd':
  112. formats.extend(self._extract_mpd_formats(
  113. content_url, video_id, mpd_id='dash', fatal=False))
  114. else:
  115. formats.append({'url': content_url})
  116. timestamp = (int_or_none(content.get('release_date'))
  117. or int_or_none(content.get('release_date_ut'))
  118. or int_or_none(content.get('start_time')))
  119. season = content.get('season') or {}
  120. return {
  121. 'id': video_id,
  122. 'title': title,
  123. 'description': content.get('description'),
  124. 'thumbnail': content.get('thumbnail'),
  125. 'timestamp': timestamp,
  126. 'duration': int_or_none(content.get('duration')),
  127. 'series': content.get('program_title'),
  128. 'age_limit': int_or_none(content.get('restriction_age')),
  129. 'view_count': int_or_none(content.get('views_count')),
  130. 'like_count': int_or_none(content.get('likes')),
  131. 'dislike_count': int_or_none(content.get('dislikes')),
  132. 'season_number': int_or_none(season.get('season_number')),
  133. 'season_id': season.get('id'),
  134. 'release_year': int_or_none(content.get('release_year')),
  135. 'formats': formats,
  136. }
  137. class YandexVideoPreviewIE(InfoExtractor):
  138. _VALID_URL = r'https?://(?:www\.)?yandex\.\w{2,3}(?:\.(?:am|ge|il|tr))?/video/preview(?:/?\?.*?filmId=|/)(?P<id>\d+)'
  139. _TESTS = [{ # Odnoklassniki
  140. 'url': 'https://yandex.ru/video/preview/?filmId=10682852472978372885&text=summer',
  141. 'info_dict': {
  142. 'id': '1352565459459',
  143. 'ext': 'mp4',
  144. 'like_count': int,
  145. 'upload_date': '20191202',
  146. 'age_limit': 0,
  147. 'duration': 196,
  148. 'thumbnail': 'https://i.mycdn.me/videoPreview?id=544866765315&type=37&idx=13&tkn=TY5qjLYZHxpmcnK8U2LgzYkgmaU&fn=external_8',
  149. 'uploader_id': '481054701571',
  150. 'title': 'LOFT - summer, summer, summer HD',
  151. 'uploader': 'АРТЁМ КУДРОВ',
  152. },
  153. }, { # youtube
  154. 'url': 'https://yandex.ru/video/preview/?filmId=4479424425337895262&source=main_redirect&text=видео&utm_source=main_stripe_big',
  155. 'only_matching': True,
  156. }, { # YandexVideo
  157. 'url': 'https://yandex.ru/video/preview/5275069442094787341',
  158. 'only_matching': True,
  159. }, { # youtube
  160. 'url': 'https://yandex.ru/video/preview/?filmId=16658118429797832897&from=tabbar&p=1&text=%D0%BF%D1%80%D0%BE%D1%81%D0%BC%D0%BE%D1%82%D1%80+%D1%84%D1%80%D0%B0%D0%B3%D0%BC%D0%B5%D0%BD%D1%82%D0%B0+%D0%BC%D0%B0%D0%BB%D0%B5%D0%BD%D1%8C%D0%BA%D0%B8%D0%B9+%D0%BF%D1%80%D0%B8%D0%BD%D1%86+%D0%BC%D1%8B+%D0%B2+%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D0%B5+%D0%B7%D0%B0+%D1%82%D0%B5%D1%85+%D0%BA%D0%BE%D0%B3%D0%BE+%D0%BF%D1%80%D0%B8%D1%80%D1%83%D1%87%D0%B8%D0%BB%D0%B8',
  161. 'only_matching': True,
  162. }, { # Odnoklassniki
  163. 'url': 'https://yandex.ru/video/preview/?text=Francis%20Lai%20-%20Le%20Bon%20Et%20Les%20MC)chants&path=wizard&parent-reqid=1643208087979310-1481782809207673478-sas3-0931-2f9-sas-l7-balancer-8080-BAL-9380&wiz_type=vital&filmId=12508152936505397283',
  164. 'only_matching': True,
  165. }, { # Odnoklassniki
  166. 'url': 'https://yandex.com/video/preview/?text=dossier%2051%20film%201978&path=yandex_search&parent-reqid=1664361087754492-8727541069609384458-sas2-0340-sas-l7-balancer-8080-BAL-8045&noreask=1&from_type=vast&filmId=5794987234584444632',
  167. 'only_matching': True,
  168. }]
  169. def _real_extract(self, url):
  170. video_id = self._match_id(url)
  171. webpage = self._download_webpage(url, video_id)
  172. data_raw = self._search_regex(r'window.Ya.__inline_params__\s*=\s*JSON.parse\(\'([^"]+?\\u0022video\\u0022:[^"]+?})\'\);', webpage, 'data_raw')
  173. data_json = self._parse_json(data_raw, video_id, transform_source=lowercase_escape)
  174. return self.url_result(data_json['video']['url'])
  175. class ZenYandexIE(InfoExtractor):
  176. _VALID_URL = r'https?://(zen\.yandex|dzen)\.ru(?:/video)?/(media|watch)/(?:(?:id/[^/]+/|[^/]+/)(?:[a-z0-9-]+)-)?(?P<id>[a-z0-9-]+)'
  177. _TESTS = [{
  178. 'url': 'https://zen.yandex.ru/media/id/606fd806cc13cb3c58c05cf5/vot-eto-focus-dedy-morozy-na-gidrociklah-60c7c443da18892ebfe85ed7',
  179. 'info_dict': {
  180. 'id': '60c7c443da18892ebfe85ed7',
  181. 'ext': 'mp4',
  182. 'title': 'ВОТ ЭТО Focus. Деды Морозы на гидроциклах',
  183. 'description': 'md5:8684912f6086f298f8078d4af0e8a600',
  184. 'thumbnail': 're:^https://avatars.dzeninfra.ru/',
  185. 'uploader': 'AcademeG DailyStream',
  186. },
  187. 'params': {
  188. 'skip_download': 'm3u8',
  189. 'format': 'bestvideo',
  190. },
  191. 'skip': 'The page does not exist',
  192. }, {
  193. 'url': 'https://dzen.ru/media/id/606fd806cc13cb3c58c05cf5/vot-eto-focus-dedy-morozy-na-gidrociklah-60c7c443da18892ebfe85ed7',
  194. 'info_dict': {
  195. 'id': '60c7c443da18892ebfe85ed7',
  196. 'ext': 'mp4',
  197. 'title': 'ВОТ ЭТО Focus. Деды Морозы на гидроциклах',
  198. 'description': 'md5:8684912f6086f298f8078d4af0e8a600',
  199. 'thumbnail': r're:^https://avatars\.dzeninfra\.ru/',
  200. 'uploader': 'AcademeG DailyStream',
  201. 'upload_date': '20191111',
  202. 'timestamp': 1573465585,
  203. },
  204. 'params': {'skip_download': 'm3u8'},
  205. }, {
  206. 'url': 'https://zen.yandex.ru/video/watch/6002240ff8b1af50bb2da5e3',
  207. 'info_dict': {
  208. 'id': '6002240ff8b1af50bb2da5e3',
  209. 'ext': 'mp4',
  210. 'title': 'Извержение вулкана из спичек: зрелищный опыт',
  211. 'description': 'md5:053ad3c61b5596d510c9a199dc8ee633',
  212. 'thumbnail': r're:^https://avatars\.dzeninfra\.ru/',
  213. 'uploader': 'TechInsider',
  214. 'timestamp': 1611378221,
  215. 'upload_date': '20210123',
  216. },
  217. 'params': {'skip_download': 'm3u8'},
  218. }, {
  219. 'url': 'https://dzen.ru/video/watch/6002240ff8b1af50bb2da5e3',
  220. 'info_dict': {
  221. 'id': '6002240ff8b1af50bb2da5e3',
  222. 'ext': 'mp4',
  223. 'title': 'Извержение вулкана из спичек: зрелищный опыт',
  224. 'description': 'md5:053ad3c61b5596d510c9a199dc8ee633',
  225. 'thumbnail': 're:^https://avatars.dzeninfra.ru/',
  226. 'uploader': 'TechInsider',
  227. 'upload_date': '20210123',
  228. 'timestamp': 1611378221,
  229. },
  230. 'params': {'skip_download': 'm3u8'},
  231. }, {
  232. 'url': 'https://zen.yandex.ru/media/id/606fd806cc13cb3c58c05cf5/novyi-samsung-fold-3-moskvich-barahlit-612f93b7f8d48e7e945792a2?from=channel&rid=2286618386.482.1630817595976.42360',
  233. 'only_matching': True,
  234. }, {
  235. 'url': 'https://dzen.ru/media/id/606fd806cc13cb3c58c05cf5/novyi-samsung-fold-3-moskvich-barahlit-612f93b7f8d48e7e945792a2?from=channel&rid=2286618386.482.1630817595976.42360',
  236. 'only_matching': True,
  237. }]
  238. def _real_extract(self, url):
  239. video_id = self._match_id(url)
  240. webpage = self._download_webpage(url, video_id)
  241. redirect = self._search_json(r'var it\s*=', webpage, 'redirect', id, default={}).get('retpath')
  242. if redirect:
  243. video_id = self._match_id(redirect)
  244. webpage = self._download_webpage(redirect, video_id, note='Redirecting')
  245. data_json = self._search_json(
  246. r'("data"\s*:|data\s*=)', webpage, 'metadata', video_id, contains_pattern=r'{["\']_*serverState_*video.+}')
  247. serverstate = self._search_regex(r'(_+serverState_+video-site_[^_]+_+)', webpage, 'server state')
  248. uploader = self._search_regex(r'(<a\s*class=["\']card-channel-link[^"\']+["\'][^>]+>)',
  249. webpage, 'uploader', default='<a>')
  250. uploader_name = extract_attributes(uploader).get('aria-label')
  251. item_id = traverse_obj(data_json, (serverstate, 'videoViewer', 'openedItemId', {str}))
  252. video_json = traverse_obj(data_json, (serverstate, 'videoViewer', 'items', item_id, {dict})) or {}
  253. formats, subtitles = [], {}
  254. for s_url in traverse_obj(video_json, ('video', 'streams', ..., {url_or_none})):
  255. ext = determine_ext(s_url)
  256. if ext == 'mpd':
  257. fmts, subs = self._extract_mpd_formats_and_subtitles(s_url, video_id, mpd_id='dash')
  258. elif ext == 'm3u8':
  259. fmts, subs = self._extract_m3u8_formats_and_subtitles(s_url, video_id, 'mp4')
  260. formats.extend(fmts)
  261. subtitles = self._merge_subtitles(subtitles, subs)
  262. return {
  263. 'id': video_id,
  264. 'title': video_json.get('title') or self._og_search_title(webpage),
  265. 'formats': formats,
  266. 'subtitles': subtitles,
  267. 'duration': int_or_none(video_json.get('duration')),
  268. 'view_count': int_or_none(video_json.get('views')),
  269. 'timestamp': int_or_none(video_json.get('publicationDate')),
  270. 'uploader': uploader_name or data_json.get('authorName') or try_get(data_json, lambda x: x['publisher']['name']),
  271. 'description': video_json.get('description') or self._og_search_description(webpage),
  272. 'thumbnail': self._og_search_thumbnail(webpage) or try_get(data_json, lambda x: x['og']['imageUrl']),
  273. }
  274. class ZenYandexChannelIE(InfoExtractor):
  275. _VALID_URL = r'https?://(zen\.yandex|dzen)\.ru/(?!media|video)(?:id/)?(?P<id>[a-z0-9-_]+)'
  276. _TESTS = [{
  277. 'url': 'https://zen.yandex.ru/tok_media',
  278. 'info_dict': {
  279. 'id': 'tok_media',
  280. 'title': 'СПЕКТР',
  281. 'description': 'md5:a9e5b3c247b7fe29fd21371a428bcf56',
  282. },
  283. 'playlist_mincount': 169,
  284. 'skip': 'The page does not exist',
  285. }, {
  286. 'url': 'https://dzen.ru/tok_media',
  287. 'info_dict': {
  288. 'id': 'tok_media',
  289. 'title': 'СПЕКТР',
  290. 'description': 'md5:a9e5b3c247b7fe29fd21371a428bcf56',
  291. },
  292. 'playlist_mincount': 169,
  293. 'skip': 'The page does not exist',
  294. }, {
  295. 'url': 'https://zen.yandex.ru/id/606fd806cc13cb3c58c05cf5',
  296. 'info_dict': {
  297. 'id': '606fd806cc13cb3c58c05cf5',
  298. 'description': 'md5:517b7c97d8ca92e940f5af65448fd928',
  299. 'title': 'AcademeG DailyStream',
  300. },
  301. 'playlist_mincount': 657,
  302. }, {
  303. # Test that the playlist extractor finishes extracting when the
  304. # channel has less than one page
  305. 'url': 'https://zen.yandex.ru/jony_me',
  306. 'info_dict': {
  307. 'id': 'jony_me',
  308. 'description': 'md5:ce0a5cad2752ab58701b5497835b2cc5',
  309. 'title': 'JONY ',
  310. },
  311. 'playlist_count': 18,
  312. }, {
  313. # Test that the playlist extractor finishes extracting when the
  314. # channel has more than one page of entries
  315. 'url': 'https://zen.yandex.ru/tatyanareva',
  316. 'info_dict': {
  317. 'id': 'tatyanareva',
  318. 'description': 'md5:40a1e51f174369ec3ba9d657734ac31f',
  319. 'title': 'Татьяна Рева',
  320. 'entries': 'maxcount:200',
  321. },
  322. 'playlist_mincount': 46,
  323. }, {
  324. 'url': 'https://dzen.ru/id/606fd806cc13cb3c58c05cf5',
  325. 'info_dict': {
  326. 'id': '606fd806cc13cb3c58c05cf5',
  327. 'title': 'AcademeG DailyStream',
  328. 'description': 'md5:517b7c97d8ca92e940f5af65448fd928',
  329. },
  330. 'playlist_mincount': 657,
  331. }]
  332. def _entries(self, item_id, server_state_json, server_settings_json):
  333. items = (traverse_obj(server_state_json, ('feed', 'items', ...))
  334. or traverse_obj(server_settings_json, ('exportData', 'items', ...)))
  335. more = (traverse_obj(server_state_json, ('links', 'more'))
  336. or traverse_obj(server_settings_json, ('exportData', 'more', 'link')))
  337. next_page_id = None
  338. for page in itertools.count(1):
  339. for item in items or []:
  340. if item.get('type') != 'gif':
  341. continue
  342. video_id = traverse_obj(item, 'publication_id', 'publicationId') or ''
  343. yield self.url_result(item['link'], ZenYandexIE, video_id.split(':')[-1])
  344. current_page_id = next_page_id
  345. next_page_id = traverse_obj(parse_qs(more), ('next_page_id', -1))
  346. if not all((more, items, next_page_id, next_page_id != current_page_id)):
  347. break
  348. data = self._download_json(more, item_id, note=f'Downloading Page {page}')
  349. items, more = data.get('items'), traverse_obj(data, ('more', 'link'))
  350. def _real_extract(self, url):
  351. item_id = self._match_id(url)
  352. webpage = self._download_webpage(url, item_id)
  353. redirect = self._search_json(
  354. r'var it\s*=', webpage, 'redirect', item_id, default={}).get('retpath')
  355. if redirect:
  356. item_id = self._match_id(redirect)
  357. webpage = self._download_webpage(redirect, item_id, note='Redirecting')
  358. data = self._search_json(
  359. r'("data"\s*:|data\s*=)', webpage, 'channel data', item_id, contains_pattern=r'{\"__serverState__.+}')
  360. server_state_json = traverse_obj(data, lambda k, _: k.startswith('__serverState__'), get_all=False)
  361. server_settings_json = traverse_obj(data, lambda k, _: k.startswith('__serverSettings__'), get_all=False)
  362. return self.playlist_result(
  363. self._entries(item_id, server_state_json, server_settings_json),
  364. item_id, traverse_obj(server_state_json, ('channel', 'source', 'title')),
  365. traverse_obj(server_state_json, ('channel', 'source', 'description')))