Repository: yt-dlp/yt-dlp

Issue:
[TrovoVod] doesn't work
### Checklist

- [X] I'm reporting a broken site
- [X] I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are alive and playable in a browser
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [ ] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Worldwide

### Description

Don't know wat to say
It's broken I think 
Can't watch Yume chan (☍﹏⁰)｡

### Verbose log

```shell
[debug] Command-line config: ['-vU', 'https://trovo.live/video/ltv-108267285_108267285_387702299126518015']
[debug] Encodings: locale UTF-8, fs utf-8, out utf-8, err utf-8, pref UTF-8
[debug] yt-dlp version 2022.04.08 [7884ade65]
[debug] Python version 3.10.4 (CPython 64bit) - Linux-5.17.3-arch1-1-x86_64-with-glibc2.35
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.0 (setts), ffprobe 5.0, rtmpdump 2.4
[debug] Optional libraries: certifi, Cryptodome, mutagen, sqlite, websockets
[debug] Proxy map: {}
Latest version: 2022.04.08, Current version: 2022.04.08
yt-dlp is up to date (2022.04.08)
[debug] [TrovoVod] Extracting URL: https://trovo.live/video/ltv-108267285_108267285_387702299126518015
[TrovoVod] ltv-108267285_108267285_387702299126518015: Downloading JSON metadata
ERROR: [TrovoVod] ltv-108267285_108267285_387702299126518015: Unable to download JSON metadata: HTTP Error 403: Forbidden (caused by <HTTPError 403: 'Forbidden'>); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 641, in extract
    ie_result = self._real_extract(url)
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/trovo.py", line 120, in _real_extract
    resp = self._call_api(vid, data=json.dumps([{
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/trovo.py", line 22, in _call_api
    return self._download_json(
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 1029, in _download_json
    res = self._download_json_handle(
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 1008, in _download_json_handle
    res = self._download_webpage_handle(
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 800, in _download_webpage_handle
    urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data, headers=headers, query=query, expected_status=expected_status)
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 785, in _request_webpage
    raise ExtractorError(errmsg, cause=err)

  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 767, in _request_webpage
    return self._downloader.urlopen(url_or_request)
  File "/usr/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 3601, in urlopen
    return self._opener.open(req, timeout=self._socket_timeout)
  File "/usr/lib/python3.10/urllib/request.py", line 525, in open
    response = meth(req, response)
  File "/usr/lib/python3.10/urllib/request.py", line 634, in http_response
    response = self.parent.error(
  File "/usr/lib/python3.10/urllib/request.py", line 563, in error
    return self._call_chain(*args)
  File "/usr/lib/python3.10/urllib/request.py", line 496, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.10/urllib/request.py", line 643, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden
```

[TrovoVod] doesn't work
### Checklist

- [X] I'm reporting a broken site
- [X] I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are alive and playable in a browser
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [ ] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Worldwide

### Description

Don't know wat to say
It's broken I think 
Can't watch Yume chan (☍﹏⁰)｡

### Verbose log

```shell
[debug] Command-line config: ['-vU', 'https://trovo.live/video/ltv-108267285_108267285_387702299126518015']
[debug] Encodings: locale UTF-8, fs utf-8, out utf-8, err utf-8, pref UTF-8
[debug] yt-dlp version 2022.04.08 [7884ade65]
[debug] Python version 3.10.4 (CPython 64bit) - Linux-5.17.3-arch1-1-x86_64-with-glibc2.35
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.0 (setts), ffprobe 5.0, rtmpdump 2.4
[debug] Optional libraries: certifi, Cryptodome, mutagen, sqlite, websockets
[debug] Proxy map: {}
Latest version: 2022.04.08, Current version: 2022.04.08
yt-dlp is up to date (2022.04.08)
[debug] [TrovoVod] Extracting URL: https://trovo.live/video/ltv-108267285_108267285_387702299126518015
[TrovoVod] ltv-108267285_108267285_387702299126518015: Downloading JSON metadata
ERROR: [TrovoVod] ltv-108267285_108267285_387702299126518015: Unable to download JSON metadata: HTTP Error 403: Forbidden (caused by <HTTPError 403: 'Forbidden'>); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 641, in extract
    ie_result = self._real_extract(url)
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/trovo.py", line 120, in _real_extract
    resp = self._call_api(vid, data=json.dumps([{
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/trovo.py", line 22, in _call_api
    return self._download_json(
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 1029, in _download_json
    res = self._download_json_handle(
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 1008, in _download_json_handle
    res = self._download_webpage_handle(
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 800, in _download_webpage_handle
    urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data, headers=headers, query=query, expected_status=expected_status)
  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 785, in _request_webpage
    raise ExtractorError(errmsg, cause=err)

  File "/usr/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 767, in _request_webpage
    return self._downloader.urlopen(url_or_request)
  File "/usr/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 3601, in urlopen
    return self._opener.open(req, timeout=self._socket_timeout)
  File "/usr/lib/python3.10/urllib/request.py", line 525, in open
    response = meth(req, response)
  File "/usr/lib/python3.10/urllib/request.py", line 634, in http_response
    response = self.parent.error(
  File "/usr/lib/python3.10/urllib/request.py", line 563, in error
    return self._call_chain(*args)
  File "/usr/lib/python3.10/urllib/request.py", line 496, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.10/urllib/request.py", line 643, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden
```



Discussion:
Can confirm, Error 403 with VoDs and clips.
Can confirm, Error 403 with VoDs and clips.

Resolving patch:
diff --git a/yt_dlp/extractor/trovo.py b/yt_dlp/extractor/trovo.py
--- a/yt_dlp/extractor/trovo.py
+++ b/yt_dlp/extractor/trovo.py
@@ -1,5 +1,7 @@
 import itertools
 import json
+import random
+import string
 
 from .common import InfoExtractor
 from ..utils import (
@@ -15,10 +17,20 @@ class TrovoBaseIE(InfoExtractor):
     _VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
     _HEADERS = {'Origin': 'https://trovo.live'}
 
-    def _call_api(self, video_id, query=None, data=None):
-        return self._download_json(
-            'https://gql.trovo.live/', video_id, query=query, data=data,
-            headers={'Accept': 'application/json'})
+    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 {}
@@ -35,27 +47,14 @@ class TrovoIE(TrovoBaseIE):
 
     def _real_extract(self, url):
         username = self._match_id(url)
-        live_info = self._call_api(username, query={
-            'query': '''{
-  getLiveInfo(params: {userName: "%s"}) {
-    isLive
-    programInfo {
-      coverUrl
-      id
-      streamInfo {
-        desc
-        playUrl
-      }
-      title
-    }
-    streamerInfo {
-        nickName
-        uid
-        userName
-    }
-  }
-}''' % username,
-        })['data']['getLiveInfo']
+        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']
@@ -90,56 +89,61 @@ def _real_extract(self, url):
 class TrovoVodIE(TrovoBaseIE):
     _VALID_URL = TrovoBaseIE._VALID_URL_BASE + r'(?:clip|video)/(?P<id>[^/?&#]+)'
     _TESTS = [{
-        'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
+        'url': 'https://trovo.live/clip/lc-5285890818705062210?ltab=videos',
+        'params': {'getcomments': True},
         'info_dict': {
-            'id': 'ltv-100095501_100095501_1609596043',
+            'id': 'lc-5285890818705062210',
             'ext': 'mp4',
-            'title': 'Spontaner 12 Stunden Stream! - Ok Boomer!',
-            'uploader': 'Exsl',
-            'timestamp': 1609640305,
-            'upload_date': '20210103',
-            'uploader_id': '100095501',
-            'duration': 43977,
+            'title': 'fatal moaning for a super good🤣🤣',
+            'uploader': 'OneTappedYou',
+            'timestamp': 1621628019,
+            'upload_date': '20210521',
+            'uploader_id': '100719456',
+            'duration': 31,
             'view_count': int,
             'like_count': int,
             'comment_count': int,
-            'comments': 'mincount:8',
-            'categories': ['Grand Theft Auto V'],
+            'comments': 'mincount:1',
+            'categories': ['Call of Duty: Mobile'],
+            'uploader_url': 'https://trovo.live/OneTappedYou',
+            'thumbnail': r're:^https?://.*\.jpg',
         },
-        'skip': '404'
     }, {
-        'url': 'https://trovo.live/clip/lc-5285890810184026005',
+        'url': 'https://trovo.live/video/ltv-100095501_100095501_1609596043',
         'only_matching': True,
     }]
 
     def _real_extract(self, url):
         vid = self._match_id(url)
-        resp = self._call_api(vid, data=json.dumps([{
-            'query': '''{
-  batchGetVodDetailInfo(params: {vids: ["%s"]}) {
-    VodDetailInfos
-  }
-}''' % vid,
-        }, {
-            'query': '''{
-  getCommentList(params: {appInfo: {postID: "%s"}, pageSize: 1000000000, preview: {}}) {
-    commentList {
-      author {
-        nickName
-        uid
-      }
-      commentID
-      content
-      createdAt
-      parentID
-    }
-  }
-}''' % vid,
-        }]).encode())
-        vod_detail_info = resp[0]['data']['batchGetVodDetailInfo']['VodDetailInfos'][vid]
+
+        # NOTE: It is also possible to extract this info from the Nuxt data on the website,
+        # however that seems unreliable - sometimes it randomly doesn't return the data,
+        # at least when using a non-residential IP.
+        resp = self._call_api(vid, data={
+            'operationName': 'batchGetVodDetailInfo',
+            'variables': {
+                'params': {
+                    'vids': [vid],
+                },
+            },
+            'extensions': {
+                'persistedQuery': {
+                    'version': 1,
+                    'sha256Hash': 'ceae0355d66476e21a1dd8e8af9f68de95b4019da2cda8b177c9a2255dad31d0',
+                },
+            },
+        })
+        vod_detail_info = resp['VodDetailInfos'][vid]
         vod_info = vod_detail_info['vodInfo']
         title = vod_info['title']
 
+        if try_get(vod_info, lambda x: x['playbackRights']['playbackRights'] != 'Normal'):
+            playback_rights_setting = vod_info['playbackRights']['playbackRightsSetting']
+            if playback_rights_setting == 'SubscriberOnly':
+                raise ExtractorError('This video is only available for subscribers', expected=True)
+            else:
+                raise ExtractorError(f'This video is not available ({playback_rights_setting})', expected=True)
+
         language = vod_info.get('languageName')
         formats = []
         for play_info in (vod_info.get('playInfos') or []):
@@ -163,23 +167,6 @@ def _real_extract(self, url):
         category = vod_info.get('categoryName')
         get_count = lambda x: int_or_none(vod_info.get(x + 'Num'))
 
-        comment_list = try_get(resp, lambda x: x[1]['data']['getCommentList']['commentList'], list) or []
-        comments = []
-        for comment in comment_list:
-            content = comment.get('content')
-            if not content:
-                continue
-            author = comment.get('author') or {}
-            parent = comment.get('parentID')
-            comments.append({
-                'author': author.get('nickName'),
-                'author_id': str_or_none(author.get('uid')),
-                'id': str_or_none(comment.get('commentID')),
-                'text': content,
-                'timestamp': int_or_none(comment.get('createdAt')),
-                'parent': 'root' if parent == 0 else str_or_none(parent),
-            })
-
         info = {
             'id': vid,
             'title': title,
@@ -190,12 +177,51 @@ def _real_extract(self, url):
             'view_count': get_count('watch'),
             'like_count': get_count('like'),
             'comment_count': get_count('comment'),
-            'comments': comments,
             'categories': [category] if category else None,
+            '__post_extractor': self.extract_comments(vid),
         }
         info.update(self._extract_streamer_info(vod_detail_info))
         return info
 
+    def _get_comments(self, vid):
+        for page in itertools.count(1):
+            comments_json = self._call_api(vid, data={
+                'operationName': 'getCommentList',
+                'variables': {
+                    'params': {
+                        'appInfo': {
+                            'postID': vid,
+                        },
+                        'preview': {},
+                        'pageSize': 99,
+                        'page': page,
+                    },
+                },
+                'extensions': {
+                    'persistedQuery': {
+                        'version': 1,
+                        'sha256Hash': 'be8e5f9522ddac7f7c604c0d284fd22481813263580849926c4c66fb767eed25',
+                    },
+                },
+            })
+            for comment in comments_json['commentList']:
+                content = comment.get('content')
+                if not content:
+                    continue
+                author = comment.get('author') or {}
+                parent = comment.get('parentID')
+                yield {
+                    'author': author.get('nickName'),
+                    'author_id': str_or_none(author.get('uid')),
+                    'id': str_or_none(comment.get('commentID')),
+                    'text': content,
+                    'timestamp': int_or_none(comment.get('createdAt')),
+                    'parent': 'root' if parent == 0 else str_or_none(parent),
+                }
+
+            if comments_json['lastPage']:
+                break
+
 
 class TrovoChannelBaseIE(TrovoBaseIE):
     def _get_vod_json(self, page, uid):
@@ -215,9 +241,15 @@ def _entries(self, uid):
 
     def _real_extract(self, url):
         id = self._match_id(url)
-        uid = str(self._call_api(id, query={
-            'query': '{getLiveInfo(params:{userName:"%s"}){streamerInfo{uid}}}' % id
-        })['data']['getLiveInfo']['streamerInfo']['uid'])
+        live_info = self._call_api(id, data={
+            'operationName': 'live_LiveReaderService_GetLiveInfo',
+            'variables': {
+                'params': {
+                    'userName': id,
+                },
+            },
+        })
+        uid = str(live_info['streamerInfo']['uid'])
         return self.playlist_result(self._entries(uid), playlist_id=uid)
 
 
@@ -233,13 +265,25 @@ class TrovoChannelVodIE(TrovoChannelBaseIE):
         },
     }]
 
-    _QUERY = '{getChannelLtvVideoInfos(params:{pageSize:99,currPage:%d,channelID:%s}){hasMore,vodInfos{vid}}}'
     _TYPE = 'video'
 
     def _get_vod_json(self, page, uid):
-        return self._call_api(uid, query={
-            'query': self._QUERY % (page, uid)
-        })['data']['getChannelLtvVideoInfos']
+        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):
@@ -254,10 +298,22 @@ class TrovoChannelClipIE(TrovoChannelBaseIE):
         },
     }]
 
-    _QUERY = '{getChannelClipVideoInfos(params:{pageSize:99,currPage:%d,channelID:%s,albumType:VOD_CLIP_ALBUM_TYPE_LATEST}){hasMore,vodInfos{vid}}}'
     _TYPE = 'clip'
 
     def _get_vod_json(self, page, uid):
-        return self._call_api(uid, query={
-            'query': self._QUERY % (page, uid)
-        })['data']['getChannelClipVideoInfos']
+        return self._call_api(uid, data={
+            'operationName': 'getChannelClipVideoInfos',
+            'variables': {
+                'params': {
+                    'channelID': int(uid),
+                    'pageSize': 99,
+                    'currPage': page,
+                },
+            },
+            'extensions': {
+                'persistedQuery': {
+                    'version': 1,
+                    'sha256Hash': 'e7924bfe20059b5c75fc8ff9e7929f43635681a7bdf3befa01072ed22c8eff31',
+                },
+            },
+        })


Test patch: