4 from .common
import InfoExtractor
14 class TrovoBaseIE(InfoExtractor
):
15 _VALID_URL_BASE
= r
'https?://(?:www\.)?trovo\.live/'
16 _HEADERS
= {'Origin': 'https://trovo.live'}
18 def _call_api(self
, video_id
, query
=None, data
=None):
19 return self
._download
_json
(
20 'https://gql.trovo.live/', video_id
, query
=query
, data
=data
,
21 headers
={'Accept': 'application/json'}
)
23 def _extract_streamer_info(self
, data
):
24 streamer_info
= data
.get('streamerInfo') or {}
25 username
= streamer_info
.get('userName')
27 'uploader': streamer_info
.get('nickName'),
28 'uploader_id': str_or_none(streamer_info
.get('uid')),
29 'uploader_url': format_field(username
, template
='https://trovo.live/%s'),
33 class TrovoIE(TrovoBaseIE
):
34 _VALID_URL
= TrovoBaseIE
._VALID
_URL
_BASE
+ r
'(?!(?:clip|video)/)(?P<id>[^/?&#]+)'
36 def _real_extract(self
, url
):
37 username
= self
._match
_id
(url
)
38 live_info
= self
._call
_api
(username
, query
={
40 getLiveInfo(params: {userName: "%s"}) {
58 })['data']['getLiveInfo']
59 if live_info
.get('isLive') == 0:
60 raise ExtractorError('%s is offline' % username
, expected
=True)
61 program_info
= live_info
['programInfo']
62 program_id
= program_info
['id']
63 title
= program_info
['title']
66 for stream_info
in (program_info
.get('streamInfo') or []):
67 play_url
= stream_info
.get('playUrl')
70 format_id
= stream_info
.get('desc')
72 'format_id': format_id
,
73 'height': int_or_none(format_id
[:-1]) if format_id
else None,
75 'http_headers': self
._HEADERS
,
77 self
._sort
_formats
(formats
)
83 'thumbnail': program_info
.get('coverUrl'),
86 info
.update(self
._extract
_streamer
_info
(live_info
))
90 class TrovoVodIE(TrovoBaseIE
):
91 _VALID_URL
= TrovoBaseIE
._VALID
_URL
_BASE
+ r
'(?:clip|video)/(?P<id>[^/?&#]+)'
93 'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
95 'id': 'ltv-100095501_100095501_1609596043',
97 'title': 'Spontaner 12 Stunden Stream! - Ok Boomer!',
99 'timestamp': 1609640305,
100 'upload_date': '20210103',
101 'uploader_id': '100095501',
105 'comment_count': int,
106 'comments': 'mincount:8',
107 'categories': ['Grand Theft Auto V'],
111 'url': 'https://trovo.live/clip/lc-5285890810184026005',
112 'only_matching': True,
115 def _real_extract(self
, url
):
116 vid
= self
._match
_id
(url
)
117 resp
= self
._call
_api
(vid
, data
=json
.dumps([{
119 batchGetVodDetailInfo(params: {vids: ["%s"]}) {
125 getCommentList(params: {appInfo: {postID: "%s"}, pageSize: 1000000000, preview: {}}) {
139 vod_detail_info
= resp
[0]['data']['batchGetVodDetailInfo']['VodDetailInfos'][vid
]
140 vod_info
= vod_detail_info
['vodInfo']
141 title
= vod_info
['title']
143 language
= vod_info
.get('languageName')
145 for play_info
in (vod_info
.get('playInfos') or []):
146 play_url
= play_info
.get('playUrl')
149 format_id
= play_info
.get('desc')
152 'filesize': int_or_none(play_info
.get('fileSize')),
153 'format_id': format_id
,
154 'height': int_or_none(format_id
[:-1]) if format_id
else None,
155 'language': language
,
156 'protocol': 'm3u8_native',
157 'tbr': int_or_none(play_info
.get('bitrate')),
159 'http_headers': self
._HEADERS
,
161 self
._sort
_formats
(formats
)
163 category
= vod_info
.get('categoryName')
164 get_count
= lambda x
: int_or_none(vod_info
.get(x
+ 'Num'))
166 comment_list
= try_get(resp
, lambda x
: x
[1]['data']['getCommentList']['commentList'], list) or []
168 for comment
in comment_list
:
169 content
= comment
.get('content')
172 author
= comment
.get('author') or {}
173 parent
= comment
.get('parentID')
175 'author': author
.get('nickName'),
176 'author_id': str_or_none(author
.get('uid')),
177 'id': str_or_none(comment
.get('commentID')),
179 'timestamp': int_or_none(comment
.get('createdAt')),
180 'parent': 'root' if parent
== 0 else str_or_none(parent
),
187 'thumbnail': vod_info
.get('coverUrl'),
188 'timestamp': int_or_none(vod_info
.get('publishTs')),
189 'duration': int_or_none(vod_info
.get('duration')),
190 'view_count': get_count('watch'),
191 'like_count': get_count('like'),
192 'comment_count': get_count('comment'),
193 'comments': comments
,
194 'categories': [category
] if category
else None,
196 info
.update(self
._extract
_streamer
_info
(vod_detail_info
))
200 class TrovoChannelBaseIE(TrovoBaseIE
):
201 def _get_vod_json(self
, page
, uid
):
202 raise NotImplementedError('This method must be implemented by subclasses')
204 def _entries(self
, uid
):
205 for page
in itertools
.count(1):
206 vod_json
= self
._get
_vod
_json
(page
, uid
)
207 vods
= vod_json
.get('vodInfos', [])
209 yield self
.url_result(
210 'https://trovo.live/%s/%s' % (self
._TYPE
, vod
.get('vid')),
211 ie
=TrovoVodIE
.ie_key())
212 has_more
= vod_json
['hasMore']
216 def _real_extract(self
, url
):
217 id = self
._match
_id
(url
)
218 uid
= str(self
._call
_api
(id, query
={
219 'query': '{getLiveInfo(params:{userName:"%s"}){streamerInfo{uid}}}' % id
220 })['data']['getLiveInfo']['streamerInfo']['uid'])
221 return self
.playlist_result(self
._entries
(uid
), playlist_id
=uid
)
224 class TrovoChannelVodIE(TrovoChannelBaseIE
):
225 _VALID_URL
= r
'trovovod:(?P<id>[^\s]+)'
226 IE_DESC
= 'All VODs of a trovo.live channel; "trovovod:" prefix'
229 'url': 'trovovod:OneTappedYou',
230 'playlist_mincount': 24,
236 _QUERY
= '{getChannelLtvVideoInfos(params:{pageSize:99,currPage:%d,channelID:%s}){hasMore,vodInfos{vid}}}'
239 def _get_vod_json(self
, page
, uid
):
240 return self
._call
_api
(uid
, query
={
241 'query': self
._QUERY
% (page
, uid
)
242 })['data']['getChannelLtvVideoInfos']
245 class TrovoChannelClipIE(TrovoChannelBaseIE
):
246 _VALID_URL
= r
'trovoclip:(?P<id>[^\s]+)'
247 IE_DESC
= 'All Clips of a trovo.live channel; "trovoclip:" prefix'
250 'url': 'trovoclip:OneTappedYou',
251 'playlist_mincount': 29,
257 _QUERY
= '{getChannelClipVideoInfos(params:{pageSize:99,currPage:%d,channelID:%s,albumType:VOD_CLIP_ALBUM_TYPE_LATEST}){hasMore,vodInfos{vid}}}'
260 def _get_vod_json(self
, page
, uid
):
261 return self
._call
_api
(uid
, query
={
262 'query': self
._QUERY
% (page
, uid
)
263 })['data']['getChannelClipVideoInfos']