Repository: yt-dlp/yt-dlp

Issue:
[niconico] `KeyError('media')` Nicovideo downloads not working
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

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

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I'm running yt-dlp version **2023.10.13** ([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 [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [ ] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Japan

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

yt-dlp doesn't work with nicovideo videos

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
PS C:\Users\Paul\Documents\!Tools\youtube-dlp> .\yt-dlp.exe -vU https://www.nicovideo.jp/watch/so42910636
[debug] Command-line config: ['-vU', 'https://www.nicovideo.jp/watch/so42910636']
[debug] Encodings: locale cp1250, fs utf-8, pref cp1250, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.10.13 [b634ba742] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.19042-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg 2023-08-30-git-7aa71ab5c0-full_build-www.gyan.dev (setts), ffprobe 2023-08-30-git-7aa71ab5c0-full_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.19.0, brotli-1.1.0, certifi-2023.07.22, mutagen-1.47.0, sqlite3-3.35.5, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1890 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Available version: stable@2023.10.13, Current version: stable@2023.10.13
Current Build Hash: 13d6a04ef6678dc61cb6e2d42eb53e69486bd2f52b8e5d778db029ccd4c600b4
yt-dlp is up to date (stable@2023.10.13)
[niconico] Extracting URL: https://www.nicovideo.jp/watch/so42910636
[niconico] so42910636: Downloading webpage
ERROR: so42910636: An extractor error has occurred. (caused by KeyError('media')); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "yt_dlp\extractor\common.py", line 715, in extract
  File "yt_dlp\extractor\niconico.py", line 423, in _real_extract
KeyError: 'media'
```

Niconico - Support for DMS formats
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field

### Checklist

- [X] I'm requesting a site-specific feature
- [X] I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and 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)
- [x] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

_No response_

### Example URLs

https://www.nicovideo.jp/watch/sm43110779

### Provide a description that is worded well enough to be understood

Since I think December, videos longer than 30 minutes can now benefit from resolutions higher than 360p on Niconico. This wasn't the case before. For the video example I provided (sm43110779), resolutions up to 720p are available on the player within a web browser. A screenshot is included.
![sm43110779_720p](https://github.com/yt-dlp/yt-dlp/assets/5454404/0aa4b210-f04e-4fe0-b046-f5e53806d5d4)
These resolutions aren't offered when trying out yt-dlp, even though I'm using --cookies-from-browser, as I usually do (as can be seen on the verbose output).

Is there something I have overlooked on my side, or is it a new feature that would have to be implemented? If the latter, it would be great as I don't see myself downloading each segment one by one and reassembling them by hand (Niconico now exclusively uses HLS now, and this particular video loads a good chunk of CMFV and CMFA segments).

It looks like from what I've briefly seen that other available tools seem to only the 360p variants too on such video.

Edit: it seems it's possible to extract matching m3u8 playlists matching this particular video. A look inside the video-h264-720p.m3u8 reveals that this video has a total of 341 CMFV segments. But downloading them manually doesn't seem to yield valid video streams. I'm not qualified enough in order to understand what is going on.

Edit2: it looks like resolutions above 360p are locked behind Premium account, that could be an indication of why yt-dlp doesn't retrieve all the available formats (I might be wrong), thing is my Niconico account is already premium and I can actually watch the video within a browser

Edit3: it looks like yt-dlp doesn't support DMS yet on Niconico. A new system called DMS has been been introduced on Niconico on November apparently, so I had to use [the commits from this pull request](https://github.com/yt-dlp/yt-dlp/pull/8685/commits) in order to be able to download the video (video stream only, no audio yet, or I did something wrong, but I can still grab the audio from the older DMC streams).

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', '--cookies-from-browser', 'firefox', '-F', 'https://www.nicovideo.jp/watch/sm43110779']
Extracting cookies from firefox
[debug] Extracting cookies from: "C:\Users\[...]\AppData\Roaming\Mozilla\Firefox\Profiles\[...]\cookies.sqlite"
Extracted 1195 cookies from firefox
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version nightly@2023.12.06.232721 from yt-dlp/yt-dlp-nightly-builds [0b6f829b1] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.19045-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg N-107951-g90aa2a88f9-20220901 (setts), ffprobe N-107951-g90aa2a88f9-20220901
[debug] Optional libraries: Cryptodome-3.19.0, brotli-1.1.0, certifi-2023.11.17, mutagen-1.47.0, requests-2.31.0, sqlite3-3.35.5, urllib3-2.1.0, websockets-12.0
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1792 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp-nightly-builds/releases/latest
Latest version: nightly@2023.12.06.232721 from yt-dlp/yt-dlp-nightly-builds
yt-dlp is up to date (nightly@2023.12.06.232721 from yt-dlp/yt-dlp-nightly-builds)
[niconico] Extracting URL: https://www.nicovideo.jp/watch/sm43110779
[niconico] sm43110779: Downloading webpage
[debug] Formats sorted by: hasvid, ie_pref, lang, quality, res, fps, hdr:12(7), vcodec:vp9.2(10), channels, acodec, size, br, asr, proto, vext, aext, hasaud, source, id
[info] Available formats for sm43110779:
ID                             EXT RESOLUTION │   FILESIZE  TBR PROTO │ VCODEC  VBR ACODEC  ABR MORE INFO
─────────────────────────────────────────────────────────────────────────────────────────────────────────────
h264_360p_low-aac_64kbps-hls   mp4 640x360    │ ~ 90.64MiB 364k dmc   │ h264   300k aac     64k DMC 低画質 HLS
h264_360p_low-aac_64kbps-http  mp4 640x360    │ ~ 90.64MiB 364k dmc   │ h264   300k aac     64k DMC 低画質 HTTP
h264_360p_low-aac_128kbps-hls  mp4 640x360    │ ~106.58MiB 428k dmc   │ h264   300k aac    128k DMC 低画質 HLS
h264_360p_low-aac_128kbps-http mp4 640x360    │ ~106.58MiB 428k dmc   │ h264   300k aac    128k DMC 低画質 HTTP
h264_360p-aac_64kbps-hls       mp4 640x360    │ ~165.35MiB 664k dmc   │ h264   600k aac     64k DMC 360p HLS
h264_360p-aac_64kbps-http      mp4 640x360    │ ~165.35MiB 664k dmc   │ h264   600k aac     64k DMC 360p HTTP
h264_360p-aac_128kbps-hls      mp4 640x360    │ ~181.29MiB 728k dmc   │ h264   600k aac    128k DMC 360p HLS
h264_360p-aac_128kbps-http     mp4 640x360    │ ~181.29MiB 728k dmc   │ h264   600k aac    128k DMC 360p HTTP
```

Nicovideo.jp "ERROR: 'NoneType' object is not subscriptable"
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

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

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
- [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 [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [ ] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

_No response_

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

Nicovideo did something to completely break every hls-extractor(not only yt-dlp) on new videos uploaded after "2024/02/20 10:10(CET/UTC+1)".

You can check for yourself by trying to download https://www.nicovideo.jp/watch/sm43420386.
Videos uploaded before that specific date work properly without any problem, such as https://www.nicovideo.jp/watch/sm43419324.

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [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.nicovideo.jp/watch/sm43420386']
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.12.30 from yt-dlp/yt-dlp [f10589e34] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.18363-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg git-2020-01-02-81172b5, ffprobe git-2020-01-02-81172b5
[debug] Optional libraries: Cryptodome-3.19.1, brotli-1.1.0, certifi-2023.11.17, mutagen-1.47.0, requests-2.31.0, sqlite3-3.35.5, urllib3-2.1.0, websockets-12.0
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1798 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2023.12.30 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2023.12.30 from yt-dlp/yt-dlp)
[niconico] Extracting URL: https://www.nicovideo.jp/watch/sm43420386
[niconico] sm43420386: Downloading webpage
ERROR: 'NoneType' object is not subscriptable
Traceback (most recent call last):
  File "yt_dlp\YoutubeDL.py", line 1587, in wrapper
  File "yt_dlp\YoutubeDL.py", line 1722, in __extract_info
  File "yt_dlp\extractor\common.py", line 718, in extract
  File "yt_dlp\extractor\niconico.py", line 422, in _real_extract
TypeError: 'NoneType' object is not subscriptable
```



Discussion:

Update: I was able to download this particular video with help of [the commits in this pull request](https://github.com/yt-dlp/yt-dlp/pull/8685/commits). It looks like Niconico is undergoing a transition to a new system called DMS, which isn't supported yet by the official yt-dlp binary releases available. By applying all the changes from this commit, I was able to retrieve its video stream, but still no audio for the moment (I might be doing something wrong).


Resolving patch:
diff --git a/yt_dlp/extractor/niconico.py b/yt_dlp/extractor/niconico.py
--- a/yt_dlp/extractor/niconico.py
+++ b/yt_dlp/extractor/niconico.py
@@ -13,13 +13,11 @@
 from ..utils import (
     ExtractorError,
     OnDemandPagedList,
-    bug_reports_message,
     clean_html,
     float_or_none,
     int_or_none,
     join_nonempty,
     parse_duration,
-    parse_filesize,
     parse_iso8601,
     parse_resolution,
     qualities,
@@ -55,25 +53,31 @@ class NiconicoIE(InfoExtractor):
             'duration': 33,
             'view_count': int,
             'comment_count': int,
+            'genres': ['未設定'],
+            'tags': [],
+            'expected_protocol': str,
         },
-        'skip': 'Requires an account',
     }, {
         # File downloaded with and without credentials are different, so omit
         # the md5 field
         'url': 'http://www.nicovideo.jp/watch/nm14296458',
         'info_dict': {
             'id': 'nm14296458',
-            'ext': 'swf',
-            'title': '【鏡音リン】Dance on media【オリジナル】take2!',
-            'description': 'md5:689f066d74610b3b22e0f1739add0f58',
+            'ext': 'mp4',
+            'title': '【Kagamine Rin】Dance on media【Original】take2!',
+            'description': 'md5:9368f2b1f4178de64f2602c2f3d6cbf5',
             'thumbnail': r're:https?://.*',
             'uploader': 'りょうた',
             'uploader_id': '18822557',
             'upload_date': '20110429',
             'timestamp': 1304065916,
-            'duration': 209,
+            'duration': 208.0,
+            'comment_count': int,
+            'view_count': int,
+            'genres': ['音楽・サウンド'],
+            'tags': ['Translation_Request', 'Kagamine_Rin', 'Rin_Original'],
+            'expected_protocol': str,
         },
-        'skip': 'Requires an account',
     }, {
         # 'video exists but is marked as "deleted"
         # md5 is unstable
@@ -107,22 +111,24 @@ class NiconicoIE(InfoExtractor):
     }, {
         # video not available via `getflv`; "old" HTML5 video
         'url': 'http://www.nicovideo.jp/watch/sm1151009',
-        'md5': '8fa81c364eb619d4085354eab075598a',
+        'md5': 'f95a3d259172667b293530cc2e41ebda',
         'info_dict': {
             'id': 'sm1151009',
             'ext': 'mp4',
             'title': 'マスターシステム本体内蔵のスペハリのメインテーマ（ＰＳＧ版）',
-            'description': 'md5:6ee077e0581ff5019773e2e714cdd0b7',
+            'description': 'md5:f95a3d259172667b293530cc2e41ebda',
             'thumbnail': r're:https?://.*',
             'duration': 184,
-            'timestamp': 1190868283,
-            'upload_date': '20070927',
+            'timestamp': 1190835883,
+            'upload_date': '20070926',
             'uploader': 'denden2',
             'uploader_id': '1392194',
             'view_count': int,
             'comment_count': int,
+            'genres': ['ゲーム'],
+            'tags': [],
+            'expected_protocol': str,
         },
-        'skip': 'Requires an account',
     }, {
         # "New" HTML5 video
         # md5 is unstable
@@ -132,16 +138,18 @@ class NiconicoIE(InfoExtractor):
             'ext': 'mp4',
             'title': '新作TVアニメ「戦姫絶唱シンフォギアAXZ」PV 最高画質',
             'description': 'md5:e52974af9a96e739196b2c1ca72b5feb',
-            'timestamp': 1498514060,
+            'timestamp': 1498481660,
             'upload_date': '20170626',
-            'uploader': 'ゲスト',
+            'uploader': 'no-namamae',
             'uploader_id': '40826363',
             'thumbnail': r're:https?://.*',
             'duration': 198,
             'view_count': int,
             'comment_count': int,
+            'genres': ['アニメ'],
+            'tags': [],
+            'expected_protocol': str,
         },
-        'skip': 'Requires an account',
     }, {
         # Video without owner
         'url': 'http://www.nicovideo.jp/watch/sm18238488',
@@ -151,7 +159,7 @@ class NiconicoIE(InfoExtractor):
             'ext': 'mp4',
             'title': '【実写版】ミュータントタートルズ',
             'description': 'md5:15df8988e47a86f9e978af2064bf6d8e',
-            'timestamp': 1341160408,
+            'timestamp': 1341128008,
             'upload_date': '20120701',
             'uploader': None,
             'uploader_id': None,
@@ -159,8 +167,10 @@ class NiconicoIE(InfoExtractor):
             'duration': 5271,
             'view_count': int,
             'comment_count': int,
+            'genres': ['エンターテイメント'],
+            'tags': [],
+            'expected_protocol': str,
         },
-        'skip': 'Requires an account',
     }, {
         'url': 'http://sp.nicovideo.jp/watch/sm28964488?ss_pos=1&cp_in=wt_tg',
         'only_matching': True,
@@ -353,15 +363,10 @@ def _extract_format_for_quality(self, video_id, audio_quality, video_quality, dm
         if not audio_quality.get('isAvailable') or not video_quality.get('isAvailable'):
             return None
 
-        def extract_video_quality(video_quality):
-            return parse_filesize('%sB' % self._search_regex(
-                r'\| ([0-9]*\.?[0-9]*[MK])', video_quality, 'vbr', default=''))
-
         format_id = '-'.join(
             [remove_start(s['id'], 'archive_') for s in (video_quality, audio_quality)] + [dmc_protocol])
 
         vid_qual_label = traverse_obj(video_quality, ('metadata', 'label'))
-        vid_quality = traverse_obj(video_quality, ('metadata', 'bitrate'))
 
         return {
             'url': 'niconico_dmc:%s/%s/%s' % (video_id, video_quality['id'], audio_quality['id']),
@@ -370,10 +375,15 @@ def extract_video_quality(video_quality):
             'ext': 'mp4',  # Session API are used in HTML5, which always serves mp4
             'acodec': 'aac',
             'vcodec': 'h264',
-            'abr': float_or_none(traverse_obj(audio_quality, ('metadata', 'bitrate')), 1000),
-            'vbr': float_or_none(vid_quality if vid_quality > 0 else extract_video_quality(vid_qual_label), 1000),
-            'height': traverse_obj(video_quality, ('metadata', 'resolution', 'height')),
-            'width': traverse_obj(video_quality, ('metadata', 'resolution', 'width')),
+            **traverse_obj(audio_quality, ('metadata', {
+                'abr': ('bitrate', {functools.partial(float_or_none, scale=1000)}),
+                'asr': ('samplingRate', {int_or_none}),
+            })),
+            **traverse_obj(video_quality, ('metadata', {
+                'vbr': ('bitrate', {functools.partial(float_or_none, scale=1000)}),
+                'height': ('resolution', 'height', {int_or_none}),
+                'width': ('resolution', 'width', {int_or_none}),
+            })),
             'quality': -2 if 'low' in video_quality['id'] else None,
             'protocol': 'niconico_dmc',
             'expected_protocol': dmc_protocol,  # XXX: This is not a documented field
@@ -383,6 +393,63 @@ def extract_video_quality(video_quality):
             }
         }
 
+    def _yield_dmc_formats(self, api_data, video_id):
+        dmc_data = traverse_obj(api_data, ('media', 'delivery', 'movie'))
+        audios = traverse_obj(dmc_data, ('audios', ..., {dict}))
+        videos = traverse_obj(dmc_data, ('videos', ..., {dict}))
+        protocols = traverse_obj(dmc_data, ('session', 'protocols', ..., {str}))
+        if not all((audios, videos, protocols)):
+            return
+
+        for audio_quality, video_quality, protocol in itertools.product(audios, videos, protocols):
+            if fmt := self._extract_format_for_quality(video_id, audio_quality, video_quality, protocol):
+                yield fmt
+
+    def _yield_dms_formats(self, api_data, video_id):
+        fmt_filter = lambda _, v: v['isAvailable'] and v['id']
+        videos = traverse_obj(api_data, ('media', 'domand', 'videos', fmt_filter))
+        audios = traverse_obj(api_data, ('media', 'domand', 'audios', fmt_filter))
+        access_key = traverse_obj(api_data, ('media', 'domand', 'accessRightKey', {str}))
+        track_id = traverse_obj(api_data, ('client', 'watchTrackId', {str}))
+        if not all((videos, audios, access_key, track_id)):
+            return
+
+        dms_m3u8_url = self._download_json(
+            f'https://nvapi.nicovideo.jp/v1/watch/{video_id}/access-rights/hls', video_id,
+            data=json.dumps({
+                'outputs': list(itertools.product((v['id'] for v in videos), (a['id'] for a in audios)))
+            }).encode(), query={'actionTrackId': track_id}, headers={
+                'x-access-right-key': access_key,
+                'x-frontend-id': 6,
+                'x-frontend-version': 0,
+                'x-request-with': 'https://www.nicovideo.jp',
+            })['data']['contentUrl']
+        # Getting all audio formats results in duplicate video formats which we filter out later
+        dms_fmts = self._extract_m3u8_formats(dms_m3u8_url, video_id)
+
+        # m3u8 extraction does not provide audio bitrates, so extract from the API data and fix
+        for audio_fmt in traverse_obj(dms_fmts, lambda _, v: v['vcodec'] == 'none'):
+            yield {
+                **audio_fmt,
+                **traverse_obj(audios, (lambda _, v: audio_fmt['format_id'].startswith(v['id']), {
+                    'format_id': ('id', {str}),
+                    'abr': ('bitRate', {functools.partial(float_or_none, scale=1000)}),
+                    'asr': ('samplingRate', {int_or_none}),
+                }), get_all=False),
+                'acodec': 'aac',
+                'ext': 'm4a',
+            }
+
+        # Sort before removing dupes to keep the format dicts with the lowest tbr
+        video_fmts = sorted((fmt for fmt in dms_fmts if fmt['vcodec'] != 'none'), key=lambda f: f['tbr'])
+        self._remove_duplicate_formats(video_fmts)
+        # Calculate the true vbr/tbr by subtracting the lowest abr
+        min_abr = min(traverse_obj(audios, (..., 'bitRate', {float_or_none})), default=0) / 1000
+        for video_fmt in video_fmts:
+            video_fmt['tbr'] -= min_abr
+            video_fmt['format_id'] = f'video-{video_fmt["tbr"]:.0f}'
+            yield video_fmt
+
     def _real_extract(self, url):
         video_id = self._match_id(url)
 
@@ -409,19 +476,17 @@ def _real_extract(self, url):
                     webpage, 'error reason', default=None)
                 if not error_msg:
                     raise
-                raise ExtractorError(re.sub(r'\s+', ' ', error_msg), expected=True)
-
-        formats = []
-
-        def get_video_info(*items, get_first=True, **kwargs):
-            return traverse_obj(api_data, ('video', *items), get_all=not get_first, **kwargs)
-
-        quality_info = api_data['media']['delivery']['movie']
-        session_api_data = quality_info['session']
-        for (audio_quality, video_quality, protocol) in itertools.product(quality_info['audios'], quality_info['videos'], session_api_data['protocols']):
-            fmt = self._extract_format_for_quality(video_id, audio_quality, video_quality, protocol)
-            if fmt:
-                formats.append(fmt)
+                raise ExtractorError(clean_html(error_msg), expected=True)
+
+        club_joined = traverse_obj(api_data, ('channel', 'viewer', 'follow', 'isFollowed', {bool}))
+        if club_joined is None:
+            fail_msg = self._html_search_regex(
+                r'<p[^>]+\bclass="fail-message"[^>]*>(?P<msg>.+?)</p>',
+                webpage, 'fail message', default=None, group='msg')
+            if fail_msg:
+                self.raise_login_required(clean_html(fail_msg), metadata_available=True)
+        elif not club_joined:
+            self.raise_login_required('This video is for members only', metadata_available=True)
 
         # Start extracting information
         tags = None
@@ -440,11 +505,15 @@ def get_video_info(*items, get_first=True, **kwargs):
 
         thumb_prefs = qualities(['url', 'middleUrl', 'largeUrl', 'player', 'ogp'])
 
+        def get_video_info(*items, get_first=True, **kwargs):
+            return traverse_obj(api_data, ('video', *items), get_all=not get_first, **kwargs)
+
         return {
             'id': video_id,
             '_api_data': api_data,
             'title': get_video_info(('originalTitle', 'title')) or self._og_search_title(webpage, default=None),
-            'formats': formats,
+            'formats': [*self._yield_dmc_formats(api_data, video_id),
+                        *self._yield_dms_formats(api_data, video_id)],
             'thumbnails': [{
                 'id': key,
                 'url': url,
@@ -472,8 +541,11 @@ def get_video_info(*items, get_first=True, **kwargs):
 
     def _get_subtitles(self, video_id, api_data):
         comments_info = traverse_obj(api_data, ('comment', 'nvComment', {dict})) or {}
+        if not comments_info.get('server'):
+            return
+
         danmaku = traverse_obj(self._download_json(
-            f'{comments_info.get("server")}/v1/threads', video_id, data=json.dumps({
+            f'{comments_info["server"]}/v1/threads', video_id, data=json.dumps({
                 'additionals': {},
                 'params': comments_info.get('params'),
                 'threadKey': comments_info.get('threadKey'),
@@ -489,10 +561,6 @@ def _get_subtitles(self, video_id, api_data):
             note='Downloading comments', errnote='Failed to download comments'),
             ('data', 'threads', ..., 'comments', ...))
 
-        if not danmaku:
-            self.report_warning(f'Failed to get comments. {bug_reports_message()}')
-            return
-
         return {
             'comments': [{
                 'ext': 'json',


Test patch: