3 from .common
import InfoExtractor
14 from ..utils
.traversal
import traverse_obj
17 class NuumBaseIE(InfoExtractor
):
18 def _call_api(self
, path
, video_id
, description
, query
={}):
19 response
= self
._download
_json
(
20 f
'https://nuum.ru/api/v2/{path}', video_id
, query
=query
,
21 note
=f
'Downloading {description} metadata',
22 errnote
=f
'Unable to download {description} metadata')
23 if error
:= response
.get('error'):
24 raise ExtractorError(f
'API returned error: {error!r}')
25 return response
['result']
27 def _get_channel_info(self
, channel_name
):
28 return self
._call
_api
(
29 'broadcasts/public', video_id
=channel_name
, description
='channel',
32 'channel_name': channel_name
,
33 'with_deleted': 'true',
36 def _parse_video_data(self
, container
, extract_formats
=True):
37 stream
= traverse_obj(container
, ('media_container_streams', 0, {dict}
)) or {}
38 media
= traverse_obj(stream
, ('stream_media', 0, {dict}
)) or {}
39 media_url
= traverse_obj(media
, (
40 'media_meta', ('media_archive_url', 'media_url'), {url_or_none}
), get_all
=False)
42 video_id
= str(container
['media_container_id'])
43 is_live
= media
.get('media_status') == 'RUNNING'
45 formats
, subtitles
= None, None
46 headers
= {'Referer': 'https://nuum.ru/'}
48 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(
49 media_url
, video_id
, 'mp4', live
=is_live
, headers
=headers
)
55 'subtitles': subtitles
,
56 'http_headers': headers
,
57 **traverse_obj(container
, {
58 'title': ('media_container_name', {str}
),
59 'description': ('media_container_description', {str}
),
60 'timestamp': ('created_at', {parse_iso8601}
),
61 'channel': ('media_container_channel', 'channel_name', {str}
),
62 'channel_id': ('media_container_channel', 'channel_id', {str_or_none}
),
64 **traverse_obj(stream
, {
65 'view_count': ('stream_total_viewers', {int_or_none}
),
66 'concurrent_view_count': ('stream_current_viewers', {int_or_none}
),
68 **traverse_obj(media
, {
69 'duration': ('media_duration', {int_or_none}
),
70 'thumbnail': ('media_meta', ('media_preview_archive_url', 'media_preview_url'), {url_or_none}
),
75 class NuumMediaIE(NuumBaseIE
):
76 IE_NAME
= 'nuum:media'
77 _VALID_URL
= r
'https?://nuum\.ru/(?:streams|videos|clips)/(?P<id>[\d]+)'
79 'url': 'https://nuum.ru/streams/1592713-7-days-to-die',
80 'only_matching': True,
82 'url': 'https://nuum.ru/videos/1567547-toxi-hurtz',
83 'md5': 'ce28837a5bbffe6952d7bfd3d39811b0',
87 'title': 'Toxi$ - Hurtz',
89 'timestamp': 1702631651,
90 'upload_date': '20231215',
91 'thumbnail': r
're:^https?://.+\.jpg',
93 'concurrent_view_count': int,
99 'url': 'https://nuum.ru/clips/1552564-pro-misu',
100 'md5': 'b248ae1565b1e55433188f11beeb0ca1',
104 'title': 'Про Мису 🙃',
105 'timestamp': 1701971828,
106 'upload_date': '20231207',
107 'thumbnail': r
're:^https?://.+\.jpg',
109 'concurrent_view_count': int,
110 'channel_id': '3320',
111 'channel': 'Misalelik',
116 def _real_extract(self
, url
):
117 video_id
= self
._match
_id
(url
)
118 video_data
= self
._call
_api
(f
'media-containers/{video_id}', video_id
, 'media')
120 return self
._parse
_video
_data
(video_data
)
123 class NuumLiveIE(NuumBaseIE
):
124 IE_NAME
= 'nuum:live'
125 _VALID_URL
= r
'https?://nuum\.ru/channel/(?P<id>[^/#?]+)/?(?:$|[#?])'
127 'url': 'https://nuum.ru/channel/mts_live',
128 'only_matching': True,
131 def _real_extract(self
, url
):
132 channel
= self
._match
_id
(url
)
133 channel_info
= self
._get
_channel
_info
(channel
)
134 if traverse_obj(channel_info
, ('channel', 'channel_is_live')) is False:
135 raise UserNotLive(video_id
=channel
)
137 info
= self
._parse
_video
_data
(channel_info
['media_container'])
139 'webpage_url': f
'https://nuum.ru/streams/{info["id"]}',
140 'extractor_key': NuumMediaIE
.ie_key(),
141 'extractor': NuumMediaIE
.IE_NAME
,
146 class NuumTabIE(NuumBaseIE
):
148 _VALID_URL
= r
'https?://nuum\.ru/channel/(?P<id>[^/#?]+)/(?P<type>streams|videos|clips)'
150 'url': 'https://nuum.ru/channel/dankon_/clips',
152 'id': 'dankon__clips',
155 'playlist_mincount': 29,
157 'url': 'https://nuum.ru/channel/dankon_/videos',
159 'id': 'dankon__videos',
162 'playlist_mincount': 2,
164 'url': 'https://nuum.ru/channel/dankon_/streams',
166 'id': 'dankon__streams',
169 'playlist_mincount': 1,
174 def _fetch_page(self
, channel_id
, tab_type
, tab_id
, page
):
176 'clips': ['SHORT_VIDEO', 'REVIEW_VIDEO'],
177 'videos': ['LONG_VIDEO'],
178 'streams': ['SINGLE'],
181 media_containers
= self
._call
_api
(
182 'media-containers', video_id
=tab_id
, description
=f
'{tab_type} tab page {page + 1}',
184 'limit': self
._PAGE
_SIZE
,
185 'offset': page
* self
._PAGE
_SIZE
,
186 'channel_id': channel_id
,
187 'media_container_status': 'STOPPED',
188 'media_container_type': CONTAINER_TYPES
[tab_type
],
190 for container
in traverse_obj(media_containers
, (..., {dict}
)):
191 metadata
= self
._parse
_video
_data
(container
, extract_formats
=False)
192 yield self
.url_result(f
'https://nuum.ru/videos/{metadata["id"]}', NuumMediaIE
, **metadata
)
194 def _real_extract(self
, url
):
195 channel_name
, tab_type
= self
._match
_valid
_url
(url
).group('id', 'type')
196 tab_id
= f
'{channel_name}_{tab_type}'
197 channel_data
= self
._get
_channel
_info
(channel_name
)['channel']
199 return self
.playlist_result(OnDemandPagedList(functools
.partial(
200 self
._fetch
_page
, channel_data
['channel_id'], tab_type
, tab_id
), self
._PAGE
_SIZE
),
201 playlist_id
=tab_id
, playlist_title
=channel_data
.get('channel_name'))