6 from .common
import InfoExtractor
16 class TrovoBaseIE(InfoExtractor
):
17 _VALID_URL_BASE
= r
'https?://(?:www\.)?trovo\.live/'
18 _HEADERS
= {'Origin': 'https://trovo.live'}
20 def _call_api(self
, video_id
, data
):
21 if 'persistedQuery' in data
.get('extensions', {}):
22 url
= 'https://gql.trovo.live'
24 url
= 'https://api-web.trovo.live/graphql'
26 resp
= self
._download
_json
(
27 url
, video_id
, data
=json
.dumps([data
]).encode(), headers
={'Accept': 'application/json'}
,
29 'qid': ''.join(random
.choices(string
.ascii_uppercase
+ string
.digits
, k
=10)),
32 raise ExtractorError(f
'Trovo said: {resp["errors"][0]["message"]}')
33 return resp
['data'][data
['operationName']]
35 def _extract_streamer_info(self
, data
):
36 streamer_info
= data
.get('streamerInfo') or {}
37 username
= streamer_info
.get('userName')
39 'uploader': streamer_info
.get('nickName'),
40 'uploader_id': str_or_none(streamer_info
.get('uid')),
41 'uploader_url': format_field(username
, template
='https://trovo.live/%s'),
45 class TrovoIE(TrovoBaseIE
):
46 _VALID_URL
= TrovoBaseIE
._VALID
_URL
_BASE
+ r
'(?!(?:clip|video)/)(?P<id>[^/?&#]+)'
48 def _real_extract(self
, url
):
49 username
= self
._match
_id
(url
)
50 live_info
= self
._call
_api
(username
, data
={
51 'operationName': 'live_LiveReaderService_GetLiveInfo',
58 if live_info
.get('isLive') == 0:
59 raise ExtractorError('%s is offline' % username
, expected
=True)
60 program_info
= live_info
['programInfo']
61 program_id
= program_info
['id']
62 title
= program_info
['title']
65 for stream_info
in (program_info
.get('streamInfo') or []):
66 play_url
= stream_info
.get('playUrl')
69 format_id
= stream_info
.get('desc')
71 'format_id': format_id
,
72 'height': int_or_none(format_id
[:-1]) if format_id
else None,
74 'http_headers': self
._HEADERS
,
76 self
._sort
_formats
(formats
)
82 'thumbnail': program_info
.get('coverUrl'),
85 info
.update(self
._extract
_streamer
_info
(live_info
))
89 class TrovoVodIE(TrovoBaseIE
):
90 _VALID_URL
= TrovoBaseIE
._VALID
_URL
_BASE
+ r
'(?:clip|video)/(?P<id>[^/?&#]+)'
92 'url': 'https://trovo.live/clip/lc-5285890818705062210?ltab=videos',
93 'params': {'getcomments': True}
,
95 'id': 'lc-5285890818705062210',
97 'title': 'fatal moaning for a super good🤣🤣',
98 'uploader': 'OneTappedYou',
99 'timestamp': 1621628019,
100 'upload_date': '20210521',
101 'uploader_id': '100719456',
105 'comment_count': int,
106 'comments': 'mincount:1',
107 'categories': ['Call of Duty: Mobile'],
108 'uploader_url': 'https://trovo.live/OneTappedYou',
109 'thumbnail': r
're:^https?://.*\.jpg',
112 'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
113 'only_matching': True,
116 def _real_extract(self
, url
):
117 vid
= self
._match
_id
(url
)
119 # NOTE: It is also possible to extract this info from the Nuxt data on the website,
120 # however that seems unreliable - sometimes it randomly doesn't return the data,
121 # at least when using a non-residential IP.
122 resp
= self
._call
_api
(vid
, data
={
123 'operationName': 'batchGetVodDetailInfo',
132 'sha256Hash': 'ceae0355d66476e21a1dd8e8af9f68de95b4019da2cda8b177c9a2255dad31d0',
136 vod_detail_info
= resp
['VodDetailInfos'][vid
]
137 vod_info
= vod_detail_info
['vodInfo']
138 title
= vod_info
['title']
140 if try_get(vod_info
, lambda x
: x
['playbackRights']['playbackRights'] != 'Normal'):
141 playback_rights_setting
= vod_info
['playbackRights']['playbackRightsSetting']
142 if playback_rights_setting
== 'SubscriberOnly':
143 raise ExtractorError('This video is only available for subscribers', expected
=True)
145 raise ExtractorError(f
'This video is not available ({playback_rights_setting})', expected
=True)
147 language
= vod_info
.get('languageName')
149 for play_info
in (vod_info
.get('playInfos') or []):
150 play_url
= play_info
.get('playUrl')
153 format_id
= play_info
.get('desc')
156 'filesize': int_or_none(play_info
.get('fileSize')),
157 'format_id': format_id
,
158 'height': int_or_none(format_id
[:-1]) if format_id
else None,
159 'language': language
,
160 'protocol': 'm3u8_native',
161 'tbr': int_or_none(play_info
.get('bitrate')),
163 'http_headers': self
._HEADERS
,
165 self
._sort
_formats
(formats
)
167 category
= vod_info
.get('categoryName')
168 get_count
= lambda x
: int_or_none(vod_info
.get(x
+ 'Num'))
174 'thumbnail': vod_info
.get('coverUrl'),
175 'timestamp': int_or_none(vod_info
.get('publishTs')),
176 'duration': int_or_none(vod_info
.get('duration')),
177 'view_count': get_count('watch'),
178 'like_count': get_count('like'),
179 'comment_count': get_count('comment'),
180 'categories': [category
] if category
else None,
181 '__post_extractor': self
.extract_comments(vid
),
183 info
.update(self
._extract
_streamer
_info
(vod_detail_info
))
186 def _get_comments(self
, vid
):
187 for page
in itertools
.count(1):
188 comments_json
= self
._call
_api
(vid
, data
={
189 'operationName': 'getCommentList',
203 'sha256Hash': 'be8e5f9522ddac7f7c604c0d284fd22481813263580849926c4c66fb767eed25',
207 for comment
in comments_json
['commentList']:
208 content
= comment
.get('content')
211 author
= comment
.get('author') or {}
212 parent
= comment
.get('parentID')
214 'author': author
.get('nickName'),
215 'author_id': str_or_none(author
.get('uid')),
216 'id': str_or_none(comment
.get('commentID')),
218 'timestamp': int_or_none(comment
.get('createdAt')),
219 'parent': 'root' if parent
== 0 else str_or_none(parent
),
222 if comments_json
['lastPage']:
226 class TrovoChannelBaseIE(TrovoBaseIE
):
227 def _get_vod_json(self
, page
, uid
):
228 raise NotImplementedError('This method must be implemented by subclasses')
230 def _entries(self
, uid
):
231 for page
in itertools
.count(1):
232 vod_json
= self
._get
_vod
_json
(page
, uid
)
233 vods
= vod_json
.get('vodInfos', [])
235 yield self
.url_result(
236 'https://trovo.live/%s/%s' % (self
._TYPE
, vod
.get('vid')),
237 ie
=TrovoVodIE
.ie_key())
238 has_more
= vod_json
['hasMore']
242 def _real_extract(self
, url
):
243 id = self
._match
_id
(url
)
244 live_info
= self
._call
_api
(id, data
={
245 'operationName': 'live_LiveReaderService_GetLiveInfo',
252 uid
= str(live_info
['streamerInfo']['uid'])
253 return self
.playlist_result(self
._entries
(uid
), playlist_id
=uid
)
256 class TrovoChannelVodIE(TrovoChannelBaseIE
):
257 _VALID_URL
= r
'trovovod:(?P<id>[^\s]+)'
258 IE_DESC
= 'All VODs of a trovo.live channel; "trovovod:" prefix'
261 'url': 'trovovod:OneTappedYou',
262 'playlist_mincount': 24,
270 def _get_vod_json(self
, page
, uid
):
271 return self
._call
_api
(uid
, data
={
272 'operationName': 'getChannelLtvVideoInfos',
275 'channelID': int(uid
),
283 'sha256Hash': '78fe32792005eab7e922cafcdad9c56bed8bbc5f5df3c7cd24fcb84a744f5f78',
289 class TrovoChannelClipIE(TrovoChannelBaseIE
):
290 _VALID_URL
= r
'trovoclip:(?P<id>[^\s]+)'
291 IE_DESC
= 'All Clips of a trovo.live channel; "trovoclip:" prefix'
294 'url': 'trovoclip:OneTappedYou',
295 'playlist_mincount': 29,
303 def _get_vod_json(self
, page
, uid
):
304 return self
._call
_api
(uid
, data
={
305 'operationName': 'getChannelClipVideoInfos',
308 'channelID': int(uid
),
316 'sha256Hash': 'e7924bfe20059b5c75fc8ff9e7929f43635681a7bdf3befa01072ed22c8eff31',