Repository: yt-dlp/yt-dlp

Issue:
`InfoExtractor._json_ld` fails under certain conditions
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting a bug unrelated to a specific site
- [X] I've verified that I'm running yt-dlp version **2022.10.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#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)

### Provide a description that is worded well enough to be understood

`InfoExtractor._json_ld` fails under certain conditions.
Inside `traverse_json_ld` `e` is assumed a dict eventhough it does not always have to be.

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', 'https://www.autoweek.nl/autotests/artikel/porsche-911-gt3-rs-rij-impressie-2/']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.10.04 [4e0511f27] (source)
[debug] Lazy loading extractors is disabled
[debug] Plugins: ['SamplePluginIE', 'SamplePluginPP']
[debug] Python 3.10.5 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg n5.1-1-g6d83c5f876-20220727 (setts), ffprobe n5.1-1-g6d83c5f876-20220727
[debug] Optional libraries: Cryptodome-3.14.1, brotli-1.0.9, certifi-2022.09.14, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Loaded 1691 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.10.04, Current version: 2022.10.04
yt-dlp is up to date (2022.10.04)
[debug] [generic] Extracting URL: https://www.autoweek.nl/autotests/artikel/porsche-911-gt3-rs-rij-impressie-2/
[generic] porsche-911-gt3-rs-rij-impressie-2: Downloading webpage
[redirect] Following redirect to https://myprivacy.dpgmedia.nl/consent?siteKey=ujm6mv0jrqiz5syr&callbackUrl=https%3A%2F%2Fwww.autoweek.nl%2Fprivacygate-confirm%3FredirectUri%3D%252Fautotests%252Fartikel%252Fporsche-911-gt3-rs-rij-impressie-2%252F
[debug] [generic] Extracting URL: https://myprivacy.dpgmedia.nl/consent?siteKey=ujm6mv0jrqiz5syr&callbackUrl=https%3A%2F%2Fwww.autoweek.nl%2Fprivacygate-confirm%3FredirectUri%3D%252Fautotests%252Fartikel%252Fporsche-911-gt3-rs-rij-impressie-2%252F
[generic] consent?siteKey=ujm6mv0jrqiz5syr&callbackUrl=https://www.autoweek: Downloading webpage
[redirect] Following redirect to https://www.autoweek.nl/autotests/artikel/porsche-911-gt3-rs-rij-impressie-2/
[debug] [generic] Extracting URL: https://www.autoweek.nl/autotests/artikel/porsche-911-gt3-rs-rij-impressie-2/
[generic] porsche-911-gt3-rs-rij-impressie-2: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] porsche-911-gt3-rs-rij-impressie-2: Extracting information
[debug] Looking for Brightcove embeds
[debug] Looking for embeds
ERROR: 'list' object has no attribute 'get'
Traceback (most recent call last):
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\YoutubeDL.py", line 1477, in wrapper
    return func(self, *args, **kwargs)
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\YoutubeDL.py", line 1553, in __extract_info
    ie_result = ie.extract(url)
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\extractor\common.py", line 672, in extract
    ie_result = self._real_extract(url)
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\extractor\generic.py", line 2897, in _real_extract
    json_ld = self._search_json_ld(webpage, video_id, default={})
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\extractor\common.py", line 1450, in _search_json_ld
    info = self._json_ld(
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\extractor\common.py", line 1631, in _json_ld
    traverse_json_ld(json_ld)
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\extractor\common.py", line 1576, in traverse_json_ld
    traverse_json_ld(variadic(e['@graph'], allowed_types=(dict,)), at_top_level=False)
  File "D:\Documents\GitHub\Grub4K\yt-dlp\yt_dlp\extractor\common.py", line 1624, in traverse_json_ld
    video = e.get('video')
AttributeError: 'list' object has no attribute 'get'
```


Discussion:
Grub4K: The `json_ld` provided has a graph looking like this: `[{"@graph": [{...}, [{...}]]}]`.
Currently the nested list inside the `@graph` list doesn't get traversed.
I am not sure if it makes make sense to allow such nesting. I have a patch already written, but on the other hand, traversing deeper might give unrelated things we wouldn't want.
pukkandan: > I am not sure if it makes make sense to allow such nesting

Does it make sense for the example you are looking at? Or, does it end up giving redundant/useless info? We should implement this only if we have actual examples where this is useful
Grub4K: It does not change the result for this case, I just thought it might make sense for potential future cases.

If you think it is better to wait for a specific case then this is good to go.

Patch:
diff --git a/README.md b/README.md
index e0a1ea059..9b59e096a 100644
--- a/README.md
+++ b/README.md
@@ -1226,6 +1226,7 @@ # OUTPUT TEMPLATE
  - `duration` (numeric): Length of the video in seconds
  - `duration_string` (string): Length of the video (HH:mm:ss)
  - `view_count` (numeric): How many users have watched the video on the platform
+ - `concurrent_view_count` (numeric): How many users are currently watching the video on the platform.
  - `like_count` (numeric): Number of positive ratings of the video
  - `dislike_count` (numeric): Number of negative ratings of the video
  - `repost_count` (numeric): Number of reposts of the video
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 44c189f79..75cb3fcab 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -440,6 +440,7 @@
     AnimalPlanetIE,
     TLCIE,
     MotorTrendIE,
+    MotorTrendOnDemandIE,
     DiscoveryPlusIndiaIE,
     DiscoveryNetworksDeIE,
     DiscoveryPlusItalyIE,
@@ -1180,6 +1181,7 @@
 from .noovo import NoovoIE
 from .normalboots import NormalbootsIE
 from .nosvideo import NosVideoIE
+from .nosnl import NOSNLArticleIE
 from .nova import (
     NovaEmbedIE,
     NovaIE,
@@ -1564,6 +1566,7 @@
 from .sapo import SapoIE
 from .savefrom import SaveFromIE
 from .sbs import SBSIE
+from .screen9 import Screen9IE
 from .screencast import ScreencastIE
 from .screencastomatic import ScreencastOMaticIE
 from .scrippsnetworks import (
diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py
index 944b196a1..18a52a855 100644
--- a/yt_dlp/extractor/common.py
+++ b/yt_dlp/extractor/common.py
@@ -284,6 +284,7 @@ class InfoExtractor:
                     captions instead of normal subtitles
     duration:       Length of the video in seconds, as an integer or float.
     view_count:     How many users have watched the video on the platform.
+    concurrent_view_count: How many users are currently watching the video on the platform.
     like_count:     Number of positive ratings of the video
     dislike_count:  Number of negative ratings of the video
     repost_count:   Number of reposts of the video
@@ -1466,10 +1467,6 @@ def _json_ld(self, json_ld, video_id, fatal=True, expected_type=None):
         if not json_ld:
             return {}
         info = {}
-        if not isinstance(json_ld, (list, tuple, dict)):
-            return info
-        if isinstance(json_ld, dict):
-            json_ld = [json_ld]
 
         INTERACTION_TYPE_MAP = {
             'CommentAction': 'comment',
@@ -1569,11 +1566,13 @@ def extract_video_object(e):
             extract_chapter_information(e)
 
         def traverse_json_ld(json_ld, at_top_level=True):
-            for e in json_ld:
+            for e in variadic(json_ld):
+                if not isinstance(e, dict):
+                    continue
                 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)
+                    traverse_json_ld(e['@graph'], at_top_level=False)
                     break
                 if expected_type is not None and not is_type(e, expected_type):
                     continue
@@ -1628,8 +1627,8 @@ def traverse_json_ld(json_ld, at_top_level=True):
                     continue
                 else:
                     break
-        traverse_json_ld(json_ld)
 
+        traverse_json_ld(json_ld)
         return filter_dict(info)
 
     def _search_nextjs_data(self, webpage, video_id, *, transform_source=None, fatal=True, **kw):
diff --git a/yt_dlp/extractor/dplay.py b/yt_dlp/extractor/dplay.py
index e7629a5e1..3f0b315a5 100644
--- a/yt_dlp/extractor/dplay.py
+++ b/yt_dlp/extractor/dplay.py
@@ -745,6 +745,45 @@ class MotorTrendIE(DiscoveryPlusBaseIE):
     }
 
 
+class MotorTrendOnDemandIE(DiscoveryPlusBaseIE):
+    _VALID_URL = r'https?://(?:www\.)?motortrendondemand\.com/detail' + DPlayBaseIE._PATH_REGEX
+    _TESTS = [{
+        'url': 'https://www.motortrendondemand.com/detail/wheelstanding-dump-truck-stubby-bobs-comeback/37699/784',
+        'info_dict': {
+            'id': '37699',
+            'display_id': 'wheelstanding-dump-truck-stubby-bobs-comeback/37699',
+            'ext': 'mp4',
+            'title': 'Wheelstanding Dump Truck! Stubby Bob’s Comeback',
+            'description': 'md5:996915abe52a1c3dfc83aecea3cce8e7',
+            'season_number': 5,
+            'episode_number': 52,
+            'episode': 'Episode 52',
+            'season': 'Season 5',
+            'thumbnail': r're:^https?://.+\.jpe?g$',
+            'timestamp': 1388534401,
+            'duration': 1887.345,
+            'creator': 'Originals',
+            'series': 'Roadkill',
+            'upload_date': '20140101',
+            'tags': [],
+        },
+    }]
+
+    _PRODUCT = 'MTOD'
+    _DISCO_API_PARAMS = {
+        'disco_host': 'us1-prod-direct.motortrendondemand.com',
+        'realm': 'motortrend',
+        'country': 'us',
+    }
+
+    def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
+        headers.update({
+            'x-disco-params': f'realm={realm}',
+            'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:4.39.1-gi1',
+            'Authorization': self._get_auth(disco_base, display_id, realm),
+        })
+
+
 class DiscoveryPlusIE(DiscoveryPlusBaseIE):
     _VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?!it/)(?:\w{2}/)?video' + DPlayBaseIE._PATH_REGEX
     _TESTS = [{
diff --git a/yt_dlp/extractor/generic.py b/yt_dlp/extractor/generic.py
index 73422f937..92390a387 100644
--- a/yt_dlp/extractor/generic.py
+++ b/yt_dlp/extractor/generic.py
@@ -2463,6 +2463,21 @@ class GenericIE(InfoExtractor):
                 'duration': 111.0,
             }
         },
+        {
+            'note': 'JSON LD with unexpected data type',
+            'url': 'https://www.autoweek.nl/autotests/artikel/porsche-911-gt3-rs-rij-impressie-2/',
+            'info_dict': {
+                'id': 'porsche-911-gt3-rs-rij-impressie-2',
+                'ext': 'mp4',
+                'title': 'Test: Porsche 911 GT3 RS',
+                'description': 'Je ziet het niet, maar het is er wel. Downforce, hebben we het dan over. En in de nieuwe Porsche 911 GT3 RS is er zelfs heel veel downforce.',
+                'timestamp': 1664920902,
+                'upload_date': '20221004',
+                'thumbnail': r're:^https://media.autoweek.nl/m/.+\.jpg$',
+                'age_limit': 0,
+                'direct': True,
+            }
+        }
     ]
 
     def report_following_redirect(self, new_url):
diff --git a/yt_dlp/extractor/nosnl.py b/yt_dlp/extractor/nosnl.py
new file mode 100644
index 000000000..eba94c416
--- /dev/null
+++ b/yt_dlp/extractor/nosnl.py
@@ -0,0 +1,95 @@
+from .common import InfoExtractor
+from ..utils import parse_duration, parse_iso8601, traverse_obj
+
+
+class NOSNLArticleIE(InfoExtractor):
+    _VALID_URL = r'https?://nos\.nl/((?!video)(\w+/)?\w+/)\d+-(?P<display_id>[\w-]+)'
+    _TESTS = [
+        {
+            # only 1 video
+            'url': 'https://nos.nl/nieuwsuur/artikel/2440353-verzakking-door-droogte-dreigt-tot-een-miljoen-kwetsbare-huizen',
+            'info_dict': {
+                'id': '2440340',
+                'ext': 'mp4',
+                'description': 'md5:5f83185d902ac97af3af4bed7ece3db5',
+                'title': '\'We hebben een huis vol met scheuren\'',
+                'duration': 95.0,
+                'thumbnail': 'https://cdn.nos.nl/image/2022/08/12/887149/3840x2160a.jpg',
+            }
+        }, {
+            # more than 1 video
+            'url': 'https://nos.nl/artikel/2440409-vannacht-sliepen-weer-enkele-honderden-asielzoekers-in-ter-apel-buiten',
+            'info_dict': {
+                'id': '2440409',
+                'title': 'Vannacht sliepen weer enkele honderden asielzoekers in Ter Apel buiten',
+                'description': 'Er werd wel geprobeerd om kwetsbare migranten onderdak te bieden, zegt het COA.',
+                'tags': ['aanmeldcentrum', 'Centraal Orgaan opvang asielzoekers', 'COA', 'asielzoekers', 'Ter Apel'],
+                'modified_timestamp': 1660452773,
+                'modified_date': '20220814',
+                'upload_date': '20220813',
+                'thumbnail': 'https://cdn.nos.nl/image/2022/07/18/880346/1024x576a.jpg',
+                'timestamp': 1660401384,
+            },
+            'playlist_count': 2,
+        }, {
+            # audio + video
+            'url': 'https://nos.nl/artikel/2440789-wekdienst-16-8-groningse-acties-tien-jaar-na-zware-aardbeving-femke-bol-in-actie-op-ek-atletiek',
+            'info_dict': {
+                'id': '2440789',
+                'title': 'Wekdienst 16/8: Groningse acties tien jaar na zware aardbeving • Femke Bol in actie op EK atletiek ',
+                'description': 'Nieuws, weer, verkeer: met dit overzicht begin je geïnformeerd aan de dag.',
+                'tags': ['wekdienst'],
+                'modified_date': '20220816',
+                'modified_timestamp': 1660625449,
+                'timestamp': 1660625449,
+                'upload_date': '20220816',
+                'thumbnail': 'https://cdn.nos.nl/image/2022/08/16/888178/1024x576a.jpg',
+            },
+            'playlist_count': 2,
+        }
+    ]
+
+    def _entries(self, nextjs_json, display_id):
+        for item in nextjs_json['items']:
+            if item.get('type') == 'video':
+                formats, subtitle = self._extract_m3u8_formats_and_subtitles(
+                    traverse_obj(item, ('source', 'url')), display_id, ext='mp4')
+                yield {
+                    'id': str(item['id']),
+                    'title': item.get('title'),
+                    'description': item.get('description'),
+                    'formats': formats,
+                    'subtitles': subtitle,
+                    'duration': parse_duration(item.get('duration')),
+                    'thumbnails': [{
+                        'url': traverse_obj(image, ('url', ...), get_all=False),
+                        'width': image.get('width'),
+                        'height': image.get('height')
+                    } for image in traverse_obj(item, ('imagesByRatio', ...))[0]],
+                }
+
+            elif item.get('type') == 'audio':
+                yield {
+                    'id': str(item['id']),
+                    'title': item.get('title'),
+                    'url': traverse_obj(item, ('media', 'src')),
+                    'ext': 'mp3',
+                }
+
+    def _real_extract(self, url):
+        display_id = self._match_valid_url(url).group('display_id')
+        webpage = self._download_webpage(url, display_id)
+
+        nextjs_json = self._search_nextjs_data(webpage, display_id)['props']['pageProps']['data']
+        return {
+            '_type': 'playlist',
+            'entries': self._entries(nextjs_json, display_id),
+            'id': str(nextjs_json['id']),
+            'title': nextjs_json.get('title') or self._html_search_meta(['title', 'og:title', 'twitter:title'], webpage),
+            'description': (nextjs_json.get('description')
+                            or self._html_search_meta(['description', 'twitter:description', 'og:description'], webpage)),
+            'tags': nextjs_json.get('keywords'),
+            'modified_timestamp': parse_iso8601(nextjs_json.get('modifiedAt')),
+            'thumbnail': nextjs_json.get('shareImageSrc') or self._html_search_meta(['og:image', 'twitter:image'], webpage),
+            'timestamp': parse_iso8601(nextjs_json.get('publishedAt'))
+        }
diff --git a/yt_dlp/extractor/reddit.py b/yt_dlp/extractor/reddit.py
index aabc8dba9..c713b24fe 100644
--- a/yt_dlp/extractor/reddit.py
+++ b/yt_dlp/extractor/reddit.py
@@ -36,6 +36,26 @@ class RedditIE(InfoExtractor):
         'params': {
             'skip_download': True,
         },
+    }, {
+        # 1080p fallback format
+        'url': 'https://www.reddit.com/r/aww/comments/90bu6w/heat_index_was_110_degrees_so_we_offered_him_a/',
+        'md5': '8b5902cfda3006bf90faea7adf765a49',
+        'info_dict': {
+            'id': 'gyh95hiqc0b11',
+            'ext': 'mp4',
+            'display_id': '90bu6w',
+            'title': 'Heat index was 110 degrees so we offered him a cold drink. He went for a full body soak instead',
+            'thumbnail': r're:^https?://.*\.(?:jpg|png)',
+            'thumbnails': 'count:7',
+            'timestamp': 1532051078,
+            'upload_date': '20180720',
+            'uploader': 'FootLoosePickleJuice',
+            'duration': 14,
+            'like_count': int,
+            'dislike_count': int,
+            'comment_count': int,
+            'age_limit': 0,
+        },
     }, {
         'url': 'https://www.reddit.com/r/videos/comments/6rrwyj',
         'only_matching': True,
@@ -145,9 +165,18 @@ def add_thumbnail(src):
             dash_playlist_url = playlist_urls[0] or f'https://v.redd.it/{video_id}/DASHPlaylist.mpd'
             hls_playlist_url = playlist_urls[1] or f'https://v.redd.it/{video_id}/HLSPlaylist.m3u8'
 
-            formats = self._extract_m3u8_formats(
-                hls_playlist_url, display_id, 'mp4',
-                entry_protocol='m3u8_native', m3u8_id='hls', fatal=False)
+            formats = [{
+                'url': unescapeHTML(reddit_video['fallback_url']),
+                'height': int_or_none(reddit_video.get('height')),
+                'width': int_or_none(reddit_video.get('width')),
+                'tbr': int_or_none(reddit_video.get('bitrate_kbps')),
+                'acodec': 'none',
+                'ext': 'mp4',
+                'format_id': 'fallback',
+                'format_note': 'DASH video, mp4_dash',
+            }]
+            formats.extend(self._extract_m3u8_formats(
+                hls_playlist_url, display_id, 'mp4', m3u8_id='hls', fatal=False))
             formats.extend(self._extract_mpd_formats(
                 dash_playlist_url, display_id, mpd_id='dash', fatal=False))
             self._sort_formats(formats)
diff --git a/yt_dlp/extractor/screen9.py b/yt_dlp/extractor/screen9.py
new file mode 100644
index 000000000..eae652af7
--- /dev/null
+++ b/yt_dlp/extractor/screen9.py
@@ -0,0 +1,63 @@
+from .common import InfoExtractor
+from ..utils import traverse_obj
+
+
+class Screen9IE(InfoExtractor):
+    _VALID_URL = r'https?://(?:\w+\.screen9\.(?:tv|com)|play\.su\.se)/(?:embed|media)/(?P<id>[^?#/]+)'
+    _TESTS = [
+        {
+            'url': 'https://api.screen9.com/embed/8kTNEjvoXGM33dmWwF0uDA',
+            'md5': 'd60d23f8980583b930724b01fa6ddb41',
+            'info_dict': {
+                'id': '8kTNEjvoXGM33dmWwF0uDA',
+                'ext': 'mp4',
+                'title': 'Östersjön i förändrat klimat',
+                'thumbnail': r're:^https?://.+\.jpg',
+            },
+        },
+        {
+            'url': 'https://folkhogskolekanalen.screen9.tv/media/gy35PKLHe-5K29RYHga2bw/ett-starkare-samhalle-en-snabbguide-om-sveriges-folkhogskolor',
+            'md5': 'c9389806e78573ea34fc48b6f94465dc',
+            'info_dict': {
+                'id': 'gy35PKLHe-5K29RYHga2bw',
+                'ext': 'mp4',
+                'title': 'Ett starkare samhälle - en snabbguide om Sveriges folkhögskolor',
+                'thumbnail': r're:^https?://.+\.jpg',
+            },
+        },
+        {
+            'url': 'https://play.su.se/media/H1YA0EYNCxiesrSU1kaRBQ/baltic-breakfast',
+            'md5': '2b817647c3058002526269deff4c0683',
+            'info_dict': {
+                'id': 'H1YA0EYNCxiesrSU1kaRBQ',
+                'ext': 'mp4',
+                'title': 'Baltic Breakfast',
+                'thumbnail': r're:^https?://.+\.jpg',
+            },
+        },
+    ]
+
+    def _real_extract(self, url):
+        video_id = self._match_id(url)
+        webpage = self._download_webpage(f'https://api.screen9.com/embed/{video_id}', video_id)
+        config = self._search_json(r'var\s+config\s*=', webpage, 'config', video_id)
+        formats, subtitles = self._extract_m3u8_formats_and_subtitles(
+            traverse_obj(config, ('src', lambda _, v: v['type'] == 'application/x-mpegURL', 'src'), get_all=False),
+            video_id, ext='mp4')
+        formats.append({
+            'url': traverse_obj(config, ('src', lambda _, v: v['type'] == 'video/mp4', 'src'), get_all=False),
+            'format': 'mp4',
+        })
+
+        self._sort_formats(formats)
+        return {
+            'id': video_id,
+            'title': traverse_obj(
+                config,
+                ('plugins', (('title', 'title'), ('googleAnalytics', 'title'), ('share', 'mediaTitle'))),
+                get_all=False),
+            'description': traverse_obj(config, ('plugins', 'title', 'description')),
+            'thumbnail': traverse_obj(config, ('poster')),
+            'formats': formats,
+            'subtitles': subtitles,
+        }
diff --git a/yt_dlp/extractor/tnaflix.py b/yt_dlp/extractor/tnaflix.py
index 34361e515..8cbfeb7fb 100644
--- a/yt_dlp/extractor/tnaflix.py
+++ b/yt_dlp/extractor/tnaflix.py
@@ -19,6 +19,7 @@ class TNAFlixNetworkBaseIE(InfoExtractor):
         r'config\s*=\s*(["\'])(?P<url>(?:https?:)?//(?:(?!\1).)+)\1',
     ]
     _HOST = 'tna'
+    _VIDEO_XML_URL = 'https://www.tnaflix.com/cdn/cdn.php?file={}.fid&key={}&VID={}&nomp4=1&catID=0&rollover=1&startThumb=12&embed=0&utm_source=0&multiview=0&premium=1&country=0user=0&vip=1&cd=0&ref=0&alpha'
     _VKEY_SUFFIX = ''
     _TITLE_REGEX = r'<input[^>]+name="title" value="([^"]+)"'
     _DESCRIPTION_REGEX = r'<input[^>]+name="description" value="([^"]+)"'
@@ -71,6 +72,10 @@ def get_child(elem, names):
     def _real_extract(self, url):
         mobj = self._match_valid_url(url)
         video_id = mobj.group('id')
+
+        def extract_field(pattern, name):
+            return self._html_search_regex(pattern, webpage, name, default=None) if pattern else None
+
         for display_id_key in ('display_id', 'display_id_2'):
             if display_id_key in mobj.groupdict():
                 display_id = mobj.group(display_id_key)
@@ -85,6 +90,13 @@ def _real_extract(self, url):
             self._CONFIG_REGEX, webpage, 'flashvars.config', default=None,
             group='url'), 'http:')
 
+        if not cfg_url:
+            vkey = extract_field(r'<input\b[^>]+\bid="vkey"\b[^>]+\bvalue="([^"]+)"', 'vkey')
+            nkey = extract_field(r'<input\b[^>]+\bid="nkey"\b[^>]+\bvalue="([^"]+)"', 'nkey')
+            vid = extract_field(r'<input\b[^>]+\bid="VID"\b[^>]+\bvalue="([^"]+)"', 'vid')
+            if vkey and nkey and vid:
+                cfg_url = self._proto_relative_url(self._VIDEO_XML_URL.format(vkey, nkey, vid), 'http:')
+
         if not cfg_url:
             inputs = self._hidden_inputs(webpage)
             cfg_url = ('https://cdn-fck.%sflix.com/%sflix/%s%s.fid?key=%s&VID=%s&premium=1&vip=1&alpha'
@@ -139,9 +151,6 @@ def extract_video_url(vl):
         duration = parse_duration(self._html_search_meta(
             'duration', webpage, 'duration', default=None))
 
-        def extract_field(pattern, name):
-            return self._html_search_regex(pattern, webpage, name, default=None) if pattern else None
-
         description = extract_field(self._DESCRIPTION_REGEX, 'description')
         uploader = extract_field(self._UPLOADER_REGEX, 'uploader')
         view_count = str_to_int(extract_field(self._VIEW_COUNT_REGEX, 'view count'))
diff --git a/yt_dlp/extractor/tubitv.py b/yt_dlp/extractor/tubitv.py
index d91a46500..f5ed950be 100644
--- a/yt_dlp/extractor/tubitv.py
+++ b/yt_dlp/extractor/tubitv.py
@@ -22,6 +22,19 @@ class TubiTvIE(InfoExtractor):
     _NETRC_MACHINE = 'tubitv'
     _GEO_COUNTRIES = ['US']
     _TESTS = [{
+        'url': 'https://tubitv.com/movies/383676/tracker',
+        'md5': '566fa0f76870302d11af0de89511d3f0',
+        'info_dict': {
+            'id': '383676',
+            'ext': 'mp4',
+            'title': 'Tracker',
+            'description': 'md5:ff320baf43d0ad2655e538c1d5cd9706',
+            'uploader_id': 'f866e2677ea2f0dff719788e4f7f9195',
+            'release_year': 2010,
+            'thumbnail': r're:^https?://.+\.(jpe?g|png)$',
+            'duration': 6122,
+        },
+    }, {
         'url': 'http://tubitv.com/video/283829/the_comedian_at_the_friday',
         'md5': '43ac06be9326f41912dc64ccf7a80320',
         'info_dict': {
@@ -31,12 +44,10 @@ class TubiTvIE(InfoExtractor):
             'description': 'A stand up comedian is forced to look at the decisions in his life while on a one week trip to the west coast.',
             'uploader_id': 'bc168bee0d18dd1cb3b86c68706ab434',
         },
+        'skip': 'Content Unavailable'
     }, {
         'url': 'http://tubitv.com/tv-shows/321886/s01_e01_on_nom_stories',
         'only_matching': True,
-    }, {
-        'url': 'http://tubitv.com/movies/383676/tracker',
-        'only_matching': True,
     }, {
         'url': 'https://tubitv.com/movies/560057/penitentiary?start=true',
         'info_dict': {
@@ -47,11 +58,13 @@ class TubiTvIE(InfoExtractor):
             'uploader_id': 'd8fed30d4f24fcb22ec294421b9defc2',
             'release_year': 1979,
         },
-        'params': {
-            'skip_download': True,
-        },
+        'skip': 'Content Unavailable'
     }]
 
+    # DRM formats are included only to raise appropriate error
+    _UNPLAYABLE_FORMATS = ('hlsv6_widevine', 'hlsv6_widevine_nonclearlead', 'hlsv6_playready_psshv0',
+                           'hlsv6_fairplay', 'dash_widevine', 'dash_widevine_nonclearlead')
+
     def _perform_login(self, username, password):
         self.report_login()
         form_data = {
@@ -69,17 +82,26 @@ def _perform_login(self, username, password):
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        video_data = self._download_json(
-            'https://tubitv.com/oz/videos/%s/content?video_resources=dash&video_resources=hlsv3&video_resources=hlsv6' % video_id, video_id)
+        video_data = self._download_json(f'https://tubitv.com/oz/videos/{video_id}/content', video_id, query={
+            'video_resources': ['dash', 'hlsv3', 'hlsv6', *self._UNPLAYABLE_FORMATS],
+        })
         title = video_data['title']
 
         formats = []
+        drm_formats = False
 
         for resource in video_data['video_resources']:
             if resource['type'] in ('dash', ):
                 formats += self._extract_mpd_formats(resource['manifest']['url'], video_id, mpd_id=resource['type'], fatal=False)
             elif resource['type'] in ('hlsv3', 'hlsv6'):
                 formats += self._extract_m3u8_formats(resource['manifest']['url'], video_id, 'mp4', m3u8_id=resource['type'], fatal=False)
+            elif resource['type'] in self._UNPLAYABLE_FORMATS:
+                drm_formats = True
+
+        if not formats and drm_formats:
+            self.report_drm(video_id)
+        elif not formats and not video_data.get('policy_match'):  # policy_match is False if content was removed
+            raise ExtractorError('This content is currently unavailable', expected=True)
 
         self._sort_formats(formats)
 
diff --git a/yt_dlp/extractor/viu.py b/yt_dlp/extractor/viu.py
index 63b6fd3a1..d27091c94 100644
--- a/yt_dlp/extractor/viu.py
+++ b/yt_dlp/extractor/viu.py
@@ -164,12 +164,17 @@ class ViuOTTIE(InfoExtractor):
         },
         'skip': 'Geo-restricted to Singapore',
     }, {
-        'url': 'http://www.viu.com/ott/hk/zh-hk/vod/7123/%E5%A4%A7%E4%BA%BA%E5%A5%B3%E5%AD%90',
+        'url': 'https://www.viu.com/ott/hk/zh-hk/vod/430078/%E7%AC%AC%E5%85%AD%E6%84%9F-3',
         'info_dict': {
-            'id': '7123',
+            'id': '430078',
             'ext': 'mp4',
-            'title': '這就是我的生活之道',
-            'description': 'md5:4eb0d8b08cf04fcdc6bbbeb16043434f',
+            'title': '大韓民國的1%',
+            'description': 'md5:74d6db47ddd9ddb9c89a05739103ccdb',
+            'episode_number': 1,
+            'duration': 6614,
+            'episode': '大韓民國的1%',
+            'series': '第六感 3',
+            'thumbnail': 'https://d2anahhhmp1ffz.cloudfront.net/1313295781/d2b14f48d008ef2f3a9200c98d8e9b63967b9cc2',
         },
         'params': {
             'skip_download': 'm3u8 download',
@@ -177,11 +182,12 @@ class ViuOTTIE(InfoExtractor):
         },
         'skip': 'Geo-restricted to Hong Kong',
     }, {
-        'url': 'https://www.viu.com/ott/hk/zh-hk/vod/68776/%E6%99%82%E5%B0%9A%E5%AA%BD%E5%92%AA',
-        'playlist_count': 12,
+        'url': 'https://www.viu.com/ott/hk/zh-hk/vod/444666/%E6%88%91%E7%9A%84%E5%AE%A4%E5%8F%8B%E6%98%AF%E4%B9%9D%E5%B0%BE%E7%8B%90',
+        'playlist_count': 16,
         'info_dict': {
-            'id': '3916',
-            'title': '時尚媽咪',
+            'id': '23807',
+            'title': '我的室友是九尾狐',
+            'description': 'md5:b42c95f2b4a316cdd6ae14ca695f33b9',
         },
         'params': {
             'skip_download': 'm3u8 download',
@@ -363,13 +369,19 @@ def download_playback():
 
         subtitles = {}
         for sub in video_data.get('subtitle') or []:
-            sub_url = sub.get('url')
-            if not sub_url:
-                continue
-            subtitles.setdefault(sub.get('name'), []).append({
-                'url': sub_url,
-                'ext': 'srt',
-            })
+            lang = sub.get('name') or 'und'
+            if sub.get('url'):
+                subtitles.setdefault(lang, []).append({
+                    'url': sub['url'],
+                    'ext': 'srt',
+                    'name': f'Spoken text for {lang}',
+                })
+            if sub.get('second_subtitle_url'):
+                subtitles.setdefault(f'{lang}_ost', []).append({
+                    'url': sub['second_subtitle_url'],
+                    'ext': 'srt',
+                    'name': f'On-screen text for {lang}',
+                })
 
         title = strip_or_none(video_data.get('synopsis'))
         return {
diff --git a/yt_dlp/extractor/vk.py b/yt_dlp/extractor/vk.py
index 69f518b69..0c856e2b0 100644
--- a/yt_dlp/extractor/vk.py
+++ b/yt_dlp/extractor/vk.py
@@ -536,7 +536,7 @@ def _real_extract(self, url):
 class VKUserVideosIE(VKBaseIE):
     IE_NAME = 'vk:uservideos'
     IE_DESC = "VK - User's Videos"
-    _VALID_URL = r'https?://(?:(?:m|new)\.)?vk\.com/video/@(?P<id>[^?$#/&]+)(?!\?.*\bz=video)(?:[/?#&](?:.*?\bsection=(?P<section>\w+))?|$)'
+    _VALID_URL = r'https?://(?:(?:m|new)\.)?vk\.com/video/(?:playlist/)?(?P<id>[^?$#/&]+)(?!\?.*\bz=video)(?:[/?#&](?:.*?\bsection=(?P<section>\w+))?|$)'
     _TEMPLATE_URL = 'https://vk.com/videos'
     _TESTS = [{
         'url': 'https://vk.com/video/@mobidevices',
@@ -550,6 +550,13 @@ class VKUserVideosIE(VKBaseIE):
             'id': '-17892518_uploaded',
         },
         'playlist_mincount': 182,
+    }, {
+        'url': 'https://vk.com/video/playlist/-174476437_2',
+        'info_dict': {
+            'id': '-174476437_2',
+            'title': 'Анонсы'
+        },
+        'playlist_mincount': 108,
     }]
     _VIDEO = collections.namedtuple('Video', ['owner_id', 'id'])
 
@@ -584,11 +591,19 @@ def _entries(self, page_id, section):
     def _real_extract(self, url):
         u_id, section = self._match_valid_url(url).groups()
         webpage = self._download_webpage(url, u_id)
-        page_id = self._search_regex(r'data-owner-id\s?=\s?"([^"]+)"', webpage, 'page_id')
+
+        if u_id.startswith('@'):
+            page_id = self._search_regex(r'data-owner-id\s?=\s?"([^"]+)"', webpage, 'page_id')
+        elif '_' in u_id:
+            page_id, section = u_id.split('_', 1)
+        else:
+            raise ExtractorError('Invalid URL', expected=True)
+
         if not section:
             section = 'all'
 
-        return self.playlist_result(self._entries(page_id, section), '%s_%s' % (page_id, section))
+        playlist_title = clean_html(get_element_by_class('VideoInfoPanel__title', webpage))
+        return self.playlist_result(self._entries(page_id, section), '%s_%s' % (page_id, section), playlist_title)
 
 
 class VKWallPostIE(VKBaseIE):
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 4456110f6..6f153bb3c 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -912,8 +912,7 @@ def _extract_video(self, renderer):
                 traverse_obj(renderer, ('title', 'accessibility', 'accessibilityData', 'label'), default='', expected_type=str),
                 video_id, default=None, group='duration'))
 
-        view_count = self._get_count(renderer, 'viewCountText')
-
+        view_count = self._get_count(renderer, 'viewCountText', 'shortViewCountText')
         uploader = self._get_text(renderer, 'ownerText', 'shortBylineText')
         channel_id = traverse_obj(
             renderer, ('shortBylineText', 'runs', ..., 'navigationEndpoint', 'browseEndpoint', 'browseId'),
@@ -932,6 +931,12 @@ def _extract_video(self, renderer):
         if overlay_style == 'SHORTS' or '/shorts/' in navigation_url:
             url = f'https://www.youtube.com/shorts/{video_id}'
 
+        live_status = (
+            'is_upcoming' if scheduled_timestamp is not None
+            else 'was_live' if 'streamed' in time_text.lower()
+            else 'is_live' if overlay_style == 'LIVE' or self._has_badge(badges, BadgeType.LIVE_NOW)
+            else None)
+
         return {
             '_type': 'url',
             'ie_key': YoutubeIE.ie_key(),
@@ -940,17 +945,12 @@ def _extract_video(self, renderer):
             'title': title,
             'description': description,
             'duration': duration,
-            'view_count': view_count,
             'uploader': uploader,
             'channel_id': channel_id,
             'thumbnails': thumbnails,
             'upload_date': (strftime_or_none(self._parse_time_text(time_text), '%Y%m%d')
                             if self._configuration_arg('approximate_date', ie_key='youtubetab')
                             else None),
-            'live_status': ('is_upcoming' if scheduled_timestamp is not None
-                            else 'was_live' if 'streamed' in time_text.lower()
-                            else 'is_live' if overlay_style == 'LIVE' or self._has_badge(badges, BadgeType.LIVE_NOW)
-                            else None),
             'release_timestamp': scheduled_timestamp,
             'availability':
                 'public' if self._has_badge(badges, BadgeType.AVAILABILITY_PUBLIC)
@@ -958,7 +958,8 @@ def _extract_video(self, renderer):
                     is_private=self._has_badge(badges, BadgeType.AVAILABILITY_PRIVATE) or None,
                     needs_premium=self._has_badge(badges, BadgeType.AVAILABILITY_PREMIUM) or None,
                     needs_subscription=self._has_badge(badges, BadgeType.AVAILABILITY_SUBSCRIPTION) or None,
-                    is_unlisted=self._has_badge(badges, BadgeType.AVAILABILITY_UNLISTED) or None)
+                    is_unlisted=self._has_badge(badges, BadgeType.AVAILABILITY_UNLISTED) or None),
+            'concurrent_view_count' if live_status in ('is_live', 'is_upcoming') else 'view_count': view_count,
         }
 
 
@@ -2328,6 +2329,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
                 'view_count': int,
                 'playable_in_embed': True,
                 'description': 'md5:2ef1d002cad520f65825346e2084e49d',
+                'concurrent_view_count': int,
             },
             'params': {'skip_download': True}
         }, {
@@ -4115,6 +4117,15 @@ def process_language(container, base_url, lang_code, sub_name, query):
                     'like_count': str_to_int(like_count),
                     'dislike_count': str_to_int(dislike_count),
                 })
+            vcr = traverse_obj(vpir, ('viewCount', 'videoViewCountRenderer'))
+            if vcr:
+                vc = self._get_count(vcr, 'viewCount')
+                # Upcoming premieres with waiting count are treated as live here
+                if vcr.get('isLive'):
+                    info['concurrent_view_count'] = vc
+                elif info.get('view_count') is None:
+                    info['view_count'] = vc
+
         vsir = get_first(contents, 'videoSecondaryInfoRenderer')
         if vsir:
             vor = traverse_obj(vsir, ('owner', 'videoOwnerRenderer'))
diff --git a/yt_dlp/postprocessor/embedthumbnail.py b/yt_dlp/postprocessor/embedthumbnail.py
index 9ae59a7c3..b02d9d499 100644
--- a/yt_dlp/postprocessor/embedthumbnail.py
+++ b/yt_dlp/postprocessor/embedthumbnail.py
@@ -92,7 +92,7 @@ def run(self, info):
         if info['ext'] == 'mp3':
             options = [
                 '-c', 'copy', '-map', '0:0', '-map', '1:0', '-write_id3v1', '1', '-id3v2_version', '3',
-                '-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment="Cover (front)"']
+                '-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment=Cover (front)']
 
             self._report_run('ffmpeg', filename)
             self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py
index d0be7f19e..cb14908c7 100644
--- a/yt_dlp/utils.py
+++ b/yt_dlp/utils.py
@@ -5294,7 +5294,7 @@ def load_plugins(name, suffix, namespace):
 
 
 def traverse_obj(
-        obj, *paths, default=None, expected_type=None, get_all=True,
+        obj, *paths, default=NO_DEFAULT, expected_type=None, get_all=True,
         casesense=True, is_user_input=False, traverse_string=False):
     """
     Safely traverse nested `dict`s and `Sequence`s
@@ -5304,13 +5304,15 @@ def traverse_obj(
     "value"
 
     Each of the provided `paths` is tested and the first producing a valid result will be returned.
+    The next path will also be tested if the path branched but no results could be found.
+    Supported values for traversal are `Mapping`, `Sequence` and `re.Match`.
     A value of None is treated as the absence of a value.
 
     The paths will be wrapped in `variadic`, so that `'key'` is conveniently the same as `('key', )`.
 
     The keys in the path can be one of:
         - `None`:           Return the current object.
-        - `str`/`int`:      Return `obj[key]`.
+        - `str`/`int`:      Return `obj[key]`. For `re.Match, return `obj.group(key)`.
         - `slice`:          Branch out and return all values in `obj[key]`.
         - `Ellipsis`:       Branch out and return a list of all values.
         - `tuple`/`list`:   Branch out and return a list of all matching values.
@@ -5321,7 +5323,7 @@ def traverse_obj(
         - `dict`            Transform the current object and return a matching dict.
                             Read as: `{key: traverse_obj(obj, path) for key, path in dct.items()}`.
 
-        `tuple`, `list`, and `dict` all support nested paths and branches
+        `tuple`, `list`, and `dict` all support nested paths and branches.
 
     @params paths           Paths which to traverse by.
     @param default          Value to return if the paths do not match.
@@ -5342,6 +5344,7 @@ def traverse_obj(
     @returns                The result of the object traversal.
                             If successful, `get_all=True`, and the path branches at least once,
                             then a list of results is returned instead.
+                            A list is always returned if the last path branches and no `default` is given.
     """
     is_sequence = lambda x: isinstance(x, collections.abc.Sequence) and not isinstance(x, (str, bytes))
     casefold = lambda k: k.casefold() if isinstance(k, str) else k
@@ -5368,6 +5371,8 @@ def apply_key(key, obj):
                 yield from obj.values()
             elif is_sequence(obj):
                 yield from obj
+            elif isinstance(obj, re.Match):
+                yield from obj.groups()
             elif traverse_string:
                 yield from str(obj)
 
@@ -5376,6 +5381,8 @@ def apply_key(key, obj):
                 iter_obj = enumerate(obj)
             elif isinstance(obj, collections.abc.Mapping):
                 iter_obj = obj.items()
+            elif isinstance(obj, re.Match):
+                iter_obj = enumerate((obj.group(), *obj.groups()))
             elif traverse_string:
                 iter_obj = enumerate(str(obj))
             else:
@@ -5385,12 +5392,23 @@ def apply_key(key, obj):
         elif isinstance(key, dict):
             iter_obj = ((k, _traverse_obj(obj, v)) for k, v in key.items())
             yield {k: v if v is not None else default for k, v in iter_obj
-                   if v is not None or default is not None}
+                   if v is not None or default is not NO_DEFAULT}
 
-        elif isinstance(obj, dict):
+        elif isinstance(obj, collections.abc.Mapping):
             yield (obj.get(key) if casesense or (key in obj)
                    else next((v for k, v in obj.items() if casefold(k) == key), None))
 
+        elif isinstance(obj, re.Match):
+            if isinstance(key, int) or casesense:
+                with contextlib.suppress(IndexError):
+                    yield obj.group(key)
+                    return
+
+            if not isinstance(key, str):
+                return
+
+            yield next((v for k, v in obj.groupdict().items() if casefold(k) == key), None)
+
         else:
             if is_user_input:
                 key = (int_or_none(key) if ':' not in key
@@ -5426,18 +5444,22 @@ def apply_path(start_obj, path):
 
         return has_branched, objs
 
-    def _traverse_obj(obj, path):
+    def _traverse_obj(obj, path, use_list=True):
         has_branched, results = apply_path(obj, path)
         results = LazyList(x for x in map(type_test, results) if x is not None)
-        if results:
-            return results.exhaust() if get_all and has_branched else results[0]
 
-    for path in paths:
-        result = _traverse_obj(obj, path)
+        if get_all and has_branched:
+            return results.exhaust() if results or use_list else None
+
+        return results[0] if results else None
+
+    for index, path in enumerate(paths, 1):
+        use_list = default is NO_DEFAULT and index == len(paths)
+        result = _traverse_obj(obj, path, use_list)
         if result is not None:
             return result
 
-    return default
+    return None if default is NO_DEFAULT else default
 
 
 def traverse_dict(dictn, keys, casesense=True):


Test patch:
diff --git a/test/test_utils.py b/test/test_utils.py
index 69313564a..90085a9c0 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -2,6 +2,7 @@
 
 # Allow direct execution
 import os
+import re
 import sys
 import unittest
 
@@ -1890,6 +1891,7 @@ def test_traverse_obj(self):
                 {'index': 2},
                 {'index': 3},
             ),
+            'dict': {},
         }
 
         # Test base functionality
@@ -1926,11 +1928,15 @@ def test_traverse_obj(self):
 
         # Test alternative paths
         self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'str'), 'str',
-                         msg='multiple `path_list` should be treated as alternative paths')
+                         msg='multiple `paths` should be treated as alternative paths')
         self.assertEqual(traverse_obj(_TEST_DATA, 'str', 100), 'str',
                          msg='alternatives should exit early')
         self.assertEqual(traverse_obj(_TEST_DATA, 'fail', 'fail'), None,
                          msg='alternatives should return `default` if exhausted')
+        self.assertEqual(traverse_obj(_TEST_DATA, (..., 'fail'), 100), 100,
+                         msg='alternatives should track their own branching return')
+        self.assertEqual(traverse_obj(_TEST_DATA, ('dict', ...), ('data', ...)), list(_TEST_DATA['data']),
+                         msg='alternatives on empty objects should search further')
 
         # Test branch and path nesting
         self.assertEqual(traverse_obj(_TEST_DATA, ('urls', (3, 0), 'url')), ['https://www.example.com/0'],
@@ -1963,8 +1969,16 @@ def test_traverse_obj(self):
         self.assertEqual(traverse_obj(_TEST_DATA, {0: ('urls', ((1, ('fail', 'url')), (0, 'url')))}),
                          {0: ['https://www.example.com/1', 'https://www.example.com/0']},
                          msg='tripple nesting in dict path should be treated as branches')
-        self.assertEqual(traverse_obj({}, {0: 1}, default=...), {0: ...},
-                         msg='do not remove `None` values when dict key')
+        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}), {},
+                         msg='remove `None` values when dict key')
+        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'fail'}, default=...), {0: ...},
+                         msg='do not remove `None` values if `default`')
+        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}), {0: {}},
+                         msg='do not remove empty values when dict key')
+        self.assertEqual(traverse_obj(_TEST_DATA, {0: 'dict'}, default=...), {0: {}},
+                         msg='do not remove empty values when dict key and a default')
+        self.assertEqual(traverse_obj(_TEST_DATA, {0: ('dict', ...)}), {0: []},
+                         msg='if branch in dict key not successful, return `[]`')
 
         # Testing default parameter behavior
         _DEFAULT_DATA = {'None': None, 'int': 0, 'list': []}
@@ -1981,7 +1995,13 @@ def test_traverse_obj(self):
         self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', 10)), None,
                          msg='`IndexError` should result in `default`')
         self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=1), 1,
-                         msg='if branched but not successfull return `default`, not `[]`')
+                         msg='if branched but not successful return `default` if defined, not `[]`')
+        self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail'), default=None), None,
+                         msg='if branched but not successful return `default` even if `default` is `None`')
+        self.assertEqual(traverse_obj(_DEFAULT_DATA, (..., 'fail')), [],
+                         msg='if branched but not successful return `[]`, not `default`')
+        self.assertEqual(traverse_obj(_DEFAULT_DATA, ('list', ...)), [],
+                         msg='if branched but object is empty return `[]`, not `default`')
 
         # Testing expected_type behavior
         _EXPECTED_TYPE_DATA = {'str': 'str', 'int': 0}
@@ -2061,6 +2081,25 @@ def test_traverse_obj(self):
         with self.assertRaises(TypeError, msg='too many params should result in error'):
             traverse_obj(_IS_USER_INPUT_DATA, ('range8', ':::'), is_user_input=True)
 
+        # Test re.Match as input obj
+        mobj = re.fullmatch(r'0(12)(?P<group>3)(4)?', '0123')
+        self.assertEqual(traverse_obj(mobj, ...), [x for x in mobj.groups() if x is not None],
+                         msg='`...` on a `re.Match` should give its `groups()`')
+        self.assertEqual(traverse_obj(mobj, lambda k, _: k in (0, 2)), ['0123', '3'],
+                         msg='function on a `re.Match` should give groupno, value starting at 0')
+        self.assertEqual(traverse_obj(mobj, 'group'), '3',
+                         msg='str key on a `re.Match` should give group with that name')
+        self.assertEqual(traverse_obj(mobj, 2), '3',
+                         msg='int key on a `re.Match` should give group with that name')
+        self.assertEqual(traverse_obj(mobj, 'gRoUp', casesense=False), '3',
+                         msg='str key on a `re.Match` should respect casesense')
+        self.assertEqual(traverse_obj(mobj, 'fail'), None,
+                         msg='failing str key on a `re.Match` should return `default`')
+        self.assertEqual(traverse_obj(mobj, 'gRoUpS', casesense=False), None,
+                         msg='failing str key on a `re.Match` should return `default`')
+        self.assertEqual(traverse_obj(mobj, 8), None,
+                         msg='failing int key on a `re.Match` should return `default`')
+
 
 if __name__ == '__main__':
     unittest.main()