import itertools import json import random import string from .common import InfoExtractor from ..utils import ( ExtractorError, format_field, int_or_none, str_or_none, try_get, ) class TrovoBaseIE(InfoExtractor): _VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/' _HEADERS = {'Origin': 'https://trovo.live'} def _call_api(self, video_id, data): if 'persistedQuery' in data.get('extensions', {}): url = 'https://gql.trovo.live' else: url = 'https://api-web.trovo.live/graphql' resp = self._download_json( url, video_id, data=json.dumps([data]).encode(), headers={'Accept': 'application/json'}, query={ 'qid': ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)), })[0] if 'errors' in resp: raise ExtractorError(f'Trovo said: {resp["errors"][0]["message"]}') return resp['data'][data['operationName']] def _extract_streamer_info(self, data): streamer_info = data.get('streamerInfo') or {} username = streamer_info.get('userName') return { 'uploader': streamer_info.get('nickName'), 'uploader_id': str_or_none(streamer_info.get('uid')), 'uploader_url': format_field(username, None, 'https://trovo.live/%s'), } class TrovoIE(TrovoBaseIE): _VALID_URL = TrovoBaseIE._VALID_URL_BASE + r'(?:s/)?(?!(?:clip|video)/)(?P(?!s/)[^/?&#]+(?![^#]+[?&]vid=))' _TESTS = [{ 'url': 'https://trovo.live/Exsl', 'only_matching': True, }, { 'url': 'https://trovo.live/s/SkenonSLive/549759191497', 'only_matching': True, }, { 'url': 'https://trovo.live/s/zijo987/208251706', 'info_dict': { 'id': '104125853_104125853_1656439572', 'ext': 'flv', 'uploader_url': 'https://trovo.live/zijo987', 'uploader_id': '104125853', 'thumbnail': 'https://livecover.trovo.live/screenshot/73846_104125853_104125853-2022-06-29-04-00-22-852x480.jpg', 'uploader': 'zijo987', 'title': 'šŸ’„IGRAMO IGRICE UPADAJTEšŸ’„2500/5000 2022-06-28 22:01', 'live_status': 'is_live', }, 'skip': 'May not be live' }] def _real_extract(self, url): username = self._match_id(url) live_info = self._call_api(username, data={ 'operationName': 'live_LiveReaderService_GetLiveInfo', 'variables': { 'params': { 'userName': username, }, }, }) if live_info.get('isLive') == 0: raise ExtractorError('%s is offline' % username, expected=True) program_info = live_info['programInfo'] program_id = program_info['id'] title = program_info['title'] formats = [] for stream_info in (program_info.get('streamInfo') or []): play_url = stream_info.get('playUrl') if not play_url: continue format_id = stream_info.get('desc') formats.append({ 'format_id': format_id, 'height': int_or_none(format_id[:-1]) if format_id else None, 'url': play_url, 'tbr': stream_info.get('bitrate'), 'http_headers': self._HEADERS, }) self._sort_formats(formats) info = { 'id': program_id, 'title': title, 'formats': formats, 'thumbnail': program_info.get('coverUrl'), 'is_live': True, } info.update(self._extract_streamer_info(live_info)) return info class TrovoVodIE(TrovoBaseIE): _VALID_URL = TrovoBaseIE._VALID_URL_BASE + r'(?:clip|video|s)/(?:[^/]+/\d+[^#]*[?&]vid=)?(?P(?[^\s]+)' IE_DESC = 'All VODs of a trovo.live channel; "trovovod:" prefix' _TESTS = [{ 'url': 'trovovod:OneTappedYou', 'playlist_mincount': 24, 'info_dict': { 'id': '100719456', }, }] _TYPE = 'video' def _get_vod_json(self, page, uid): return self._call_api(uid, data={ 'operationName': 'getChannelLtvVideoInfos', 'variables': { 'params': { 'channelID': int(uid), 'pageSize': 99, 'currPage': page, }, }, 'extensions': { 'persistedQuery': { 'version': 1, 'sha256Hash': '78fe32792005eab7e922cafcdad9c56bed8bbc5f5df3c7cd24fcb84a744f5f78', }, }, }) class TrovoChannelClipIE(TrovoChannelBaseIE): _VALID_URL = r'trovoclip:(?P[^\s]+)' IE_DESC = 'All Clips of a trovo.live channel; "trovoclip:" prefix' _TESTS = [{ 'url': 'trovoclip:OneTappedYou', 'playlist_mincount': 29, 'info_dict': { 'id': '100719456', }, }] _TYPE = 'clip' def _get_vod_json(self, page, uid): return self._call_api(uid, data={ 'operationName': 'getChannelClipVideoInfos', 'variables': { 'params': { 'channelID': int(uid), 'pageSize': 99, 'currPage': page, }, }, 'extensions': { 'persistedQuery': { 'version': 1, 'sha256Hash': 'e7924bfe20059b5c75fc8ff9e7929f43635681a7bdf3befa01072ed22c8eff31', }, }, })