1# coding: utf-8
2from __future__ import unicode_literals
3
4import itertools
5import json
6
7from .common import InfoExtractor
8from ..utils import (
9    ExtractorError,
10    int_or_none,
11    str_or_none,
12    try_get,
13)
14
15
16class TrovoBaseIE(InfoExtractor):
17    _VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
18    _HEADERS = {'Origin': 'https://trovo.live'}
19
20    def _call_api(self, video_id, query=None, data=None):
21        return self._download_json(
22            'https://gql.trovo.live/', video_id, query=query, data=data,
23            headers={'Accept': 'application/json'})
24
25    def _extract_streamer_info(self, data):
26        streamer_info = data.get('streamerInfo') or {}
27        username = streamer_info.get('userName')
28        return {
29            'uploader': streamer_info.get('nickName'),
30            'uploader_id': str_or_none(streamer_info.get('uid')),
31            'uploader_url': 'https://trovo.live/' + username if username else None,
32        }
33
34
35class TrovoIE(TrovoBaseIE):
36    _VALID_URL = TrovoBaseIE._VALID_URL_BASE + r'(?!(?:clip|video)/)(?P<id>[^/?&#]+)'
37
38    def _real_extract(self, url):
39        username = self._match_id(url)
40        live_info = self._call_api(username, query={
41            'query': '''{
42  getLiveInfo(params: {userName: "%s"}) {
43    isLive
44    programInfo {
45      coverUrl
46      id
47      streamInfo {
48        desc
49        playUrl
50      }
51      title
52    }
53    streamerInfo {
54        nickName
55        uid
56        userName
57    }
58  }
59}''' % username,
60        })['data']['getLiveInfo']
61        if live_info.get('isLive') == 0:
62            raise ExtractorError('%s is offline' % username, expected=True)
63        program_info = live_info['programInfo']
64        program_id = program_info['id']
65        title = program_info['title']
66
67        formats = []
68        for stream_info in (program_info.get('streamInfo') or []):
69            play_url = stream_info.get('playUrl')
70            if not play_url:
71                continue
72            format_id = stream_info.get('desc')
73            formats.append({
74                'format_id': format_id,
75                'height': int_or_none(format_id[:-1]) if format_id else None,
76                'url': play_url,
77                'http_headers': self._HEADERS,
78            })
79        self._sort_formats(formats)
80
81        info = {
82            'id': program_id,
83            'title': title,
84            'formats': formats,
85            'thumbnail': program_info.get('coverUrl'),
86            'is_live': True,
87        }
88        info.update(self._extract_streamer_info(live_info))
89        return info
90
91
92class TrovoVodIE(TrovoBaseIE):
93    _VALID_URL = TrovoBaseIE._VALID_URL_BASE + r'(?:clip|video)/(?P<id>[^/?&#]+)'
94    _TESTS = [{
95        'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
96        'info_dict': {
97            'id': 'ltv-100095501_100095501_1609596043',
98            'ext': 'mp4',
99            'title': 'Spontaner 12 Stunden Stream! - Ok Boomer!',
100            'uploader': 'Exsl',
101            'timestamp': 1609640305,
102            'upload_date': '20210103',
103            'uploader_id': '100095501',
104            'duration': 43977,
105            'view_count': int,
106            'like_count': int,
107            'comment_count': int,
108            'comments': 'mincount:8',
109            'categories': ['Grand Theft Auto V'],
110        },
111        'skip': '404'
112    }, {
113        'url': 'https://trovo.live/clip/lc-5285890810184026005',
114        'only_matching': True,
115    }]
116
117    def _real_extract(self, url):
118        vid = self._match_id(url)
119        resp = self._call_api(vid, data=json.dumps([{
120            'query': '''{
121  batchGetVodDetailInfo(params: {vids: ["%s"]}) {
122    VodDetailInfos
123  }
124}''' % vid,
125        }, {
126            'query': '''{
127  getCommentList(params: {appInfo: {postID: "%s"}, pageSize: 1000000000, preview: {}}) {
128    commentList {
129      author {
130        nickName
131        uid
132      }
133      commentID
134      content
135      createdAt
136      parentID
137    }
138  }
139}''' % vid,
140        }]).encode())
141        vod_detail_info = resp[0]['data']['batchGetVodDetailInfo']['VodDetailInfos'][vid]
142        vod_info = vod_detail_info['vodInfo']
143        title = vod_info['title']
144
145        language = vod_info.get('languageName')
146        formats = []
147        for play_info in (vod_info.get('playInfos') or []):
148            play_url = play_info.get('playUrl')
149            if not play_url:
150                continue
151            format_id = play_info.get('desc')
152            formats.append({
153                'ext': 'mp4',
154                'filesize': int_or_none(play_info.get('fileSize')),
155                'format_id': format_id,
156                'height': int_or_none(format_id[:-1]) if format_id else None,
157                'language': language,
158                'protocol': 'm3u8_native',
159                'tbr': int_or_none(play_info.get('bitrate')),
160                'url': play_url,
161                'http_headers': self._HEADERS,
162            })
163        self._sort_formats(formats)
164
165        category = vod_info.get('categoryName')
166        get_count = lambda x: int_or_none(vod_info.get(x + 'Num'))
167
168        comment_list = try_get(resp, lambda x: x[1]['data']['getCommentList']['commentList'], list) or []
169        comments = []
170        for comment in comment_list:
171            content = comment.get('content')
172            if not content:
173                continue
174            author = comment.get('author') or {}
175            parent = comment.get('parentID')
176            comments.append({
177                'author': author.get('nickName'),
178                'author_id': str_or_none(author.get('uid')),
179                'id': str_or_none(comment.get('commentID')),
180                'text': content,
181                'timestamp': int_or_none(comment.get('createdAt')),
182                'parent': 'root' if parent == 0 else str_or_none(parent),
183            })
184
185        info = {
186            'id': vid,
187            'title': title,
188            'formats': formats,
189            'thumbnail': vod_info.get('coverUrl'),
190            'timestamp': int_or_none(vod_info.get('publishTs')),
191            'duration': int_or_none(vod_info.get('duration')),
192            'view_count': get_count('watch'),
193            'like_count': get_count('like'),
194            'comment_count': get_count('comment'),
195            'comments': comments,
196            'categories': [category] if category else None,
197        }
198        info.update(self._extract_streamer_info(vod_detail_info))
199        return info
200
201
202class TrovoChannelBaseIE(TrovoBaseIE):
203    def _get_vod_json(self, page, uid):
204        raise NotImplementedError('This method must be implemented by subclasses')
205
206    def _entries(self, uid):
207        for page in itertools.count(1):
208            vod_json = self._get_vod_json(page, uid)
209            vods = vod_json.get('vodInfos', [])
210            for vod in vods:
211                yield self.url_result(
212                    'https://trovo.live/%s/%s' % (self._TYPE, vod.get('vid')),
213                    ie=TrovoVodIE.ie_key())
214            has_more = vod_json['hasMore']
215            if not has_more:
216                break
217
218    def _real_extract(self, url):
219        id = self._match_id(url)
220        uid = str(self._call_api(id, query={
221            'query': '{getLiveInfo(params:{userName:"%s"}){streamerInfo{uid}}}' % id
222        })['data']['getLiveInfo']['streamerInfo']['uid'])
223        return self.playlist_result(self._entries(uid), playlist_id=uid)
224
225
226class TrovoChannelVodIE(TrovoChannelBaseIE):
227    _VALID_URL = r'trovovod:(?P<id>[^\s]+)'
228    IE_DESC = 'All VODs of a trovo.live channel; "trovovod:" prefix'
229
230    _TESTS = [{
231        'url': 'trovovod:OneTappedYou',
232        'playlist_mincount': 24,
233        'info_dict': {
234            'id': '100719456',
235        },
236    }]
237
238    _QUERY = '{getChannelLtvVideoInfos(params:{pageSize:99,currPage:%d,channelID:%s}){hasMore,vodInfos{vid}}}'
239    _TYPE = 'video'
240
241    def _get_vod_json(self, page, uid):
242        return self._call_api(uid, query={
243            'query': self._QUERY % (page, uid)
244        })['data']['getChannelLtvVideoInfos']
245
246
247class TrovoChannelClipIE(TrovoChannelBaseIE):
248    _VALID_URL = r'trovoclip:(?P<id>[^\s]+)'
249    IE_DESC = 'All Clips of a trovo.live channel; "trovoclip:" prefix'
250
251    _TESTS = [{
252        'url': 'trovoclip:OneTappedYou',
253        'playlist_mincount': 29,
254        'info_dict': {
255            'id': '100719456',
256        },
257    }]
258
259    _QUERY = '{getChannelClipVideoInfos(params:{pageSize:99,currPage:%d,channelID:%s,albumType:VOD_CLIP_ALBUM_TYPE_LATEST}){hasMore,vodInfos{vid}}}'
260    _TYPE = 'clip'
261
262    def _get_vod_json(self, page, uid):
263        return self._call_api(uid, query={
264            'query': self._QUERY % (page, uid)
265        })['data']['getChannelClipVideoInfos']
266