6 from .common
import InfoExtractor
17 class TrovoBaseIE(InfoExtractor
):
18 _VALID_URL_BASE
= r
'https?://(?:www\.)?trovo\.live/'
19 _HEADERS
= {'Origin': 'https://trovo.live'}
21 def _call_api(self
, video_id
, data
):
22 if 'persistedQuery' in data
.get('extensions', {}):
23 url
= 'https://gql.trovo.live'
25 url
= 'https://api-web.trovo.live/graphql'
27 resp
= self
._download
_json
(
28 url
, video_id
, data
=json
.dumps([data
]).encode(), headers
={'Accept': 'application/json'}
,
30 'qid': ''.join(random
.choices(string
.ascii_uppercase
+ string
.digits
, k
=16)),
33 raise ExtractorError(f
'Trovo said: {resp["errors"][0]["message"]}')
34 return resp
['data'][data
['operationName']]
36 def _extract_streamer_info(self
, data
):
37 streamer_info
= data
.get('streamerInfo') or {}
38 username
= streamer_info
.get('userName')
40 'uploader': streamer_info
.get('nickName'),
41 'uploader_id': str_or_none(streamer_info
.get('uid')),
42 'uploader_url': format_field(username
, None, 'https://trovo.live/%s'),
46 class TrovoIE(TrovoBaseIE
):
47 _VALID_URL
= TrovoBaseIE
._VALID
_URL
_BASE
+ r
'(?:s/)?(?!(?:clip|video)/)(?P<id>(?!s/)[^/?&#]+(?![^#]+[?&]vid=))'
49 'url': 'https://trovo.live/Exsl',
50 'only_matching': True,
52 'url': 'https://trovo.live/s/SkenonSLive/549759191497',
53 'only_matching': True,
55 'url': 'https://trovo.live/s/zijo987/208251706',
57 'id': '104125853_104125853_1656439572',
59 'uploader_url': 'https://trovo.live/zijo987',
60 'uploader_id': '104125853',
61 'thumbnail': 'https://livecover.trovo.live/screenshot/73846_104125853_104125853-2022-06-29-04-00-22-852x480.jpg',
62 'uploader': 'zijo987',
63 'title': 'š„IGRAMO IGRICE UPADAJTEš„2500/5000 2022-06-28 22:01',
64 'live_status': 'is_live',
66 'skip': 'May not be live'
69 def _real_extract(self
, url
):
70 username
= self
._match
_id
(url
)
71 live_info
= self
._call
_api
(username
, data
={
72 'operationName': 'live_LiveReaderService_GetLiveInfo',
79 if live_info
.get('isLive') == 0:
80 raise ExtractorError('%s is offline' % username
, expected
=True)
81 program_info
= live_info
['programInfo']
82 program_id
= program_info
['id']
83 title
= program_info
['title']
86 for stream_info
in (program_info
.get('streamInfo') or []):
87 play_url
= stream_info
.get('playUrl')
90 format_id
= stream_info
.get('desc')
92 'format_id': format_id
,
93 'height': int_or_none(format_id
[:-1]) if format_id
else None,
95 'tbr': stream_info
.get('bitrate'),
96 'http_headers': self
._HEADERS
,
98 self
._sort
_formats
(formats
)
104 'thumbnail': program_info
.get('coverUrl'),
107 info
.update(self
._extract
_streamer
_info
(live_info
))
111 class TrovoVodIE(TrovoBaseIE
):
112 _VALID_URL
= TrovoBaseIE
._VALID
_URL
_BASE
+ r
'(?:clip|video|s)/(?:[^/]+/\d+[^#]*[?&]vid=)?(?P<id>(?<!/s/)[^/?&#]+)'
114 'url': 'https://trovo.live/clip/lc-5285890818705062210?ltab=videos',
115 'params': {'getcomments': True}
,
117 'id': 'lc-5285890818705062210',
119 'title': 'fatal moaning for a super goodš¤£š¤£',
120 'uploader': 'OneTappedYou',
121 'timestamp': 1621628019,
122 'upload_date': '20210521',
123 'uploader_id': '100719456',
127 'comment_count': int,
128 'comments': 'mincount:1',
129 'categories': ['Call of Duty: Mobile'],
130 'uploader_url': 'https://trovo.live/OneTappedYou',
131 'thumbnail': r
're:^https?://.*\.jpg',
134 'url': 'https://trovo.live/s/SkenonSLive/549759191497?vid=ltv-100829718_100829718_387702301737980280',
136 'id': 'ltv-100829718_100829718_387702301737980280',
138 'timestamp': 1654909624,
139 'thumbnail': 'http://vod.trovo.live/1f09baf0vodtransger1301120758/ef9ea3f0387702301737980280/coverBySnapshot/coverBySnapshot_10_0.jpg',
140 'uploader_id': '100829718',
141 'uploader': 'SkenonSLive',
142 'title': 'Trovo u secanju, uz par modova i muzike :)',
143 'uploader_url': 'https://trovo.live/SkenonSLive',
147 'upload_date': '20220611',
148 'comment_count': int,
149 'categories': ['Minecraft'],
151 'skip': 'Not available',
153 'url': 'https://trovo.live/s/Trovo/549756886599?vid=ltv-100264059_100264059_387702304241698583',
155 'id': 'ltv-100264059_100264059_387702304241698583',
157 'timestamp': 1661479563,
158 'thumbnail': 'http://vod.trovo.live/be5ae591vodtransusw1301120758/cccb9915387702304241698583/coverBySnapshot/coverBySnapshot_10_0.jpg',
159 'uploader_id': '100264059',
161 'title': 'Dev Corner 8/25',
162 'uploader_url': 'https://trovo.live/Trovo',
166 'upload_date': '20220826',
167 'comment_count': int,
168 'categories': ['Talk Shows'],
171 'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
172 'only_matching': True,
174 'url': 'https://trovo.live/s/SkenonSLive/549759191497?foo=bar&vid=ltv-100829718_100829718_387702301737980280',
175 'only_matching': True,
178 def _real_extract(self
, url
):
179 vid
= self
._match
_id
(url
)
181 # NOTE: It is also possible to extract this info from the Nuxt data on the website,
182 # however that seems unreliable - sometimes it randomly doesn't return the data,
183 # at least when using a non-residential IP.
184 resp
= self
._call
_api
(vid
, data
={
185 'operationName': 'vod_VodReaderService_BatchGetVodDetailInfo',
194 vod_detail_info
= traverse_obj(resp
, ('VodDetailInfos', vid
), expected_type
=dict)
195 if not vod_detail_info
:
196 raise ExtractorError('This video not found or not available anymore', expected
=True)
197 vod_info
= vod_detail_info
.get('vodInfo')
198 title
= vod_info
.get('title')
200 if try_get(vod_info
, lambda x
: x
['playbackRights']['playbackRights'] != 'Normal'):
201 playback_rights_setting
= vod_info
['playbackRights']['playbackRightsSetting']
202 if playback_rights_setting
== 'SubscriberOnly':
203 raise ExtractorError('This video is only available for subscribers', expected
=True)
205 raise ExtractorError(f
'This video is not available ({playback_rights_setting})', expected
=True)
207 language
= vod_info
.get('languageName')
209 for play_info
in (vod_info
.get('playInfos') or []):
210 play_url
= play_info
.get('playUrl')
213 format_id
= play_info
.get('desc')
216 'filesize': int_or_none(play_info
.get('fileSize')),
217 'format_id': format_id
,
218 'height': int_or_none(format_id
[:-1]) if format_id
else None,
219 'language': language
,
220 'protocol': 'm3u8_native',
221 'tbr': int_or_none(play_info
.get('bitrate')),
223 'http_headers': self
._HEADERS
,
225 self
._sort
_formats
(formats
)
227 category
= vod_info
.get('categoryName')
228 get_count
= lambda x
: int_or_none(vod_info
.get(x
+ 'Num'))
234 'thumbnail': vod_info
.get('coverUrl'),
235 'timestamp': int_or_none(vod_info
.get('publishTs')),
236 'duration': int_or_none(vod_info
.get('duration')),
237 'view_count': get_count('watch'),
238 'like_count': get_count('like'),
239 'comment_count': get_count('comment'),
240 'categories': [category
] if category
else None,
241 '__post_extractor': self
.extract_comments(vid
),
243 info
.update(self
._extract
_streamer
_info
(vod_detail_info
))
246 def _get_comments(self
, vid
):
247 for page
in itertools
.count(1):
248 comments_json
= self
._call
_api
(vid
, data
={
249 'operationName': 'public_CommentProxyService_GetCommentList',
264 for comment
in comments_json
['commentList']:
265 content
= comment
.get('content')
268 author
= comment
.get('author') or {}
269 parent
= comment
.get('parentID')
271 'author': author
.get('nickName'),
272 'author_id': str_or_none(author
.get('uid')),
273 'id': str_or_none(comment
.get('commentID')),
275 'timestamp': int_or_none(comment
.get('createdAt')),
276 'parent': 'root' if parent
== 0 else str_or_none(parent
),
279 if comments_json
['lastPage']:
283 class TrovoChannelBaseIE(TrovoBaseIE
):
284 def _entries(self
, spacename
):
285 for page
in itertools
.count(1):
286 vod_json
= self
._call
_api
(spacename
, data
={
287 'operationName': self
._OPERATION
,
291 'spaceName': spacename
,
301 vods
= vod_json
.get('vodInfos', [])
304 room
= traverse_obj(vod
, ('spaceInfo', 'roomID'))
305 yield self
.url_result(
306 f
'https://trovo.live/s/{spacename}/{room}?vid={vid}',
307 ie
=TrovoVodIE
.ie_key())
308 has_more
= vod_json
.get('hasMore')
312 def _real_extract(self
, url
):
313 spacename
= self
._match
_id
(url
)
314 return self
.playlist_result(self
._entries
(spacename
), playlist_id
=spacename
)
317 class TrovoChannelVodIE(TrovoChannelBaseIE
):
318 _VALID_URL
= r
'trovovod:(?P<id>[^\s]+)'
319 IE_DESC
= 'All VODs of a trovo.live channel; "trovovod:" prefix'
322 'url': 'trovovod:OneTappedYou',
323 'playlist_mincount': 24,
325 'id': 'OneTappedYou',
329 _OPERATION
= 'vod_VodReaderService_GetChannelLtvVideoInfos'
332 class TrovoChannelClipIE(TrovoChannelBaseIE
):
333 _VALID_URL
= r
'trovoclip:(?P<id>[^\s]+)'
334 IE_DESC
= 'All Clips of a trovo.live channel; "trovoclip:" prefix'
337 'url': 'trovoclip:OneTappedYou',
338 'playlist_mincount': 29,
340 'id': 'OneTappedYou',
344 _OPERATION
= 'vod_VodReaderService_GetChannelClipVideoInfos'