3 from .common
import InfoExtractor
16 from ..utils
.traversal
import (
22 class RutubeBaseIE(InfoExtractor
):
23 def _download_api_info(self
, video_id
, query
=None):
26 query
['format'] = 'json'
27 return self
._download
_json
(
28 f
'https://rutube.ru/api/video/{video_id}/',
29 video_id
, 'Downloading video JSON',
30 'Unable to download video JSON', query
=query
)
32 def _extract_info(self
, video
, video_id
=None, require_title
=True):
33 title
= video
['title'] if require_title
else video
.get('title')
35 age_limit
= video
.get('is_adult')
36 if age_limit
is not None:
37 age_limit
= 18 if age_limit
is True else 0
39 uploader_id
= try_get(video
, lambda x
: x
['author']['id'])
40 category
= try_get(video
, lambda x
: x
['category']['name'])
41 description
= video
.get('description')
42 duration
= int_or_none(video
.get('duration'))
45 'id': video
.get('id') or video_id
if video_id
else video
['id'],
47 'description': description
,
48 'thumbnail': video
.get('thumbnail_url'),
50 'uploader': try_get(video
, lambda x
: x
['author']['name']),
51 'uploader_id': str(uploader_id
) if uploader_id
else None,
52 'timestamp': unified_timestamp(video
.get('created_ts')),
53 'categories': [category
] if category
else None,
54 'age_limit': age_limit
,
55 'view_count': int_or_none(video
.get('hits')),
56 'comment_count': int_or_none(video
.get('comments_count')),
57 'is_live': bool_or_none(video
.get('is_livestream')),
58 'chapters': self
._extract
_chapters
_from
_description
(description
, duration
),
61 def _download_and_extract_info(self
, video_id
, query
=None):
62 return self
._extract
_info
(
63 self
._download
_api
_info
(video_id
, query
=query
), video_id
)
65 def _download_api_options(self
, video_id
, query
=None):
68 query
['format'] = 'json'
69 return self
._download
_json
(
70 f
'https://rutube.ru/api/play/options/{video_id}/',
71 video_id
, 'Downloading options JSON',
72 'Unable to download options JSON',
73 headers
=self
.geo_verification_headers(), query
=query
)
75 def _extract_formats_and_subtitles(self
, options
, video_id
):
78 for format_id
, format_url
in options
['video_balancer'].items():
79 ext
= determine_ext(format_url
)
81 fmts
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
82 format_url
, video_id
, 'mp4', m3u8_id
=format_id
, fatal
=False)
84 self
._merge
_subtitles
(subs
, target
=subtitles
)
86 formats
.extend(self
._extract
_f
4m
_formats
(
87 format_url
, video_id
, f4m_id
=format_id
, fatal
=False))
91 'format_id': format_id
,
93 for hls_url
in traverse_obj(options
, ('live_streams', 'hls', ..., 'url', {url_or_none}
)):
94 fmts
, subs
= self
._extract
_m
3u8_formats
_and
_subtitles
(
95 hls_url
, video_id
, 'mp4', fatal
=False, m3u8_id
='hls')
97 self
._merge
_subtitles
(subs
, target
=subtitles
)
98 self
._merge
_subtitles
(traverse_obj(options
, ('captions', ..., {
101 'name': ('langTitle', {str}
),
102 }, all
, {subs_list_to_dict(lang
='ru')})), target
=subtitles
)
103 return formats
, subtitles
105 def _download_and_extract_formats_and_subtitles(self
, video_id
, query
=None):
106 return self
._extract
_formats
_and
_subtitles
(
107 self
._download
_api
_options
(video_id
, query
=query
), video_id
)
110 class RutubeIE(RutubeBaseIE
):
112 IE_DESC
= 'Rutube videos'
113 _VALID_URL
= r
'https?://rutube\.ru/(?:(?:live/)?video(?:/private)?|(?:play/)?embed)/(?P<id>[\da-z]{32})'
114 _EMBED_REGEX
= [r
'<iframe[^>]+?src=(["\'])(?P
<url
>(?
:https?
:)?
//rutube\
.ru
/(?
:play
/)?embed
/[\da
-z
]{32}
.*?
)\
1']
117 'url
': 'https
://rutube
.ru
/video
/3eac3b4561676c17df9132a9a1e62e3e
/',
118 'md5
': '3d73fdfe5bb81b9aef139e22ef3de26a
',
120 'id': '3eac3b4561676c17df9132a9a1e62e3e
',
122 'title
': 'Раненный кенгуру забежал в аптеку
',
123 'description
': 'http
://www
.ntdtv
.ru
',
125 'uploader
': 'NTDRussian
',
126 'uploader_id
': '29790',
127 'timestamp
': 1381943602,
128 'upload_date
': '20131016',
131 'thumbnail
': 'https
://pic
.rutubelist
.ru
/video
/d2
/a0
/d2a0aec998494a396deafc7ba2c82add
.jpg
',
132 'categories
': ['Новости и СМИ
'],
136 'url
': 'https
://rutube
.ru
/play
/embed
/a10e53b86e8f349080f718582ce4c661
',
137 'only_matching
': True,
139 'url
': 'https
://rutube
.ru
/embed
/a10e53b86e8f349080f718582ce4c661
',
140 'only_matching
': True,
142 'url
': 'https
://rutube
.ru
/video
/3eac3b4561676c17df9132a9a1e62e3e
/?pl_id
=4252',
143 'only_matching
': True,
145 'url
': 'https
://rutube
.ru
/video
/10b3a03fc01d5bbcc632a2f3514e8aab
/?pl_type
=source
',
146 'only_matching
': True,
148 'url
': 'https
://rutube
.ru
/video
/private
/884fb55f07a97ab673c7d654553e0f48
/?p
=x2QojCumHTS3rsKHWXN8Lg
',
149 'md5
': '4fce7b4fcc7b1bcaa3f45eb1e1ad0dd7
',
151 'id': '884fb55f07a97ab673c7d654553e0f48
',
153 'title
': 'Яцуноками
, Nioh2
',
154 'description
': 'Nioh2
: финал сражения с боссом Яцуноками
',
157 'uploader_id
': '24222106',
158 'timestamp
': 1670646232,
159 'upload_date
': '20221210',
162 'thumbnail
': 'https
://pic
.rutubelist
.ru
/video
/f2
/d4
/f2d42b54be0a6e69c1c22539e3152156
.jpg
',
163 'categories
': ['Видеоигры
'],
167 'url
': 'https
://rutube
.ru
/video
/c65b465ad0c98c89f3b25cb03dcc87c6
/',
169 'id': 'c65b465ad0c98c89f3b25cb03dcc87c6
',
171 'chapters
': 'count
:4',
172 'categories
': ['Бизнес и предпринимательство
'],
173 'description
': 'md5
:252feac1305257d8c1bab215cedde75d
',
174 'thumbnail
': 'https
://pic
.rutubelist
.ru
/video
/71/8f
/718f27425ea9706073eb80883dd3787b
.png
',
177 'uploader_id
': '23491359',
178 'timestamp
': 1677153329,
180 'upload_date
': '20230223',
181 'title
': 'Бизнес с нуля
: найм сотрудников
. Интервью с директором строительной компании
#1',
182 'uploader': 'Стас Быков',
185 'url': 'https://rutube.ru/live/video/c58f502c7bb34a8fcdd976b221fca292/',
187 'id': 'c58f502c7bb34a8fcdd976b221fca292',
189 'categories': ['Телепередачи'],
191 'thumbnail': 'https://pic.rutubelist.ru/video/14/19/14190807c0c48b40361aca93ad0867c7.jpg',
192 'live_status': 'is_live',
194 'uploader_id': '23460655',
195 'timestamp': 1652972968,
197 'upload_date': '20220519',
198 'title': r
're:Первый канал. Прямой эфир \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
199 'uploader': 'Первый канал',
202 'url': 'https://rutube.ru/play/embed/03a9cb54bac3376af4c5cb0f18444e01/',
204 'id': '03a9cb54bac3376af4c5cb0f18444e01',
208 'title': 'Церемония начала торгов акциями ПАО «ЕвроТранс»',
210 'upload_date': '20240829',
212 'uploader': 'MOEX - Московская биржа',
213 'timestamp': 1724946628,
214 'thumbnail': 'https://pic.rutubelist.ru/video/2e/24/2e241fddb459baf0fa54acfca44874f4.jpg',
216 'uploader_id': '38420507',
217 'categories': ['Интервью'],
220 'url': 'https://rutube.ru/video/5ab908fccfac5bb43ef2b1e4182256b0/',
221 'only_matching': True,
223 'url': 'https://rutube.ru/live/video/private/c58f502c7bb34a8fcdd976b221fca292/',
224 'only_matching': True,
227 def _real_extract(self
, url
):
228 video_id
= self
._match
_id
(url
)
229 query
= parse_qs(url
)
230 info
= self
._download
_and
_extract
_info
(video_id
, query
)
231 formats
, subtitles
= self
._download
_and
_extract
_formats
_and
_subtitles
(video_id
, query
)
235 'subtitles': subtitles
,
239 class RutubeEmbedIE(RutubeBaseIE
):
240 IE_NAME
= 'rutube:embed'
241 IE_DESC
= 'Rutube embedded videos'
242 _VALID_URL
= r
'https?://rutube\.ru/(?:video|play)/embed/(?P<id>[0-9]+)(?:[?#/]|$)'
245 'url': 'https://rutube.ru/video/embed/6722881?vk_puid37=&vk_puid38=',
247 'id': 'a10e53b86e8f349080f718582ce4c661',
249 'timestamp': 1387830582,
250 'upload_date': '20131223',
251 'uploader_id': '297833',
252 'uploader': 'subziro89 ILya',
253 'title': 'Мистический городок Эйри в Индиан 5 серия озвучка subziro89',
257 'description': 'md5:a5acea57bbc3ccdc3cacd1f11a014b5b',
259 'thumbnail': 'https://pic.rutubelist.ru/video/d3/03/d3031f4670a6e6170d88fb3607948418.jpg',
260 'categories': ['Сериалы'],
263 'skip_download': True,
266 'url': 'https://rutube.ru/play/embed/8083783',
267 'only_matching': True,
270 'url': 'https://rutube.ru/play/embed/10631925?p=IbAigKqWd1do4mjaM5XLIQ',
271 'only_matching': True,
274 def _real_extract(self
, url
):
275 embed_id
= self
._match
_id
(url
)
276 # Query may contain private videos token and should be passed to API
277 # requests (see #19163)
278 query
= parse_qs(url
)
279 options
= self
._download
_api
_options
(embed_id
, query
)
280 video_id
= options
['effective_video']
281 formats
, subtitles
= self
._extract
_formats
_and
_subtitles
(options
, video_id
)
282 info
= self
._download
_and
_extract
_info
(video_id
, query
)
284 'extractor_key': 'Rutube',
286 'subtitles': subtitles
,
291 class RutubePlaylistBaseIE(RutubeBaseIE
):
292 def _next_page_url(self
, page_num
, playlist_id
, *args
, **kwargs
):
293 return self
._PAGE
_TEMPLATE
% (playlist_id
, page_num
)
295 def _entries(self
, playlist_id
, *args
, **kwargs
):
297 for pagenum
in itertools
.count(1):
298 page
= self
._download
_json
(
299 next_page_url
or self
._next
_page
_url
(
300 pagenum
, playlist_id
, *args
, **kwargs
),
301 playlist_id
, f
'Downloading page {pagenum}')
303 results
= page
.get('results')
304 if not results
or not isinstance(results
, list):
307 for result
in results
:
308 video_url
= url_or_none(result
.get('video_url'))
311 entry
= self
._extract
_info
(result
, require_title
=False)
315 'ie_key': RutubeIE
.ie_key(),
319 next_page_url
= page
.get('next')
320 if not next_page_url
or not page
.get('has_next'):
323 def _extract_playlist(self
, playlist_id
, *args
, **kwargs
):
324 return self
.playlist_result(
325 self
._entries
(playlist_id
, *args
, **kwargs
),
326 playlist_id
, kwargs
.get('playlist_name'))
328 def _real_extract(self
, url
):
329 return self
._extract
_playlist
(self
._match
_id
(url
))
332 class RutubeTagsIE(RutubePlaylistBaseIE
):
333 IE_NAME
= 'rutube:tags'
334 IE_DESC
= 'Rutube tags'
335 _VALID_URL
= r
'https?://rutube\.ru/tags/video/(?P<id>\d+)'
337 'url': 'https://rutube.ru/tags/video/1800/',
341 'playlist_mincount': 68,
344 _PAGE_TEMPLATE
= 'https://rutube.ru/api/tags/video/%s/?page=%s&format=json'
347 class RutubeMovieIE(RutubePlaylistBaseIE
):
348 IE_NAME
= 'rutube:movie'
349 IE_DESC
= 'Rutube movies'
350 _VALID_URL
= r
'https?://rutube\.ru/metainfo/tv/(?P<id>\d+)'
352 _MOVIE_TEMPLATE
= 'https://rutube.ru/api/metainfo/tv/%s/?format=json'
353 _PAGE_TEMPLATE
= 'https://rutube.ru/api/metainfo/tv/%s/video?page=%s&format=json'
355 def _real_extract(self
, url
):
356 movie_id
= self
._match
_id
(url
)
357 movie
= self
._download
_json
(
358 self
._MOVIE
_TEMPLATE
% movie_id
, movie_id
,
359 'Downloading movie JSON')
360 return self
._extract
_playlist
(
361 movie_id
, playlist_name
=movie
.get('name'))
364 class RutubePersonIE(RutubePlaylistBaseIE
):
365 IE_NAME
= 'rutube:person'
366 IE_DESC
= 'Rutube person videos'
367 _VALID_URL
= r
'https?://rutube\.ru/video/person/(?P<id>\d+)'
369 'url': 'https://rutube.ru/video/person/313878/',
373 'playlist_mincount': 36,
376 _PAGE_TEMPLATE
= 'https://rutube.ru/api/video/person/%s/?page=%s&format=json'
379 class RutubePlaylistIE(RutubePlaylistBaseIE
):
380 IE_NAME
= 'rutube:playlist'
381 IE_DESC
= 'Rutube playlists'
382 _VALID_URL
= r
'https?://rutube\.ru/plst/(?P<id>\d+)'
384 'url': 'https://rutube.ru/plst/308547/',
388 'playlist_mincount': 22,
390 _PAGE_TEMPLATE
= 'https://rutube.ru/api/playlist/custom/%s/videos?page=%s&format=json'
393 class RutubeChannelIE(RutubePlaylistBaseIE
):
394 IE_NAME
= 'rutube:channel'
395 IE_DESC
= 'Rutube channel'
396 _VALID_URL
= r
'https?://rutube\.ru/(?:channel/(?P<id>\d+)|u/(?P<slug>\w+))(?:/(?P<section>videos|shorts|playlists))?'
398 'url': 'https://rutube.ru/channel/639184/videos/',
400 'id': '639184_videos',
402 'playlist_mincount': 129,
404 'url': 'https://rutube.ru/channel/25902603/shorts/',
406 'id': '25902603_shorts',
408 'playlist_mincount': 277,
410 'url': 'https://rutube.ru/channel/25902603/',
414 'playlist_mincount': 406,
416 'url': 'https://rutube.ru/u/rutube/videos/',
418 'id': '23704195_videos',
420 'playlist_mincount': 113,
423 _PAGE_TEMPLATE
= 'https://rutube.ru/api/video/person/%s/?page=%s&format=json&origin__type=%s'
425 def _next_page_url(self
, page_num
, playlist_id
, section
):
427 'videos': 'rtb,rst,ifrm,rspa',
431 return self
._PAGE
_TEMPLATE
% (playlist_id
, page_num
, origin_type
)
433 def _real_extract(self
, url
):
434 playlist_id
, slug
, section
= self
._match
_valid
_url
(url
).group('id', 'slug', 'section')
435 if section
== 'playlists':
436 raise UnsupportedError(url
)
438 webpage
= self
._download
_webpage
(url
, slug
)
439 redux_state
= self
._search
_json
(
440 r
'window\.reduxState\s*=', webpage
, 'redux state', slug
, transform_source
=js_to_json
)
441 playlist_id
= traverse_obj(redux_state
, (
442 'api', 'queries', lambda k
, _
: k
.startswith('channelIdBySlug'),
443 'data', 'channel_id', {int}
, {str_or_none}
, any
))
444 playlist
= self
._extract
_playlist
(playlist_id
, section
=section
)
446 playlist
['id'] = f
'{playlist_id}_{section}'