Repository: yt-dlp/yt-dlp
Pull request #1983 (2021-12-12 21:49:56)

[extractor/common] Tweak JSON-LD extraction to support default implicit graph
## Please follow the guide below

- You will be asked some questions, please read them **carefully** and answer honestly
- Put an `x` into all the boxes [ ] relevant to your *pull request* (like that [x])
- Use *Preview* tab to see how your *pull request* will actually look like

---

### Before submitting a *pull request* make sure you have:
- [x] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [x] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
- [x] Checked the code with [flake8](https://pypi.python.org/pypi/flake8)

### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check one of the following options:
- [x] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/)
- [ ] I am not the original author of this code but it is in public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)

### What is the purpose of your *pull request*?
- [ ] Bug fix
- [x] Improvement
- [ ] New extractor
- [ ] New feature

---

### Description of your *pull request* and other information

#### JSON-LD extraction improvements

  * Support top-level @graph expressing implicit default graph
    * Wrap the control flow block in a function, which is called recursively upon such a structure
    * Per [W3C JSON-LD v1.1 §4.9 (non-normative ref)](https://www.w3.org/TR/json-ld11/#named-graphs):

> When a JSON-LD document's top-level structure is a map that contains no other keys than `@graph` and optionally `@context` (properties that are not mapped to an IRI or a keyword are ignored), `@graph` is  considered to express the otherwise implicit default graph.

  * Extend `TestInfoExtractor.test_search_json_ld_realworld` to cover said syntax (structure)
    * Refactor tests in a list of 3-tuples: test html string, expected dict, keyword args for `InfoExtractor._search_json_ld`
    * Add new test case for said syntax (structure)
  * Tweak description extraction from nodes with type `Article` or `NewsArticle`: extract description from `articleBody` and fall back to `description` field

This patch was ported from ytdl-org/youtube-dl#30229.

zmousm: This patch, namely b7359b8, fixes extraction of JSON-LD metadata for `Ant1NewsGrArticleIE` introduced in #1982; this error is currently handled therein as a non-fatal expected warning.

Patch:
diff --git a/README.md b/README.md
index ba4588c55..2041019c7 100644
--- a/README.md
+++ b/README.md
@@ -1565,8 +1565,10 @@ #### youtube
 * `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
 * `include_live_dash`: Include live dash formats (These formats don't download properly)
 * `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
-* `max_comments`: Maximum amount of comments to download (default all)
-* `max_comment_depth`: Maximum depth for nested comments. YouTube supports depths 1 or 2 (default)
+* `max_comments`: Limit the amount of comments to gather. Comma-seperated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`.
+    * E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total.
+* `max_comment_depth` Maximum depth for nested comments. YouTube supports depths 1 or 2 (default)
+    * **Deprecated**: Set `max-replies` to `0` or `all` in `max_comments` instead (e.g. `max_comments=all,all,0` to get no replies) 
 
 #### youtubetab (YouTube playlists, channels, feeds, etc.)
 * `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index 39fd2e1fd..317526d10 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -2673,6 +2673,9 @@ def process_info(self, info_dict):
             if self._num_downloads >= int(max_downloads):
                 raise MaxDownloadsReached()
 
+        if info_dict.get('is_live'):
+            info_dict['title'] += ' ' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
+
         # TODO: backward compatibility, to be removed
         info_dict['fulltitle'] = info_dict['title']
 
diff --git a/yt_dlp/extractor/abc.py b/yt_dlp/extractor/abc.py
index 3e202168e..e3369306c 100644
--- a/yt_dlp/extractor/abc.py
+++ b/yt_dlp/extractor/abc.py
@@ -233,8 +233,6 @@ def tokenize_url(url, token):
             }]
 
         is_live = video_params.get('livestream') == '1'
-        if is_live:
-            title = self._live_title(title)
 
         return {
             'id': video_id,
diff --git a/yt_dlp/extractor/adobeconnect.py b/yt_dlp/extractor/adobeconnect.py
index 728549eb9..e688dddcb 100644
--- a/yt_dlp/extractor/adobeconnect.py
+++ b/yt_dlp/extractor/adobeconnect.py
@@ -31,7 +31,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'formats': formats,
             'is_live': is_live,
         }
diff --git a/yt_dlp/extractor/arcpublishing.py b/yt_dlp/extractor/arcpublishing.py
index 5a9b8181a..1943fd5f8 100644
--- a/yt_dlp/extractor/arcpublishing.py
+++ b/yt_dlp/extractor/arcpublishing.py
@@ -158,7 +158,7 @@ def _real_extract(self, url):
 
         return {
             'id': uuid,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'thumbnail': try_get(video, lambda x: x['promo_image']['url']),
             'description': try_get(video, lambda x: x['subheadlines']['basic']),
             'formats': formats,
diff --git a/yt_dlp/extractor/ard.py b/yt_dlp/extractor/ard.py
index f8d57109e..1aff0361c 100644
--- a/yt_dlp/extractor/ard.py
+++ b/yt_dlp/extractor/ard.py
@@ -280,7 +280,7 @@ def _real_extract(self, url):
 
         info.update({
             'id': video_id,
-            'title': self._live_title(title) if info.get('is_live') else title,
+            'title': title,
             'description': description,
             'thumbnail': thumbnail,
         })
diff --git a/yt_dlp/extractor/awaan.py b/yt_dlp/extractor/awaan.py
index 22cc10d98..b5d1b57af 100644
--- a/yt_dlp/extractor/awaan.py
+++ b/yt_dlp/extractor/awaan.py
@@ -41,7 +41,7 @@ def _parse_video_data(self, video_data, video_id, is_live):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': video_data.get('description_en') or video_data.get('description_ar'),
             'thumbnail': 'http://admin.mangomolo.com/analytics/%s' % img if img else None,
             'duration': int_or_none(video_data.get('duration')),
diff --git a/yt_dlp/extractor/bitwave.py b/yt_dlp/extractor/bitwave.py
index eb16c469d..e6e093f59 100644
--- a/yt_dlp/extractor/bitwave.py
+++ b/yt_dlp/extractor/bitwave.py
@@ -51,7 +51,7 @@ def _real_extract(self, url):
 
         return {
             'id': username,
-            'title': self._live_title(channel['data']['title']),
+            'title': channel['data']['title'],
             'uploader': username,
             'uploader_id': username,
             'formats': formats,
diff --git a/yt_dlp/extractor/bongacams.py b/yt_dlp/extractor/bongacams.py
index 9e7551136..4e346e7b6 100644
--- a/yt_dlp/extractor/bongacams.py
+++ b/yt_dlp/extractor/bongacams.py
@@ -49,7 +49,7 @@ def _real_extract(self, url):
 
         return {
             'id': channel_id,
-            'title': self._live_title(uploader or uploader_id),
+            'title': uploader or uploader_id,
             'uploader': uploader,
             'uploader_id': uploader_id,
             'like_count': like_count,
diff --git a/yt_dlp/extractor/brightcove.py b/yt_dlp/extractor/brightcove.py
index 82bb76f29..dcd332b43 100644
--- a/yt_dlp/extractor/brightcove.py
+++ b/yt_dlp/extractor/brightcove.py
@@ -581,10 +581,9 @@ def build_format_id(kind):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': clean_html(json_data.get('description')),
-            'thumbnail': json_data.get('thumbnail') or json_data.get('poster'),
-            'thumbnials': thumbnails,
+            'thumbnails': thumbnails,
             'duration': duration,
             'timestamp': parse_iso8601(json_data.get('published_at')),
             'uploader_id': json_data.get('account_id'),
diff --git a/yt_dlp/extractor/cam4.py b/yt_dlp/extractor/cam4.py
index 30daf2be9..f47de9176 100644
--- a/yt_dlp/extractor/cam4.py
+++ b/yt_dlp/extractor/cam4.py
@@ -25,7 +25,7 @@ def _real_extract(self, url):
 
         return {
             'id': channel_id,
-            'title': self._live_title(channel_id),
+            'title': channel_id,
             'is_live': True,
             'age_limit': 18,
             'formats': formats,
diff --git a/yt_dlp/extractor/cammodels.py b/yt_dlp/extractor/cammodels.py
index eb2a8b4c6..3dc19377b 100644
--- a/yt_dlp/extractor/cammodels.py
+++ b/yt_dlp/extractor/cammodels.py
@@ -91,7 +91,7 @@ def _real_extract(self, url):
 
         return {
             'id': user_id,
-            'title': self._live_title(user_id),
+            'title': user_id,
             'is_live': True,
             'formats': formats,
             'age_limit': 18
diff --git a/yt_dlp/extractor/ceskatelevize.py b/yt_dlp/extractor/ceskatelevize.py
index 6ca2f38b5..6c90b247e 100644
--- a/yt_dlp/extractor/ceskatelevize.py
+++ b/yt_dlp/extractor/ceskatelevize.py
@@ -212,8 +212,6 @@ def _real_extract(self, url):
 
                 if playlist_len == 1:
                     final_title = playlist_title or title
-                    if is_live:
-                        final_title = self._live_title(final_title)
                 else:
                     final_title = '%s (%s)' % (playlist_title, title)
 
diff --git a/yt_dlp/extractor/chaturbate.py b/yt_dlp/extractor/chaturbate.py
index a459dcb8d..8da51f919 100644
--- a/yt_dlp/extractor/chaturbate.py
+++ b/yt_dlp/extractor/chaturbate.py
@@ -101,7 +101,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(video_id),
+            'title': video_id,
             'thumbnail': 'https://roomimg.stream.highwebmedia.com/ri/%s.jpg' % video_id,
             'age_limit': self._rta_search(webpage),
             'is_live': True,
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
index 5562d1862..52099b4b4 100644
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -3,7 +3,6 @@
 
 import base64
 import collections
-import datetime
 import hashlib
 import itertools
 import json
@@ -1452,8 +1451,13 @@ def extract_video_object(e):
             })
             extract_interaction_statistic(e)
 
-        for e in json_ld:
-            if '@context' in e:
+        def traverse_json_ld(json_ld, at_top_level=True):
+            for e in json_ld:
+                if at_top_level and '@context' not in e:
+                    continue
+                if at_top_level and set(e.keys()) == {'@context', '@graph'}:
+                    traverse_json_ld(variadic(e['@graph'], allowed_types=(dict,)), at_top_level=False)
+                    break
                 item_type = e.get('@type')
                 if expected_type is not None and expected_type != item_type:
                     continue
@@ -1489,7 +1493,7 @@ def extract_video_object(e):
                     info.update({
                         'timestamp': parse_iso8601(e.get('datePublished')),
                         'title': unescapeHTML(e.get('headline')),
-                        'description': unescapeHTML(e.get('articleBody')),
+                        'description': unescapeHTML(e.get('articleBody') or e.get('description')),
                     })
                 elif item_type == 'VideoObject':
                     extract_video_object(e)
@@ -1504,6 +1508,8 @@ def extract_video_object(e):
                     continue
                 else:
                     break
+        traverse_json_ld(json_ld)
+
         return dict((k, v) for k, v in info.items() if v is not None)
 
     def _search_nextjs_data(self, webpage, video_id, **kw):
@@ -3456,10 +3462,8 @@ def _parse_jwplayer_formats(self, jwplayer_sources_data, video_id=None,
         return formats
 
     def _live_title(self, name):
-        """ Generate the title for a live video """
-        now = datetime.datetime.now()
-        now_str = now.strftime('%Y-%m-%d %H:%M')
-        return name + ' ' + now_str
+        self._downloader.deprecation_warning('yt_dlp.InfoExtractor._live_title is deprecated and does not work as expected')
+        return name
 
     def _int(self, v, name, fatal=False, **kwargs):
         res = int_or_none(v, **kwargs)
diff --git a/yt_dlp/extractor/dailymotion.py b/yt_dlp/extractor/dailymotion.py
index e04e10b86..b4211e1e4 100644
--- a/yt_dlp/extractor/dailymotion.py
+++ b/yt_dlp/extractor/dailymotion.py
@@ -305,7 +305,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': clean_html(media.get('description')),
             'thumbnails': thumbnails,
             'duration': int_or_none(metadata.get('duration')) or None,
diff --git a/yt_dlp/extractor/dlive.py b/yt_dlp/extractor/dlive.py
index 90462c0ab..7410eb6c8 100644
--- a/yt_dlp/extractor/dlive.py
+++ b/yt_dlp/extractor/dlive.py
@@ -84,7 +84,7 @@ def _real_extract(self, url):
         self._sort_formats(formats)
         return {
             'id': display_name,
-            'title': self._live_title(title),
+            'title': title,
             'uploader': display_name,
             'uploader_id': username,
             'formats': formats,
diff --git a/yt_dlp/extractor/douyutv.py b/yt_dlp/extractor/douyutv.py
index 9757f4422..26a8d645c 100644
--- a/yt_dlp/extractor/douyutv.py
+++ b/yt_dlp/extractor/douyutv.py
@@ -105,7 +105,7 @@ def _real_extract(self, url):
                 'aid': 'pcclient'
             })['data']['live_url']
 
-        title = self._live_title(unescapeHTML(room['room_name']))
+        title = unescapeHTML(room['room_name'])
         description = room.get('show_details')
         thumbnail = room.get('room_src')
         uploader = room.get('nickname')
diff --git a/yt_dlp/extractor/drtv.py b/yt_dlp/extractor/drtv.py
index 7bb15f8d4..70134204c 100644
--- a/yt_dlp/extractor/drtv.py
+++ b/yt_dlp/extractor/drtv.py
@@ -321,7 +321,7 @@ def _real_extract(self, url):
         channel_data = self._download_json(
             'https://www.dr.dk/mu-online/api/1.0/channel/' + channel_id,
             channel_id)
-        title = self._live_title(channel_data['Title'])
+        title = channel_data['Title']
 
         formats = []
         for streaming_server in channel_data.get('StreamingServers', []):
diff --git a/yt_dlp/extractor/filmon.py b/yt_dlp/extractor/filmon.py
index f775fe0ba..7b43ecc0f 100644
--- a/yt_dlp/extractor/filmon.py
+++ b/yt_dlp/extractor/filmon.py
@@ -170,7 +170,7 @@ def _real_extract(self, url):
         return {
             'id': channel_id,
             'display_id': channel_data.get('alias'),
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': channel_data.get('description'),
             'thumbnails': thumbnails,
             'formats': formats,
diff --git a/yt_dlp/extractor/francetv.py b/yt_dlp/extractor/francetv.py
index bc5ef4df9..877c5c055 100644
--- a/yt_dlp/extractor/francetv.py
+++ b/yt_dlp/extractor/francetv.py
@@ -203,7 +203,7 @@ def _extract_video(self, video_id, catalogue=None):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'thumbnail': image,
             'duration': duration,
             'timestamp': timestamp,
diff --git a/yt_dlp/extractor/freshlive.py b/yt_dlp/extractor/freshlive.py
index 72a845945..ad19b8109 100644
--- a/yt_dlp/extractor/freshlive.py
+++ b/yt_dlp/extractor/freshlive.py
@@ -59,9 +59,6 @@ def _real_extract(self, url):
             stream_url, video_id, 'mp4',
             'm3u8_native', m3u8_id='hls')
 
-        if is_live:
-            title = self._live_title(title)
-
         return {
             'id': video_id,
             'formats': formats,
diff --git a/yt_dlp/extractor/hitbox.py b/yt_dlp/extractor/hitbox.py
index 3e5ff2685..0470d0a99 100644
--- a/yt_dlp/extractor/hitbox.py
+++ b/yt_dlp/extractor/hitbox.py
@@ -209,6 +209,6 @@ def _real_extract(self, url):
             'https://www.smashcast.tv/api/media/live', video_id)
         metadata['formats'] = formats
         metadata['is_live'] = True
-        metadata['title'] = self._live_title(metadata.get('title'))
+        metadata['title'] = metadata.get('title')
 
         return metadata
diff --git a/yt_dlp/extractor/imggaming.py b/yt_dlp/extractor/imggaming.py
index ef20a4b9e..14d3fad55 100644
--- a/yt_dlp/extractor/imggaming.py
+++ b/yt_dlp/extractor/imggaming.py
@@ -88,7 +88,7 @@ def _real_extract(self, url):
         video_data = self._download_json(dve_api_url, media_id)
         is_live = media_type == 'live'
         if is_live:
-            title = self._live_title(self._call_api('event/', media_id)['title'])
+            title = self._call_api('event/', media_id)['title']
         else:
             title = video_data['name']
 
diff --git a/yt_dlp/extractor/ivideon.py b/yt_dlp/extractor/ivideon.py
index 01e7b22d4..44b220846 100644
--- a/yt_dlp/extractor/ivideon.py
+++ b/yt_dlp/extractor/ivideon.py
@@ -75,7 +75,7 @@ def _real_extract(self, url):
 
         return {
             'id': server_id,
-            'title': self._live_title(camera_name or server_id),
+            'title': camera_name or server_id,
             'description': description,
             'is_live': True,
             'formats': formats,
diff --git a/yt_dlp/extractor/laola1tv.py b/yt_dlp/extractor/laola1tv.py
index fa217365a..b5d27c2f0 100644
--- a/yt_dlp/extractor/laola1tv.py
+++ b/yt_dlp/extractor/laola1tv.py
@@ -112,7 +112,7 @@ def get_flashvar(x, *args, **kwargs):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'upload_date': unified_strdate(_v('time_date')),
             'uploader': _v('meta_organisation'),
             'categories': categories,
@@ -161,7 +161,7 @@ def _extract_video(self, url):
         return {
             'id': video_id,
             'display_id': display_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': video_data.get('description'),
             'thumbnail': video_data.get('image'),
             'categories': categories,
diff --git a/yt_dlp/extractor/line.py b/yt_dlp/extractor/line.py
index d4bcae6c1..e1d5f21e1 100644
--- a/yt_dlp/extractor/line.py
+++ b/yt_dlp/extractor/line.py
@@ -116,7 +116,7 @@ def _parse_broadcast_item(self, item):
 
         return {
             'id': broadcast_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'thumbnails': thumbnails,
             'timestamp': int_or_none(item.get('createdAt')),
             'channel': channel.get('name'),
diff --git a/yt_dlp/extractor/livestream.py b/yt_dlp/extractor/livestream.py
index f591289ec..45bf26d26 100644
--- a/yt_dlp/extractor/livestream.py
+++ b/yt_dlp/extractor/livestream.py
@@ -176,7 +176,7 @@ def _extract_stream_info(self, stream_info):
         return {
             'id': broadcast_id,
             'formats': formats,
-            'title': self._live_title(stream_info['stream_title']) if is_live else stream_info['stream_title'],
+            'title': stream_info['stream_title'],
             'thumbnail': stream_info.get('thumbnail_url'),
             'is_live': is_live,
         }
@@ -344,7 +344,7 @@ def _real_extract(self, url):
             is_live = video_data.get('isLive')
             info.update({
                 'id': content_id,
-                'title': self._live_title(info['title']) if is_live else info['title'],
+                'title': info['title'],
                 'formats': self._extract_video_formats(video_data, content_id),
                 'is_live': is_live,
             })
diff --git a/yt_dlp/extractor/mangomolo.py b/yt_dlp/extractor/mangomolo.py
index acee370e9..68ce138b3 100644
--- a/yt_dlp/extractor/mangomolo.py
+++ b/yt_dlp/extractor/mangomolo.py
@@ -33,7 +33,7 @@ def _real_extract(self, url):
 
         return {
             'id': page_id,
-            'title': self._live_title(page_id) if self._IS_LIVE else page_id,
+            'title': page_id,
             'uploader_id': hidden_inputs.get('userid'),
             'duration': int_or_none(hidden_inputs.get('duration')),
             'is_live': self._IS_LIVE,
diff --git a/yt_dlp/extractor/matchtv.py b/yt_dlp/extractor/matchtv.py
index bc9933a81..e003b8d25 100644
--- a/yt_dlp/extractor/matchtv.py
+++ b/yt_dlp/extractor/matchtv.py
@@ -49,7 +49,7 @@ def _real_extract(self, url):
         self._sort_formats(formats)
         return {
             'id': video_id,
-            'title': self._live_title('Матч ТВ - Прямой эфир'),
+            'title': 'Матч ТВ - Прямой эфир',
             'is_live': True,
             'formats': formats,
         }
diff --git a/yt_dlp/extractor/muenchentv.py b/yt_dlp/extractor/muenchentv.py
index d256236d1..a53929e1b 100644
--- a/yt_dlp/extractor/muenchentv.py
+++ b/yt_dlp/extractor/muenchentv.py
@@ -33,7 +33,7 @@ def _real_extract(self, url):
         display_id = 'live'
         webpage = self._download_webpage(url, display_id)
 
-        title = self._live_title(self._og_search_title(webpage))
+        title = self._og_search_title(webpage)
 
         data_js = self._search_regex(
             r'(?s)\nplaylist:\s*(\[.*?}\]),',
diff --git a/yt_dlp/extractor/nbc.py b/yt_dlp/extractor/nbc.py
index f304f191a..cd573690b 100644
--- a/yt_dlp/extractor/nbc.py
+++ b/yt_dlp/extractor/nbc.py
@@ -305,7 +305,7 @@ def _real_extract(self, url):
         self._sort_formats(formats)
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': live_source.get('description'),
             'formats': formats,
             'is_live': is_live,
@@ -545,8 +545,6 @@ def _real_extract(self, url):
 
         title = event_config['eventTitle']
         is_live = {'live': True, 'replay': False}.get(event_config.get('eventStatus'))
-        if is_live:
-            title = self._live_title(title)
 
         source_url = self._download_json(
             f'https://api-leap.nbcsports.com/feeds/assets/{pid}?application=NBCOlympics&platform=desktop&format=nbc-player&env=staging',
diff --git a/yt_dlp/extractor/ndr.py b/yt_dlp/extractor/ndr.py
index f2bae2c1a..1917254b8 100644
--- a/yt_dlp/extractor/ndr.py
+++ b/yt_dlp/extractor/ndr.py
@@ -245,8 +245,6 @@ def _real_extract(self, url):
 
         live = playlist.get('config', {}).get('streamType') in ['httpVideoLive', 'httpAudioLive']
         title = config['title']
-        if live:
-            title = self._live_title(title)
         uploader = ppjson.get('config', {}).get('branding')
         upload_date = ppjson.get('config', {}).get('publicationDate')
         duration = int_or_none(config.get('duration'))
diff --git a/yt_dlp/extractor/nfl.py b/yt_dlp/extractor/nfl.py
index 871923e4c..821276a31 100644
--- a/yt_dlp/extractor/nfl.py
+++ b/yt_dlp/extractor/nfl.py
@@ -89,7 +89,7 @@ def _parse_video_config(self, video_config, display_id):
                     'ext': determine_ext(image_url, 'jpg'),
                 }]
             info.update({
-                'title': self._live_title(title) if is_live else title,
+                'title': title,
                 'is_live': is_live,
                 'description': clean_html(item.get('description')),
                 'thumbnails': thumbnails,
diff --git a/yt_dlp/extractor/nova.py b/yt_dlp/extractor/nova.py
index bb8f6dbf7..00a64f88d 100644
--- a/yt_dlp/extractor/nova.py
+++ b/yt_dlp/extractor/nova.py
@@ -54,7 +54,7 @@ def _real_extract(self, url):
 
         player = self._parse_json(
             self._search_regex(
-                (r'(?:replacePlaceholders.*?:\s*)?replacePlaceholders\s*\(\s*(?P<json>{.*})\s*\)(?:\s*\))?\s*,',
+                (r'(?:(?:replacePlaceholders|processAdTagModifier).*?:\s*)?(?:replacePlaceholders|processAdTagModifier)\s*\(\s*(?P<json>{.*?})\s*\)(?:\s*\))?\s*,',
                     r'Player\.init\s*\([^,]+,(?P<cndn>\s*\w+\s*\?)?\s*(?P<json>{(?(cndn).+?|.+)})\s*(?(cndn):|,\s*{.+?}\s*\)\s*;)'),
                 webpage, 'player', default='{}', group='json'), video_id, fatal=False)
         if player:
diff --git a/yt_dlp/extractor/npo.py b/yt_dlp/extractor/npo.py
index ed547d04b..a8aaef6f1 100644
--- a/yt_dlp/extractor/npo.py
+++ b/yt_dlp/extractor/npo.py
@@ -467,7 +467,7 @@ def add_format_url(format_url):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': metadata.get('info'),
             'thumbnail': metadata.get('images', [{'url': None}])[-1]['url'],
             'upload_date': unified_strdate(metadata.get('gidsdatum')),
@@ -561,7 +561,7 @@ def _real_extract(self, url):
         return {
             'id': video_id,
             'url': stream['url'],
-            'title': self._live_title(title),
+            'title': title,
             'acodec': codec,
             'ext': codec,
             'is_live': True,
diff --git a/yt_dlp/extractor/odnoklassniki.py b/yt_dlp/extractor/odnoklassniki.py
index eeb57f2fe..42f210a9b 100644
--- a/yt_dlp/extractor/odnoklassniki.py
+++ b/yt_dlp/extractor/odnoklassniki.py
@@ -235,7 +235,7 @@ def _extract_desktop(self, url):
 
         assert title
         if provider == 'LIVE_TV_APP':
-            info['title'] = self._live_title(title)
+            info['title'] = title
 
         quality = qualities(('4', '0', '1', '2', '3', '5'))
 
diff --git a/yt_dlp/extractor/oktoberfesttv.py b/yt_dlp/extractor/oktoberfesttv.py
index a914068f9..276567436 100644
--- a/yt_dlp/extractor/oktoberfesttv.py
+++ b/yt_dlp/extractor/oktoberfesttv.py
@@ -25,8 +25,8 @@ def _real_extract(self, url):
         video_id = self._match_id(url)
         webpage = self._download_webpage(url, video_id)
 
-        title = self._live_title(self._html_search_regex(
-            r'<h1><strong>.*?</strong>(.*?)</h1>', webpage, 'title'))
+        title = self._html_search_regex(
+            r'<h1><strong>.*?</strong>(.*?)</h1>', webpage, 'title')
 
         clip = self._search_regex(
             r"clip:\s*\{\s*url:\s*'([^']+)'", webpage, 'clip')
diff --git a/yt_dlp/extractor/picarto.py b/yt_dlp/extractor/picarto.py
index 17d08d69e..adf21fda8 100644
--- a/yt_dlp/extractor/picarto.py
+++ b/yt_dlp/extractor/picarto.py
@@ -77,7 +77,7 @@ def _real_extract(self, url):
 
         return {
             'id': channel_id,
-            'title': self._live_title(title.strip()),
+            'title': title.strip(),
             'is_live': True,
             'channel': channel_id,
             'channel_id': metadata.get('id'),
diff --git a/yt_dlp/extractor/playtvak.py b/yt_dlp/extractor/playtvak.py
index 84e92dda4..30c8a599e 100644
--- a/yt_dlp/extractor/playtvak.py
+++ b/yt_dlp/extractor/playtvak.py
@@ -167,8 +167,6 @@ def _real_extract(self, url):
 
         title = item['title']
         is_live = item['type'] == 'stream'
-        if is_live:
-            title = self._live_title(title)
         description = self._og_search_description(webpage, default=None) or self._html_search_meta(
             'description', webpage, 'description', default=None)
         timestamp = None
diff --git a/yt_dlp/extractor/radiode.py b/yt_dlp/extractor/radiode.py
index 2c06c8b1e..038287363 100644
--- a/yt_dlp/extractor/radiode.py
+++ b/yt_dlp/extractor/radiode.py
@@ -29,7 +29,7 @@ def _real_extract(self, url):
             webpage, 'broadcast')
 
         broadcast = self._parse_json(jscode, radio_id)
-        title = self._live_title(broadcast['name'])
+        title = broadcast['name']
         description = broadcast.get('description') or broadcast.get('shortDescription')
         thumbnail = broadcast.get('picture4Url') or broadcast.get('picture4TransUrl') or broadcast.get('logo100x100')
 
diff --git a/yt_dlp/extractor/rai.py b/yt_dlp/extractor/rai.py
index 4699fe17e..39e57decd 100644
--- a/yt_dlp/extractor/rai.py
+++ b/yt_dlp/extractor/rai.py
@@ -318,8 +318,7 @@ def _real_extract(self, url):
         info = {
             'id': remove_start(media.get('id'), 'ContentItem-') or video_id,
             'display_id': video_id,
-            'title': self._live_title(title) if relinker_info.get(
-                'is_live') else title,
+            'title': title,
             'alt_title': strip_or_none(alt_title),
             'description': media.get('description'),
             'uploader': strip_or_none(media.get('channel')),
diff --git a/yt_dlp/extractor/rtbf.py b/yt_dlp/extractor/rtbf.py
index f9979d0a4..4b61fdb17 100644
--- a/yt_dlp/extractor/rtbf.py
+++ b/yt_dlp/extractor/rtbf.py
@@ -85,8 +85,6 @@ def _real_extract(self, url):
 
         title = data['title']
         is_live = data.get('isLive')
-        if is_live:
-            title = self._live_title(title)
         height_re = r'-(\d+)p\.'
         formats = []
 
diff --git a/yt_dlp/extractor/rtve.py b/yt_dlp/extractor/rtve.py
index 59832eeac..0654fb08b 100644
--- a/yt_dlp/extractor/rtve.py
+++ b/yt_dlp/extractor/rtve.py
@@ -160,7 +160,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'formats': formats,
             'thumbnail': info.get('image'),
             'subtitles': subtitles,
@@ -230,7 +230,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(title),
+            'title': title,
             'formats': self._extract_png_formats(vidplayer_id),
             'is_live': True,
         }
diff --git a/yt_dlp/extractor/rutv.py b/yt_dlp/extractor/rutv.py
index 7e0de994a..3de86b232 100644
--- a/yt_dlp/extractor/rutv.py
+++ b/yt_dlp/extractor/rutv.py
@@ -201,7 +201,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': description,
             'thumbnail': thumbnail,
             'view_count': view_count,
diff --git a/yt_dlp/extractor/showroomlive.py b/yt_dlp/extractor/showroomlive.py
index efd9d561f..1aada69ac 100644
--- a/yt_dlp/extractor/showroomlive.py
+++ b/yt_dlp/extractor/showroomlive.py
@@ -73,7 +73,7 @@ def _real_extract(self, url):
 
         return {
             'id': compat_str(room.get('live_id') or broadcaster_id),
-            'title': self._live_title(title),
+            'title': title,
             'description': room.get('description'),
             'timestamp': int_or_none(room.get('current_live_started_at')),
             'uploader': uploader,
diff --git a/yt_dlp/extractor/skyit.py b/yt_dlp/extractor/skyit.py
index 14a4d8d4c..496bb42a2 100644
--- a/yt_dlp/extractor/skyit.py
+++ b/yt_dlp/extractor/skyit.py
@@ -55,7 +55,7 @@ def _parse_video(self, video, video_id):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'formats': formats,
             'thumbnail': dict_get(video, ('video_still', 'video_still_medium', 'thumb')),
             'description': video.get('short_desc') or None,
diff --git a/yt_dlp/extractor/skylinewebcams.py b/yt_dlp/extractor/skylinewebcams.py
index b7f8ac736..47bbb7632 100644
--- a/yt_dlp/extractor/skylinewebcams.py
+++ b/yt_dlp/extractor/skylinewebcams.py
@@ -36,7 +36,7 @@ def _real_extract(self, url):
             'id': video_id,
             'url': stream_url,
             'ext': 'mp4',
-            'title': self._live_title(title),
+            'title': title,
             'description': description,
             'is_live': True,
         }
diff --git a/yt_dlp/extractor/stripchat.py b/yt_dlp/extractor/stripchat.py
index efd0afc75..0d4a0ce4c 100644
--- a/yt_dlp/extractor/stripchat.py
+++ b/yt_dlp/extractor/stripchat.py
@@ -57,7 +57,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(video_id),
+            'title': video_id,
             'description': self._og_search_description(webpage),
             'is_live': True,
             'formats': formats,
diff --git a/yt_dlp/extractor/svt.py b/yt_dlp/extractor/svt.py
index 489f197fe..6ad01a912 100644
--- a/yt_dlp/extractor/svt.py
+++ b/yt_dlp/extractor/svt.py
@@ -203,10 +203,6 @@ class SVTPlayIE(SVTPlayBaseIE):
         'only_matching': True,
     }]
 
-    def _adjust_title(self, info):
-        if info['is_live']:
-            info['title'] = self._live_title(info['title'])
-
     def _extract_by_video_id(self, video_id, webpage=None):
         data = self._download_json(
             'https://api.svt.se/videoplayer-api/video/%s' % video_id,
@@ -220,7 +216,6 @@ def _extract_by_video_id(self, video_id, webpage=None):
             if not title:
                 title = video_id
             info_dict['title'] = title
-        self._adjust_title(info_dict)
         return info_dict
 
     def _real_extract(self, url):
@@ -251,7 +246,6 @@ def _real_extract(self, url):
                     'title': data['context']['dispatcher']['stores']['MetaStore']['title'],
                     'thumbnail': thumbnail,
                 })
-                self._adjust_title(info_dict)
                 return info_dict
 
             svt_id = try_get(
diff --git a/yt_dlp/extractor/telebruxelles.py b/yt_dlp/extractor/telebruxelles.py
index a0353fe3a..9e8c89bd6 100644
--- a/yt_dlp/extractor/telebruxelles.py
+++ b/yt_dlp/extractor/telebruxelles.py
@@ -69,7 +69,7 @@ def _real_extract(self, url):
         return {
             'id': article_id or display_id,
             'display_id': display_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': description,
             'formats': formats,
             'is_live': is_live,
diff --git a/yt_dlp/extractor/threeqsdn.py b/yt_dlp/extractor/threeqsdn.py
index e5c6a6de1..22b4fe7c8 100644
--- a/yt_dlp/extractor/threeqsdn.py
+++ b/yt_dlp/extractor/threeqsdn.py
@@ -148,7 +148,7 @@ def _real_extract(self, url):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if live else title,
+            'title': title,
             'thumbnail': config.get('poster') or None,
             'description': config.get('description') or None,
             'timestamp': parse_iso8601(config.get('upload_date')),
diff --git a/yt_dlp/extractor/tiktok.py b/yt_dlp/extractor/tiktok.py
index a3079151a..2cd7ba02e 100644
--- a/yt_dlp/extractor/tiktok.py
+++ b/yt_dlp/extractor/tiktok.py
@@ -180,7 +180,7 @@ def extract_addr(addr, add_meta={}):
         user_url = self._UPLOADER_URL_FORMAT % (traverse_obj(author_info,
                                                              'sec_uid', 'id', 'uid', 'unique_id',
                                                              expected_type=str_or_none, get_all=False))
-        labels = traverse_obj(aweme_detail, ('hybrid_label', ..., 'text'), expected_type=str)
+        labels = traverse_obj(aweme_detail, ('hybrid_label', ..., 'text'), expected_type=str, default=[])
 
         contained_music_track = traverse_obj(
             music_info, ('matched_song', 'title'), ('matched_pgc_sound', 'title'), expected_type=str)
diff --git a/yt_dlp/extractor/trovo.py b/yt_dlp/extractor/trovo.py
index 127a5d2dc..580cb533b 100644
--- a/yt_dlp/extractor/trovo.py
+++ b/yt_dlp/extractor/trovo.py
@@ -62,7 +62,7 @@ def _real_extract(self, url):
             raise ExtractorError('%s is offline' % username, expected=True)
         program_info = live_info['programInfo']
         program_id = program_info['id']
-        title = self._live_title(program_info['title'])
+        title = program_info['title']
 
         formats = []
         for stream_info in (program_info.get('streamInfo') or []):
diff --git a/yt_dlp/extractor/tunein.py b/yt_dlp/extractor/tunein.py
index c7a5f5a63..7e51de89e 100644
--- a/yt_dlp/extractor/tunein.py
+++ b/yt_dlp/extractor/tunein.py
@@ -62,7 +62,7 @@ def _real_extract(self, url):
 
         return {
             'id': content_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'formats': formats,
             'thumbnail': thumbnail,
             'location': location,
diff --git a/yt_dlp/extractor/turner.py b/yt_dlp/extractor/turner.py
index 32125bc79..519dc323c 100644
--- a/yt_dlp/extractor/turner.py
+++ b/yt_dlp/extractor/turner.py
@@ -205,7 +205,7 @@ def _extract_cvp_info(self, data_src, video_id, path_data={}, ap_data={}, fatal=
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'formats': formats,
             'subtitles': subtitles,
             'thumbnails': thumbnails,
diff --git a/yt_dlp/extractor/tv2.py b/yt_dlp/extractor/tv2.py
index da351eeb0..b48dfe389 100644
--- a/yt_dlp/extractor/tv2.py
+++ b/yt_dlp/extractor/tv2.py
@@ -106,7 +106,7 @@ def _real_extract(self, url):
         return {
             'id': video_id,
             'url': video_url,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': strip_or_none(asset.get('description')),
             'thumbnails': thumbnails,
             'timestamp': parse_iso8601(asset.get('live_broadcast_time') or asset.get('update_time')),
@@ -271,7 +271,7 @@ def _real_extract(self, url):
         return {
             'id': video_id,
             'url': video_url,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'description': strip_or_none(asset.get('description')),
             'thumbnails': thumbnails,
             'timestamp': parse_iso8601(asset.get('createTime')),
diff --git a/yt_dlp/extractor/tvnet.py b/yt_dlp/extractor/tvnet.py
index 4222ff9ee..4fe8dfb6c 100644
--- a/yt_dlp/extractor/tvnet.py
+++ b/yt_dlp/extractor/tvnet.py
@@ -130,9 +130,6 @@ def _real_extract(self, url):
                 r'data-image=(["\'])(?P<url>(?:https?:)?//.+?)\1', webpage,
                 'thumbnail', default=None, group='url'))
 
-        if is_live:
-            title = self._live_title(title)
-
         view_count = int_or_none(self._search_regex(
             r'(?s)<div[^>]+\bclass=["\'].*?view-count[^>]+>.*?(\d+).*?</div>',
             webpage, 'view count', default=None))
diff --git a/yt_dlp/extractor/tvplayer.py b/yt_dlp/extractor/tvplayer.py
index 8f8686a65..5970596b2 100644
--- a/yt_dlp/extractor/tvplayer.py
+++ b/yt_dlp/extractor/tvplayer.py
@@ -80,7 +80,7 @@ def _real_extract(self, url):
         return {
             'id': resource_id,
             'display_id': display_id,
-            'title': self._live_title(title),
+            'title': title,
             'formats': formats,
             'is_live': True,
         }
diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py
index cd97f0a24..54e500edd 100644
--- a/yt_dlp/extractor/twitch.py
+++ b/yt_dlp/extractor/twitch.py
@@ -903,7 +903,7 @@ def _real_extract(self, url):
         return {
             'id': stream_id,
             'display_id': channel_name,
-            'title': self._live_title(title),
+            'title': title,
             'description': description,
             'thumbnail': thumbnail,
             'uploader': uploader,
diff --git a/yt_dlp/extractor/vgtv.py b/yt_dlp/extractor/vgtv.py
index b6131ff82..10083cd24 100644
--- a/yt_dlp/extractor/vgtv.py
+++ b/yt_dlp/extractor/vgtv.py
@@ -242,7 +242,7 @@ def _real_extract(self, url):
 
         info.update({
             'id': video_id,
-            'title': self._live_title(data['title']) if is_live else data['title'],
+            'title': data['title'],
             'description': data['description'],
             'thumbnail': data['images']['main'] + '?t[]=900x506q80',
             'timestamp': data['published'],
diff --git a/yt_dlp/extractor/vimeo.py b/yt_dlp/extractor/vimeo.py
index 8c3b6af65..57391d766 100644
--- a/yt_dlp/extractor/vimeo.py
+++ b/yt_dlp/extractor/vimeo.py
@@ -224,7 +224,7 @@ def _parse_config(self, config, video_id):
 
         return {
             'id': str_or_none(video_data.get('id')) or video_id,
-            'title': self._live_title(video_title) if is_live else video_title,
+            'title': video_title,
             'uploader': owner.get('name'),
             'uploader_id': video_uploader_url.split('/')[-1] if video_uploader_url else None,
             'uploader_url': video_uploader_url,
diff --git a/yt_dlp/extractor/vk.py b/yt_dlp/extractor/vk.py
index 9a5c9ee6b..5cdb1542d 100644
--- a/yt_dlp/extractor/vk.py
+++ b/yt_dlp/extractor/vk.py
@@ -434,8 +434,6 @@ def _real_extract(self, url):
         # 2 = live
         # 3 = post live (finished live)
         is_live = data.get('live') == 2
-        if is_live:
-            title = self._live_title(title)
 
         timestamp = unified_timestamp(self._html_search_regex(
             r'class=["\']mv_info_date[^>]+>([^<]+)(?:<|from)', info_page,
diff --git a/yt_dlp/extractor/vlive.py b/yt_dlp/extractor/vlive.py
index 8fccf1b63..547bdd323 100644
--- a/yt_dlp/extractor/vlive.py
+++ b/yt_dlp/extractor/vlive.py
@@ -210,7 +210,7 @@ def get_common_fields():
                 self._sort_formats(formats)
                 info = get_common_fields()
                 info.update({
-                    'title': self._live_title(video['title']),
+                    'title': video['title'],
                     'id': video_id,
                     'formats': formats,
                     'is_live': True,
diff --git a/yt_dlp/extractor/vyborymos.py b/yt_dlp/extractor/vyborymos.py
index 9e703c4b6..4d93666c5 100644
--- a/yt_dlp/extractor/vyborymos.py
+++ b/yt_dlp/extractor/vyborymos.py
@@ -44,11 +44,11 @@ def _real_extract(self, url):
         info = self._download_json(
             'http://vybory.mos.ru/json/voting_stations/%s/%s.json'
             % (compat_str(station_id)[:3], station_id),
-            station_id, 'Downloading station JSON', fatal=False)
+            station_id, 'Downloading station JSON', fatal=False) or {}
 
         return {
             'id': station_id,
-            'title': self._live_title(info['name'] if info else station_id),
+            'title': info.get('name') or station_id,
             'description': info.get('address'),
             'is_live': True,
             'formats': formats,
diff --git a/yt_dlp/extractor/wdr.py b/yt_dlp/extractor/wdr.py
index d3229d8af..45bfe5f3a 100644
--- a/yt_dlp/extractor/wdr.py
+++ b/yt_dlp/extractor/wdr.py
@@ -117,7 +117,7 @@ def _real_extract(self, url):
 
         return {
             'id': tracker_data.get('trackerClipId', video_id),
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'alt_title': tracker_data.get('trackerClipSubcategory'),
             'formats': formats,
             'subtitles': subtitles,
diff --git a/yt_dlp/extractor/yahoo.py b/yt_dlp/extractor/yahoo.py
index 53556de00..313e596f5 100644
--- a/yt_dlp/extractor/yahoo.py
+++ b/yt_dlp/extractor/yahoo.py
@@ -264,7 +264,7 @@ def _extract_yahoo_video(self, video_id, country):
 
         return {
             'id': video_id,
-            'title': self._live_title(title) if is_live else title,
+            'title': title,
             'formats': formats,
             'thumbnails': thumbnails,
             'description': clean_html(video.get('description')),
diff --git a/yt_dlp/extractor/younow.py b/yt_dlp/extractor/younow.py
index 04dbc87fc..128faa30d 100644
--- a/yt_dlp/extractor/younow.py
+++ b/yt_dlp/extractor/younow.py
@@ -58,7 +58,7 @@ def _real_extract(self, url):
         return {
             'id': uploader,
             'is_live': True,
-            'title': self._live_title(uploader),
+            'title': uploader,
             'thumbnail': data.get('awsUrl'),
             'tags': data.get('tags'),
             'categories': data.get('tags'),
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 1e04a3a50..8f64b6657 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -2,7 +2,6 @@
 
 from __future__ import unicode_literals
 
-import base64
 import calendar
 import copy
 import datetime
@@ -13,6 +12,7 @@
 import os.path
 import random
 import re
+import sys
 import time
 import traceback
 
@@ -30,7 +30,6 @@
 from ..jsinterp import JSInterpreter
 from ..utils import (
     bug_reports_message,
-    bytes_to_intlist,
     clean_html,
     datetime_from_str,
     dict_get,
@@ -39,7 +38,6 @@
     float_or_none,
     format_field,
     int_or_none,
-    intlist_to_bytes,
     is_html,
     join_nonempty,
     mimetype2ext,
@@ -2117,20 +2115,21 @@ def _extract_comment(self, comment_renderer, parent=None):
             'parent': parent or 'root'
         }
 
-    def _comment_entries(self, root_continuation_data, ytcfg, video_id, parent=None, comment_counts=None):
+    def _comment_entries(self, root_continuation_data, ytcfg, video_id, parent=None, tracker=None):
+
+        get_single_config_arg = lambda c: self._configuration_arg(c, [''])[0]
 
         def extract_header(contents):
             _continuation = None
             for content in contents:
-                comments_header_renderer = try_get(content, lambda x: x['commentsHeaderRenderer'])
+                comments_header_renderer = traverse_obj(content, 'commentsHeaderRenderer')
                 expected_comment_count = parse_count(self._get_text(
                     comments_header_renderer, 'countText', 'commentsCount', max_runs=1))
 
                 if expected_comment_count:
-                    comment_counts[1] = expected_comment_count
-                    self.to_screen('Downloading ~%d comments' % expected_comment_count)
-                sort_mode_str = self._configuration_arg('comment_sort', [''])[0]
-                comment_sort_index = int(sort_mode_str != 'top')  # 1 = new, 0 = top
+                    tracker['est_total'] = expected_comment_count
+                    self.to_screen(f'Downloading ~{expected_comment_count} comments')
+                comment_sort_index = int(get_single_config_arg('comment_sort') != 'top')  # 1 = new, 0 = top
 
                 sort_menu_item = try_get(
                     comments_header_renderer,
@@ -2141,76 +2140,84 @@ def extract_header(contents):
                 if not _continuation:
                     continue
 
-                sort_text = sort_menu_item.get('title')
-                if isinstance(sort_text, compat_str):
-                    sort_text = sort_text.lower()
-                else:
+                sort_text = str_or_none(sort_menu_item.get('title'))
+                if not sort_text:
                     sort_text = 'top comments' if comment_sort_index == 0 else 'newest first'
-                self.to_screen('Sorting comments by %s' % sort_text)
+                self.to_screen('Sorting comments by %s' % sort_text.lower())
                 break
             return _continuation
 
         def extract_thread(contents):
             if not parent:
-                comment_counts[2] = 0
+                tracker['current_page_thread'] = 0
             for content in contents:
+                if not parent and tracker['total_parent_comments'] >= max_parents:
+                    yield
                 comment_thread_renderer = try_get(content, lambda x: x['commentThreadRenderer'])
-                comment_renderer = try_get(
-                    comment_thread_renderer, (lambda x: x['comment']['commentRenderer'], dict)) or try_get(
-                    content, (lambda x: x['commentRenderer'], dict))
+                comment_renderer = get_first(
+                    (comment_thread_renderer, content), [['commentRenderer', ('comment', 'commentRenderer')]],
+                    expected_type=dict, default={})
 
-                if not comment_renderer:
-                    continue
                 comment = self._extract_comment(comment_renderer, parent)
                 if not comment:
                     continue
-                comment_counts[0] += 1
+
+                tracker['running_total'] += 1
+                tracker['total_reply_comments' if parent else 'total_parent_comments'] += 1
                 yield comment
+
                 # Attempt to get the replies
                 comment_replies_renderer = try_get(
                     comment_thread_renderer, lambda x: x['replies']['commentRepliesRenderer'], dict)
 
                 if comment_replies_renderer:
-                    comment_counts[2] += 1
+                    tracker['current_page_thread'] += 1
                     comment_entries_iter = self._comment_entries(
                         comment_replies_renderer, ytcfg, video_id,
-                        parent=comment.get('id'), comment_counts=comment_counts)
-
-                    for reply_comment in comment_entries_iter:
+                        parent=comment.get('id'), tracker=tracker)
+                    for reply_comment in itertools.islice(comment_entries_iter, min(max_replies_per_thread, max(0, max_replies - tracker['total_reply_comments']))):
                         yield reply_comment
 
+        # Keeps track of counts across recursive calls
+        if not tracker:
+            tracker = dict(
+                running_total=0,
+                est_total=0,
+                current_page_thread=0,
+                total_parent_comments=0,
+                total_reply_comments=0)
+
+        # TODO: Deprecated
         # YouTube comments have a max depth of 2
-        max_depth = int_or_none(self._configuration_arg('max_comment_depth', [''])[0]) or float('inf')
+        max_depth = int_or_none(get_single_config_arg('max_comment_depth'))
+        if max_depth:
+            self._downloader.deprecation_warning(
+                '[youtube] max_comment_depth extractor argument is deprecated. Set max replies in the max-comments extractor argument instead.')
         if max_depth == 1 and parent:
             return
-        if not comment_counts:
-            # comment so far, est. total comments, current comment thread #
-            comment_counts = [0, 0, 0]
 
-        continuation = self._extract_continuation(root_continuation_data)
-        if continuation and len(continuation['continuation']) < 27:
-            self.write_debug('Detected old API continuation token. Generating new API compatible token.')
-            continuation_token = self._generate_comment_continuation(video_id)
-            continuation = self._build_api_continuation_query(continuation_token, None)
+        max_comments, max_parents, max_replies, max_replies_per_thread, *_ = map(
+            lambda p: int_or_none(p, default=sys.maxsize), self._configuration_arg('max_comments', ) + [''] * 4)
 
+        continuation = self._extract_continuation(root_continuation_data)
         message = self._get_text(root_continuation_data, ('contents', ..., 'messageRenderer', 'text'), max_runs=1)
         if message and not parent:
             self.report_warning(message, video_id=video_id)
 
-        visitor_data = None
+        response = None
         is_first_continuation = parent is None
 
         for page_num in itertools.count(0):
             if not continuation:
                 break
-            headers = self.generate_api_headers(ytcfg=ytcfg, visitor_data=visitor_data)
-            comment_prog_str = '(%d/%d)' % (comment_counts[0], comment_counts[1])
+            headers = self.generate_api_headers(ytcfg=ytcfg, visitor_data=self._extract_visitor_data(response))
+            comment_prog_str = f"({tracker['running_total']}/{tracker['est_total']})"
             if page_num == 0:
                 if is_first_continuation:
                     note_prefix = 'Downloading comment section API JSON'
                 else:
                     note_prefix = '    Downloading comment API JSON reply thread %d %s' % (
-                        comment_counts[2], comment_prog_str)
+                        tracker['current_page_thread'], comment_prog_str)
             else:
                 note_prefix = '%sDownloading comment%s API JSON page %d %s' % (
                     '       ' if parent else '', ' replies' if parent else '',
@@ -2219,83 +2226,32 @@ def extract_thread(contents):
             response = self._extract_response(
                 item_id=None, query=continuation,
                 ep='next', ytcfg=ytcfg, headers=headers, note=note_prefix,
-                check_get_keys=('onResponseReceivedEndpoints', 'continuationContents'))
-            if not response:
-                break
-            visitor_data = try_get(
-                response,
-                lambda x: x['responseContext']['webResponseContextExtensionData']['ytConfigData']['visitorData'],
-                compat_str) or visitor_data
+                check_get_keys='onResponseReceivedEndpoints')
 
-            continuation_contents = dict_get(response, ('onResponseReceivedEndpoints', 'continuationContents'))
+            continuation_contents = traverse_obj(
+                response, 'onResponseReceivedEndpoints', expected_type=list, default=[])
 
             continuation = None
-            if isinstance(continuation_contents, list):
-                for continuation_section in continuation_contents:
-                    if not isinstance(continuation_section, dict):
-                        continue
-                    continuation_items = try_get(
-                        continuation_section,
-                        (lambda x: x['reloadContinuationItemsCommand']['continuationItems'],
-                         lambda x: x['appendContinuationItemsAction']['continuationItems']),
-                        list) or []
-                    if is_first_continuation:
-                        continuation = extract_header(continuation_items)
-                        is_first_continuation = False
-                        if continuation:
-                            break
-                        continue
-                    count = 0
-                    for count, entry in enumerate(extract_thread(continuation_items)):
-                        yield entry
-                    continuation = self._extract_continuation({'contents': continuation_items})
+            for continuation_section in continuation_contents:
+                continuation_items = traverse_obj(
+                    continuation_section,
+                    (('reloadContinuationItemsCommand', 'appendContinuationItemsAction'), 'continuationItems'),
+                    get_all=False, expected_type=list) or []
+                if is_first_continuation:
+                    continuation = extract_header(continuation_items)
+                    is_first_continuation = False
                     if continuation:
-                        # Sometimes YouTube provides a continuation without any comments
-                        # In most cases we end up just downloading these with very little comments to come.
-                        if count == 0:
-                            if not parent:
-                                self.report_warning('No comments received - assuming end of comments')
-                            continuation = None
                         break
+                    continue
 
-            # Deprecated response structure
-            elif isinstance(continuation_contents, dict):
-                known_continuation_renderers = ('itemSectionContinuation', 'commentRepliesContinuation')
-                for key, continuation_renderer in continuation_contents.items():
-                    if key not in known_continuation_renderers:
-                        continue
-                    if not isinstance(continuation_renderer, dict):
-                        continue
-                    if is_first_continuation:
-                        header_continuation_items = [continuation_renderer.get('header') or {}]
-                        continuation = extract_header(header_continuation_items)
-                        is_first_continuation = False
-                        if continuation:
-                            break
-
-                    # Sometimes YouTube provides a continuation without any comments
-                    # In most cases we end up just downloading these with very little comments to come.
-                    count = 0
-                    for count, entry in enumerate(extract_thread(continuation_renderer.get('contents') or {})):
-                        yield entry
-                    continuation = self._extract_continuation(continuation_renderer)
-                    if count == 0:
-                        if not parent:
-                            self.report_warning('No comments received - assuming end of comments')
-                        continuation = None
+                for entry in extract_thread(continuation_items):
+                    if not entry:
+                        return
+                    yield entry
+                continuation = self._extract_continuation({'contents': continuation_items})
+                if continuation:
                     break
 
-    @staticmethod
-    def _generate_comment_continuation(video_id):
-        """
-        Generates initial comment section continuation token from given video id
-        """
-        b64_vid_id = base64.b64encode(bytes(video_id.encode('utf-8')))
-        parts = ('Eg0SCw==', b64_vid_id, 'GAYyJyIRIgs=', b64_vid_id, 'MAB4AjAAQhBjb21tZW50cy1zZWN0aW9u')
-        new_continuation_intlist = list(itertools.chain.from_iterable(
-            [bytes_to_intlist(base64.b64decode(part)) for part in parts]))
-        return base64.b64encode(intlist_to_bytes(new_continuation_intlist)).decode('utf-8')
-
     def _get_comments(self, ytcfg, video_id, contents, webpage):
         """Entry for comment extraction"""
         def _real_comment_extract(contents):
@@ -2854,7 +2810,7 @@ def feed_entry(name):
 
         info = {
             'id': video_id,
-            'title': self._live_title(video_title) if is_live else video_title,
+            'title': video_title,
             'formats': formats,
             'thumbnails': thumbnails,
             # The best thumbnail that we are sure exists. Prevents unnecessary
diff --git a/yt_dlp/extractor/zattoo.py b/yt_dlp/extractor/zattoo.py
index 98d15604d..9435920b2 100644
--- a/yt_dlp/extractor/zattoo.py
+++ b/yt_dlp/extractor/zattoo.py
@@ -187,7 +187,7 @@ def _extract_video(self, channel_name, video_id, record_id=None, is_live=False):
             cid = self._extract_cid(video_id, channel_name)
             info_dict = {
                 'id': channel_name,
-                'title': self._live_title(channel_name),
+                'title': channel_name,
                 'is_live': True,
             }
         else:


Test patch:
diff --git a/test/helper.py b/test/helper.py
index aef78c79d..9fb4f2120 100644
--- a/test/helper.py
+++ b/test/helper.py
@@ -223,7 +223,7 @@ def expect_info_dict(self, got_dict, expected_dict):
 
         # Auto-generated
         'playlist', 'format_index', 'webpage_url', 'video_ext', 'audio_ext', 'duration_string', 'epoch', 'fulltitle',
-        'extractor', 'extractor_key', 'original_url', 'webpage_url_basename', 'filepath', 'infojson_filename',
+        'extractor', 'extractor_key', 'original_url', 'webpage_url_basename', 'webpage_url_domain', 'filepath', 'infojson_filename',
 
         # Only live_status needs to be checked
         'is_live', 'was_live',
diff --git a/test/test_InfoExtractor.py b/test/test_InfoExtractor.py
index cbca22c91..cf06dbde4 100644
--- a/test/test_InfoExtractor.py
+++ b/test/test_InfoExtractor.py
@@ -99,10 +99,10 @@ def test_html_search_meta(self):
         self.assertRaises(RegexNotFoundError, ie._html_search_meta, ('z', 'x'), html, None, fatal=True)
 
     def test_search_json_ld_realworld(self):
-        # https://github.com/ytdl-org/youtube-dl/issues/23306
-        expect_dict(
-            self,
-            self.ie._search_json_ld(r'''<script type="application/ld+json">
+        _TESTS = [
+            # https://github.com/ytdl-org/youtube-dl/issues/23306
+            (
+                r'''<script type="application/ld+json">
 {
 "@context": "http://schema.org/",
 "@type": "VideoObject",
@@ -135,17 +135,86 @@ def test_search_json_ld_realworld(self):
 "name": "Kleio Valentien",
 "url": "https://www.eporner.com/pornstar/kleio-valentien/"
 }]}
-</script>''', None),
-            {
-                'title': '1 On 1 With Kleio',
-                'description': 'Kleio Valentien',
-                'url': 'https://gvideo.eporner.com/xN49A1cT3eB/xN49A1cT3eB.mp4',
-                'timestamp': 1449347075,
-                'duration': 743.0,
-                'view_count': 1120958,
-                'width': 1920,
-                'height': 1080,
-            })
+                </script>''',
+                {
+                    'title': '1 On 1 With Kleio',
+                    'description': 'Kleio Valentien',
+                    'url': 'https://gvideo.eporner.com/xN49A1cT3eB/xN49A1cT3eB.mp4',
+                    'timestamp': 1449347075,
+                    'duration': 743.0,
+                    'view_count': 1120958,
+                    'width': 1920,
+                    'height': 1080,
+                },
+                {},
+            ),
+            (
+                r'''<script type="application/ld+json">
+      {
+      "@context": "https://schema.org",
+      "@graph": [
+      {
+      "@type": "NewsArticle",
+      "mainEntityOfPage": {
+      "@type": "WebPage",
+      "@id": "https://www.ant1news.gr/Society/article/620286/symmoria-anilikon-dikigoros-thymaton-ithelan-na-toys-apoteleiosoyn"
+      },
+      "headline": "Συμμορία ανηλίκων – δικηγόρος θυμάτων: ήθελαν να τους αποτελειώσουν",
+      "name": "Συμμορία ανηλίκων – δικηγόρος θυμάτων: ήθελαν να τους αποτελειώσουν",
+      "description": "Τα παιδιά δέχθηκαν την επίθεση επειδή αρνήθηκαν να γίνουν μέλη της συμμορίας, ανέφερε ο Γ. Ζαχαρόπουλος.",
+      "image": {
+      "@type": "ImageObject",
+      "url": "https://ant1media.azureedge.net/imgHandler/1100/a635c968-be71-447c-bf9c-80d843ece21e.jpg",
+      "width": 1100,
+      "height": 756            },
+      "datePublished": "2021-11-10T08:50:00+03:00",
+      "dateModified": "2021-11-10T08:52:53+03:00",
+      "author": {
+      "@type": "Person",
+      "@id": "https://www.ant1news.gr/",
+      "name": "Ant1news",
+      "image": "https://www.ant1news.gr/images/logo-e5d7e4b3e714c88e8d2eca96130142f6.png",
+      "url": "https://www.ant1news.gr/"
+      },
+      "publisher": {
+      "@type": "Organization",
+      "@id": "https://www.ant1news.gr#publisher",
+      "name": "Ant1news",
+      "url": "https://www.ant1news.gr",
+      "logo": {
+      "@type": "ImageObject",
+      "url": "https://www.ant1news.gr/images/logo-e5d7e4b3e714c88e8d2eca96130142f6.png",
+      "width": 400,
+      "height": 400                },
+      "sameAs": [
+      "https://www.facebook.com/Ant1news.gr",
+      "https://twitter.com/antennanews",
+      "https://www.youtube.com/channel/UC0smvAbfczoN75dP0Hw4Pzw",
+      "https://www.instagram.com/ant1news/"
+      ]
+      },
+
+      "keywords": "μαχαίρωμα,συμμορία ανηλίκων,ΕΙΔΗΣΕΙΣ,ΕΙΔΗΣΕΙΣ ΣΗΜΕΡΑ,ΝΕΑ,Κοινωνία - Ant1news",
+
+
+      "articleSection": "Κοινωνία"
+      }
+      ]
+      }
+                </script>''',
+                {
+                    'timestamp': 1636523400,
+                    'title': 'md5:91fe569e952e4d146485740ae927662b',
+                },
+                {'expected_type': 'NewsArticle'},
+            ),
+        ]
+        for html, expected_dict, search_json_ld_kwargs in _TESTS:
+            expect_dict(
+                self,
+                self.ie._search_json_ld(html, None, **search_json_ld_kwargs),
+                expected_dict
+            )
 
     def test_download_json(self):
         uri = encode_data_uri(b'{"foo": "blah"}', 'application/json')