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(f
'{username} is offline', 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
,
103 'thumbnail': program_info
.get('coverUrl'),
106 info
.update(self
._extract
_streamer
_info
(live_info
))
110 class TrovoVodIE(TrovoBaseIE
):
111 _VALID_URL
= TrovoBaseIE
._VALID
_URL
_BASE
+ r
'(?:clip|video|s)/(?:[^/]+/\d+[^#]*[?&]vid=)?(?P<id>(?<!/s/)[^/?&#]+)'
113 'url': 'https://trovo.live/clip/lc-5285890818705062210?ltab=videos',
114 'params': {'getcomments': True},
116 'id': 'lc-5285890818705062210',
118 'title': 'fatal moaning for a super goodš¤£š¤£',
119 'uploader': 'OneTappedYou',
120 'timestamp': 1621628019,
121 'upload_date': '20210521',
122 'uploader_id': '100719456',
126 'comment_count': int,
127 'comments': 'mincount:1',
128 'categories': ['Call of Duty: Mobile'],
129 'uploader_url': 'https://trovo.live/OneTappedYou',
130 'thumbnail': r
're:^https?://.*\.jpg',
133 'url': 'https://trovo.live/s/SkenonSLive/549759191497?vid=ltv-100829718_100829718_387702301737980280',
135 'id': 'ltv-100829718_100829718_387702301737980280',
137 'timestamp': 1654909624,
138 'thumbnail': 'http://vod.trovo.live/1f09baf0vodtransger1301120758/ef9ea3f0387702301737980280/coverBySnapshot/coverBySnapshot_10_0.jpg',
139 'uploader_id': '100829718',
140 'uploader': 'SkenonSLive',
141 'title': 'Trovo u secanju, uz par modova i muzike :)',
142 'uploader_url': 'https://trovo.live/SkenonSLive',
146 'upload_date': '20220611',
147 'comment_count': int,
148 'categories': ['Minecraft'],
150 'skip': 'Not available',
152 'url': 'https://trovo.live/s/Trovo/549756886599?vid=ltv-100264059_100264059_387702304241698583',
154 'id': 'ltv-100264059_100264059_387702304241698583',
156 'timestamp': 1661479563,
157 'thumbnail': 'http://vod.trovo.live/be5ae591vodtransusw1301120758/cccb9915387702304241698583/coverBySnapshot/coverBySnapshot_10_0.jpg',
158 'uploader_id': '100264059',
160 'title': 'Dev Corner 8/25',
161 'uploader_url': 'https://trovo.live/Trovo',
165 'upload_date': '20220826',
166 'comment_count': int,
167 'categories': ['Talk Shows'],
170 'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
171 'only_matching': True,
173 'url': 'https://trovo.live/s/SkenonSLive/549759191497?foo=bar&vid=ltv-100829718_100829718_387702301737980280',
174 'only_matching': True,
177 def _real_extract(self
, url
):
178 vid
= self
._match
_id
(url
)
180 # NOTE: It is also possible to extract this info from the Nuxt data on the website,
181 # however that seems unreliable - sometimes it randomly doesn't return the data,
182 # at least when using a non-residential IP.
183 resp
= self
._call
_api
(vid
, data
={
184 'operationName': 'vod_VodReaderService_BatchGetVodDetailInfo',
193 vod_detail_info
= traverse_obj(resp
, ('VodDetailInfos', vid
), expected_type
=dict)
194 if not vod_detail_info
:
195 raise ExtractorError('This video not found or not available anymore', expected
=True)
196 vod_info
= vod_detail_info
.get('vodInfo')
197 title
= vod_info
.get('title')
199 if try_get(vod_info
, lambda x
: x
['playbackRights']['playbackRights'] != 'Normal'):
200 playback_rights_setting
= vod_info
['playbackRights']['playbackRightsSetting']
201 if playback_rights_setting
== 'SubscriberOnly':
202 raise ExtractorError('This video is only available for subscribers', expected
=True)
204 raise ExtractorError(f
'This video is not available ({playback_rights_setting})', expected
=True)
206 language
= vod_info
.get('languageName')
208 for play_info
in (vod_info
.get('playInfos') or []):
209 play_url
= play_info
.get('playUrl')
212 format_id
= play_info
.get('desc')
215 'filesize': int_or_none(play_info
.get('fileSize')),
216 'format_id': format_id
,
217 'height': int_or_none(format_id
[:-1]) if format_id
else None,
218 'language': language
,
219 'protocol': 'm3u8_native',
220 'tbr': int_or_none(play_info
.get('bitrate')),
222 'http_headers': self
._HEADERS
,
225 category
= vod_info
.get('categoryName')
226 get_count
= lambda x
: int_or_none(vod_info
.get(x
+ 'Num'))
232 'thumbnail': vod_info
.get('coverUrl'),
233 'timestamp': int_or_none(vod_info
.get('publishTs')),
234 'duration': int_or_none(vod_info
.get('duration')),
235 'view_count': get_count('watch'),
236 'like_count': get_count('like'),
237 'comment_count': get_count('comment'),
238 'categories': [category
] if category
else None,
239 '__post_extractor': self
.extract_comments(vid
),
241 info
.update(self
._extract
_streamer
_info
(vod_detail_info
))
244 def _get_comments(self
, vid
):
245 for page
in itertools
.count(1):
246 comments_json
= self
._call
_api
(vid
, data
={
247 'operationName': 'public_CommentProxyService_GetCommentList',
262 for comment
in comments_json
['commentList']:
263 content
= comment
.get('content')
266 author
= comment
.get('author') or {}
267 parent
= comment
.get('parentID')
269 'author': author
.get('nickName'),
270 'author_id': str_or_none(author
.get('uid')),
271 'id': str_or_none(comment
.get('commentID')),
273 'timestamp': int_or_none(comment
.get('createdAt')),
274 'parent': 'root' if parent
== 0 else str_or_none(parent
),
277 if comments_json
['lastPage']:
281 class TrovoChannelBaseIE(TrovoBaseIE
):
282 def _entries(self
, spacename
):
283 for page
in itertools
.count(1):
284 vod_json
= self
._call
_api
(spacename
, data
={
285 'operationName': self
._OPERATION
,
289 'spaceName': spacename
,
299 vods
= vod_json
.get('vodInfos', [])
302 room
= traverse_obj(vod
, ('spaceInfo', 'roomID'))
303 yield self
.url_result(
304 f
'https://trovo.live/s/{spacename}/{room}?vid={vid}',
305 ie
=TrovoVodIE
.ie_key())
306 has_more
= vod_json
.get('hasMore')
310 def _real_extract(self
, url
):
311 spacename
= self
._match
_id
(url
)
312 return self
.playlist_result(self
._entries
(spacename
), playlist_id
=spacename
)
315 class TrovoChannelVodIE(TrovoChannelBaseIE
):
316 _VALID_URL
= r
'trovovod:(?P<id>[^\s]+)'
317 IE_DESC
= 'All VODs of a trovo.live channel; "trovovod:" prefix'
320 'url': 'trovovod:OneTappedYou',
321 'playlist_mincount': 24,
323 'id': 'OneTappedYou',
327 _OPERATION
= 'vod_VodReaderService_GetChannelLtvVideoInfos'
330 class TrovoChannelClipIE(TrovoChannelBaseIE
):
331 _VALID_URL
= r
'trovoclip:(?P<id>[^\s]+)'
332 IE_DESC
= 'All Clips of a trovo.live channel; "trovoclip:" prefix'
335 'url': 'trovoclip:OneTappedYou',
336 'playlist_mincount': 29,
338 'id': 'OneTappedYou',
342 _OPERATION
= 'vod_VodReaderService_GetChannelClipVideoInfos'