From 995fc27931c2e2cd417af69d3e4321fd6c8e0d0e Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:01:20 +1300 Subject: [PATCH 01/11] support storyboards --- yt_dlp/extractor/bilibili.py | 70 ++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 62f68fbc6d..80752ea2fb 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -33,6 +33,7 @@ from ..utils import ( parse_qs, parse_resolution, qualities, + sanitize_url, smuggle_url, srt_subtitles_timecode, str_or_none, @@ -66,7 +67,61 @@ class BilibiliBaseIE(InfoExtractor): f'Format(s) {missing_formats} are missing; you have to login or ' f'become a premium member to download them. {self._login_hint()}') - def extract_formats(self, play_info): + def _extract_storyboard(self, duration, aid=None, bvid=None, cid=None): + video_id = aid or bvid + query = filter_dict({ + 'aid': aid, + 'bvid': bvid, + 'cid': cid, + }) + if not aid and not bvid: + return {} + idx = 1 + while storyboard_info := traverse_obj(self._download_json( + 'https://api.bilibili.com/x/player/videoshot', video_id, + 'Downloading storyboard info', query={ + 'index': idx, + **query, + }), 'data'): + if storyboard_info.get('image') and storyboard_info.get('index'): + rows, cols = storyboard_info.get('img_x_len'), storyboard_info.get('img_y_len') + fragments = [] + last_duration = 0.0 + for i, url in enumerate(storyboard_info['image'], start=1): + duration_index = i * rows * cols - 1 + if duration_index < len(storyboard_info['index']) - 1: + current_duration = traverse_obj(storyboard_info, ('index', duration_index)) + else: + current_duration = duration + if current_duration > last_duration and current_duration <= duration: + fragments.append({ + 'url': sanitize_url(url), + 'duration': current_duration - last_duration, + }) + last_duration = current_duration + else: + break + if fragments: + yield { + 'format_id': f'sb{idx}', + 'format_note': 'storyboard', + 'ext': 'mhtml', + 'protocol': 'mhtml', + 'acodec': 'none', + 'vcodec': 'none', + 'url': 'about:invalid', + 'width': storyboard_info.get('img_x_size'), + 'height': storyboard_info.get('img_y_size'), + 'fps': len(storyboard_info['image']) * rows * cols / duration, + 'rows': rows, + 'columns': cols, + 'fragments': fragments, + } + else: + return + idx += 1 + + def extract_formats(self, play_info, aid=None, bvid=None, cid=None): format_names = { r['quality']: traverse_obj(r, 'new_description', 'display_desc') for r in traverse_obj(play_info, ('support_formats', lambda _, v: v['quality'])) @@ -128,6 +183,8 @@ class BilibiliBaseIE(InfoExtractor): }), **parse_resolution(format_names.get(play_info.get('quality'))), }) + formats.extend(self._extract_storyboard( + float_or_none(play_info.get('timelength'), scale=1000), aid=aid, bvid=bvid, cid=cid)) return formats def _get_wbi_key(self, video_id): @@ -291,7 +348,7 @@ class BilibiliBaseIE(InfoExtractor): **metainfo, 'id': f'{video_id}_{cid}', 'title': f'{metainfo.get("title")} - {next(iter(edges.values())).get("title")}', - 'formats': self.extract_formats(play_info), + 'formats': self.extract_formats(play_info, bvid=video_id, cid=cid), 'description': f'{json.dumps(edges, ensure_ascii=False)}\n{metainfo.get("description", "")}', 'duration': float_or_none(play_info.get('timelength'), scale=1000), 'subtitles': self.extract_subtitles(video_id, cid), @@ -728,14 +785,14 @@ class BiliBiliIE(BilibiliBaseIE): duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})), __post_extractor=self.extract_comments(aid)) else: - formats = self.extract_formats(play_info) + formats = self.extract_formats(play_info, bvid=video_id, cid=cid) if not traverse_obj(play_info, ('dash')): # we only have legacy formats and need additional work has_qn = lambda x: x in traverse_obj(formats, (..., 'quality')) for qn in traverse_obj(play_info, ('accept_quality', lambda _, v: not has_qn(v), {int})): formats.extend(traverse_obj( - self.extract_formats(self._download_playinfo(video_id, cid, headers=headers, qn=qn)), + self.extract_formats(self._download_playinfo(video_id, cid, headers=headers, qn=qn), bvid=video_id, cid=cid), lambda _, v: not has_qn(v['quality']))) self._check_missing_formats(play_info, formats) flv_formats = traverse_obj(formats, lambda _, v: v['fragments']) @@ -865,9 +922,10 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'Extracting episode', query={'fnval': '4048', 'ep_id': episode_id}, headers=headers) premium_only = play_info.get('code') == -10403 + bvid, cid = traverse_obj(play_info, ('result', 'play_view_business_info', 'episode_info', ('bvid', 'cid'))) play_info = traverse_obj(play_info, ('result', 'video_info', {dict})) or {} - formats = self.extract_formats(play_info) + formats = self.extract_formats(play_info, bvid=bvid, cid=cid) if not formats and (premium_only or '成为大会员抢先看' in webpage or '开通大会员观看' in webpage): self.raise_login_required('This video is for premium members only') @@ -1040,7 +1098,7 @@ class BilibiliCheeseBaseIE(BilibiliBaseIE): return { 'id': str_or_none(ep_id), 'episode_id': str_or_none(ep_id), - 'formats': self.extract_formats(play_info), + 'formats': self.extract_formats(play_info, aid=aid, cid=cid), 'extractor_key': BilibiliCheeseIE.ie_key(), 'extractor': BilibiliCheeseIE.IE_NAME, 'webpage_url': f'https://www.bilibili.com/cheese/play/ep{ep_id}', From 47254db76dc76ba8afca1c5042d258cb022f14ba Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:14:16 +1300 Subject: [PATCH 02/11] misc --- yt_dlp/extractor/bilibili.py | 93 ++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 80752ea2fb..bd3d87d931 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -68,58 +68,49 @@ class BilibiliBaseIE(InfoExtractor): f'become a premium member to download them. {self._login_hint()}') def _extract_storyboard(self, duration, aid=None, bvid=None, cid=None): - video_id = aid or bvid - query = filter_dict({ - 'aid': aid, - 'bvid': bvid, - 'cid': cid, - }) - if not aid and not bvid: + if not (video_id := aid or bvid): return {} - idx = 1 - while storyboard_info := traverse_obj(self._download_json( + if storyboard_info := traverse_obj(self._download_json( 'https://api.bilibili.com/x/player/videoshot', video_id, - 'Downloading storyboard info', query={ - 'index': idx, - **query, - }), 'data'): - if storyboard_info.get('image') and storyboard_info.get('index'): - rows, cols = storyboard_info.get('img_x_len'), storyboard_info.get('img_y_len') - fragments = [] - last_duration = 0.0 - for i, url in enumerate(storyboard_info['image'], start=1): - duration_index = i * rows * cols - 1 - if duration_index < len(storyboard_info['index']) - 1: - current_duration = traverse_obj(storyboard_info, ('index', duration_index)) - else: - current_duration = duration - if current_duration > last_duration and current_duration <= duration: - fragments.append({ - 'url': sanitize_url(url), - 'duration': current_duration - last_duration, - }) - last_duration = current_duration - else: - break - if fragments: - yield { - 'format_id': f'sb{idx}', - 'format_note': 'storyboard', - 'ext': 'mhtml', - 'protocol': 'mhtml', - 'acodec': 'none', - 'vcodec': 'none', - 'url': 'about:invalid', - 'width': storyboard_info.get('img_x_size'), - 'height': storyboard_info.get('img_y_size'), - 'fps': len(storyboard_info['image']) * rows * cols / duration, - 'rows': rows, - 'columns': cols, - 'fragments': fragments, - } - else: - return - idx += 1 + note='Downloading storyboard info', errnote='Failed to download storyboard info', + query=filter_dict({ + 'index': 1, + 'aid': aid, + 'bvid': bvid, + 'cid': cid, + })), ('data', {lambda v: v if v['image'] and v['index'] else None})): + rows, cols = traverse_obj(storyboard_info, (('img_x_len', 'img_y_len'),)) + fragments = [] + last_duration = 0.0 + for i, url in enumerate(storyboard_info['image'], start=1): + duration_index = i * rows * cols - 1 + if duration_index < len(storyboard_info['index']) - 1: + current_duration = traverse_obj(storyboard_info, ('index', duration_index)) + else: + current_duration = duration + if not current_duration or current_duration <= last_duration or current_duration > duration: + break + fragments.append({ + 'url': sanitize_url(url), + 'duration': current_duration - last_duration, + }) + if fragments: + return { + 'format_id': 'sb', + 'format_note': 'storyboard', + 'ext': 'mhtml', + 'protocol': 'mhtml', + 'acodec': 'none', + 'vcodec': 'none', + 'url': 'about:invalid', + 'width': storyboard_info.get('img_x_size'), + 'height': storyboard_info.get('img_y_size'), + 'fps': len(storyboard_info['image']) * rows * cols / duration if rows and cols else None, + 'rows': rows, + 'columns': cols, + 'fragments': fragments, + } + return {} def extract_formats(self, play_info, aid=None, bvid=None, cid=None): format_names = { @@ -183,7 +174,7 @@ class BilibiliBaseIE(InfoExtractor): }), **parse_resolution(format_names.get(play_info.get('quality'))), }) - formats.extend(self._extract_storyboard( + formats.append(self._extract_storyboard( float_or_none(play_info.get('timelength'), scale=1000), aid=aid, bvid=bvid, cid=cid)) return formats From 22e5e37c82f32e8c99596aad43ce27718c26e6b1 Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Fri, 11 Oct 2024 01:30:07 +1300 Subject: [PATCH 03/11] extract episode_info in advance --- yt_dlp/extractor/bilibili.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index bd3d87d931..e7cfac55a0 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -68,7 +68,7 @@ class BilibiliBaseIE(InfoExtractor): f'become a premium member to download them. {self._login_hint()}') def _extract_storyboard(self, duration, aid=None, bvid=None, cid=None): - if not (video_id := aid or bvid): + if not (video_id := aid or bvid) or not duration: return {} if storyboard_info := traverse_obj(self._download_json( 'https://api.bilibili.com/x/player/videoshot', video_id, @@ -913,10 +913,11 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'Extracting episode', query={'fnval': '4048', 'ep_id': episode_id}, headers=headers) premium_only = play_info.get('code') == -10403 - bvid, cid = traverse_obj(play_info, ('result', 'play_view_business_info', 'episode_info', ('bvid', 'cid'))) + episode_info = traverse_obj(play_info, ('result', 'play_view_business_info', 'episode_info')) + aid, cid = episode_info.get('aid'), episode_info.get('cid') play_info = traverse_obj(play_info, ('result', 'video_info', {dict})) or {} - formats = self.extract_formats(play_info, bvid=bvid, cid=cid) + formats = self.extract_formats(play_info, aid=aid, cid=cid) if not formats and (premium_only or '成为大会员抢先看' in webpage or '开通大会员观看' in webpage): self.raise_login_required('This video is for premium members only') @@ -927,7 +928,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): episode_number, episode_info = next(( (idx, ep) for idx, ep in enumerate(traverse_obj( bangumi_info, (('episodes', ('section', ..., 'episodes')), ..., {dict})), 1) - if str_or_none(ep.get('id')) == episode_id), (1, {})) + if str_or_none(ep.get('id')) == episode_id), (1, episode_info)) season_id = bangumi_info.get('season_id') season_number, season_title = season_id and next(( @@ -936,7 +937,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): if e.get('season_id') == season_id ), (None, None)) - aid = episode_info.get('aid') + aid, cid = episode_info.get('aid', aid), episode_info.get('cid', cid) return { 'id': episode_id, @@ -957,7 +958,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'season_id': str_or_none(season_id), 'season_number': season_number, 'duration': float_or_none(play_info.get('timelength'), scale=1000), - 'subtitles': self.extract_subtitles(episode_id, episode_info.get('cid'), aid=aid), + 'subtitles': self.extract_subtitles(episode_id, cid, aid=aid), '__post_extractor': self.extract_comments(aid), 'http_headers': {'Referer': url}, } From 7679b5241e87a2675ad238c99dad10954fe39d4f Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Fri, 11 Oct 2024 05:57:41 +1300 Subject: [PATCH 04/11] update tests --- yt_dlp/extractor/bilibili.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index e7cfac55a0..b14fe19843 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -69,7 +69,7 @@ class BilibiliBaseIE(InfoExtractor): def _extract_storyboard(self, duration, aid=None, bvid=None, cid=None): if not (video_id := aid or bvid) or not duration: - return {} + return if storyboard_info := traverse_obj(self._download_json( 'https://api.bilibili.com/x/player/videoshot', video_id, note='Downloading storyboard info', errnote='Failed to download storyboard info', @@ -110,7 +110,6 @@ class BilibiliBaseIE(InfoExtractor): 'columns': cols, 'fragments': fragments, } - return {} def extract_formats(self, play_info, aid=None, bvid=None, cid=None): format_names = { @@ -174,8 +173,9 @@ class BilibiliBaseIE(InfoExtractor): }), **parse_resolution(format_names.get(play_info.get('quality'))), }) - formats.append(self._extract_storyboard( - float_or_none(play_info.get('timelength'), scale=1000), aid=aid, bvid=bvid, cid=cid)) + if storyboard_format := self._extract_storyboard( + float_or_none(play_info.get('timelength'), scale=1000), aid=aid, bvid=bvid, cid=cid): + formats.append(storyboard_format) return formats def _get_wbi_key(self, video_id): @@ -1784,7 +1784,7 @@ class BilibiliAudioIE(BilibiliAudioBaseIE): 'id': '1003142', 'ext': 'm4a', 'title': '【tsukimi】YELLOW / 神山羊', - 'artist': 'tsukimi', + 'artists': ['tsukimi'], 'comment_count': int, 'description': 'YELLOW的mp3版!', 'duration': 183, @@ -1796,7 +1796,7 @@ class BilibiliAudioIE(BilibiliAudioBaseIE): 'thumbnail': r're:^https?://.+\.jpg', 'timestamp': 1564836614, 'upload_date': '20190803', - 'uploader': 'tsukimi-つきみぐー', + 'uploader': '十六夜tsukimiつきみぐ', 'view_count': int, }, } @@ -1851,10 +1851,10 @@ class BilibiliAudioAlbumIE(BilibiliAudioBaseIE): 'url': 'https://www.bilibili.com/audio/am10624', 'info_dict': { 'id': '10624', - 'title': '每日新曲推荐(每日11:00更新)', + 'title': '新曲推荐', 'description': '每天11:00更新,为你推送最新音乐', }, - 'playlist_count': 19, + 'playlist_mincount': 10, } def _real_extract(self, url): From 65a28bd5148f866ea385c7338ddc8c06f74efc6f Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Fri, 11 Oct 2024 06:19:04 +1300 Subject: [PATCH 05/11] add test for storyboard --- yt_dlp/extractor/bilibili.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index b14fe19843..9b31e6fc90 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -604,6 +604,27 @@ class BiliBiliIE(BilibiliBaseIE): '_old_archive_ids': ['bilibili 292734508_part1'], }, }], + }, { + 'note': 'storyboard', + 'url': 'https://www.bilibili.com/video/av170001/', + 'info_dict': { + 'id': 'BV17x411w7KC_p1', + 'title': '【MV】保加利亚妖王AZIS视频合辑 p01 Хоп', + 'ext': 'mhtml', + 'upload_date': '20111109', + 'uploader_id': '122541', + 'view_count': int, + '_old_archive_ids': ['bilibili 170001_part1'], + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', + 'uploader': '冰封.虾子', + 'timestamp': 1320850533, + 'comment_count': int, + 'tags': ['Hop', '保加利亚妖王', '保加利亚', 'Азис', 'azis', 'mv'], + 'description': 'md5:acfd7360b96547f031f7ebead9e66d9e', + 'like_count': int, + 'duration': 199.4, + }, + 'params': {'format': 'sb', 'playlist_items': '1'}, }, { 'note': '301 redirect to bangumi link', 'url': 'https://www.bilibili.com/video/BV1TE411f7f1', From a032d2b0d5fe57519505c6460e0b5c68e2efd628 Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:26:32 +1300 Subject: [PATCH 06/11] add heatmap, update tests --- yt_dlp/extractor/bilibili.py | 105 +++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 9b31e6fc90..5fa4c08894 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -67,6 +67,41 @@ class BilibiliBaseIE(InfoExtractor): f'Format(s) {missing_formats} are missing; you have to login or ' f'become a premium member to download them. {self._login_hint()}') + def _extract_heatmap(self, cid): + heatmap_json = self._download_json( + 'https://bvc.bilivideo.com/pbp/data', cid, + note='Downloading heatmap', errnote='Failed to download heatmap', fatal=False, + query={'cid': cid}) + if not isinstance(heatmap_json, dict): + return + try: + duration = self._parse_json(heatmap_json['debug'])['max_time'] + except Exception: + duration = None + step_sec = heatmap_json.get('step_sec', {int}) + heatmap_data = traverse_obj(heatmap_json, ('events', 'default', {list})) + if not step_sec or not heatmap_data: + return + peak = max(heatmap_data) + if not peak: + return + + for idx, heatmap_entry in enumerate(heatmap_data): + start_time = idx * step_sec + end_time = start_time + step_sec + if duration and end_time >= duration: + yield { + 'start_time': start_time, + 'end_time': duration, + 'value': heatmap_entry / peak, + } + break + yield { + 'start_time': start_time, + 'end_time': end_time, + 'value': heatmap_entry / peak, + } + def _extract_storyboard(self, duration, aid=None, bvid=None, cid=None): if not (video_id := aid or bvid) or not duration: return @@ -343,6 +378,7 @@ class BilibiliBaseIE(InfoExtractor): 'description': f'{json.dumps(edges, ensure_ascii=False)}\n{metainfo.get("description", "")}', 'duration': float_or_none(play_info.get('timelength'), scale=1000), 'subtitles': self.extract_subtitles(video_id, cid), + 'heatmap': list(self._extract_heatmap(cid)), } @@ -358,7 +394,7 @@ class BiliBiliIE(BilibiliBaseIE): 'description': '滴妹今天唱Closer給你聽! 有史以来,被推最多次也是最久的歌曲,其实歌词跟我原本想像差蛮多的,不过还是好听! 微博@阿滴英文', 'uploader_id': '65880958', 'uploader': '阿滴英文', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'duration': 554.117, 'tags': list, 'comment_count': int, @@ -367,6 +403,7 @@ class BiliBiliIE(BilibiliBaseIE): 'like_count': int, 'view_count': int, '_old_archive_ids': ['bilibili 8903802_part1'], + 'heatmap': [], }, }, { 'note': 'old av URL version', @@ -385,8 +422,9 @@ class BiliBiliIE(BilibiliBaseIE): 'comment_count': int, 'view_count': int, 'tags': list, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 1074402_part1'], + 'heatmap': [], }, 'params': {'skip_download': True}, }, { @@ -404,7 +442,7 @@ class BiliBiliIE(BilibiliBaseIE): 'title': '物语中的人物是如何吐槽自己的OP的 p01 Staple Stable/战场原+羽川', 'tags': 'count:10', 'timestamp': 1589601697, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'uploader': '打牌还是打桩', 'uploader_id': '150259984', 'like_count': int, @@ -414,6 +452,7 @@ class BiliBiliIE(BilibiliBaseIE): 'description': 'md5:e3c401cf7bc363118d1783dd74068a68', 'duration': 90.314, '_old_archive_ids': ['bilibili 498159642_part1'], + 'heatmap': 'count:90', }, }], }, { @@ -425,7 +464,7 @@ class BiliBiliIE(BilibiliBaseIE): 'title': '物语中的人物是如何吐槽自己的OP的 p01 Staple Stable/战场原+羽川', 'tags': 'count:10', 'timestamp': 1589601697, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'uploader': '打牌还是打桩', 'uploader_id': '150259984', 'like_count': int, @@ -435,6 +474,7 @@ class BiliBiliIE(BilibiliBaseIE): 'description': 'md5:e3c401cf7bc363118d1783dd74068a68', 'duration': 90.314, '_old_archive_ids': ['bilibili 498159642_part1'], + 'heatmap': 'count:90', }, }, { 'url': 'https://www.bilibili.com/video/av8903802/', @@ -447,13 +487,14 @@ class BiliBiliIE(BilibiliBaseIE): 'timestamp': 1488353834, 'uploader_id': '65880958', 'uploader': '阿滴英文', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'duration': 554.117, 'tags': list, 'comment_count': int, 'view_count': int, 'like_count': int, '_old_archive_ids': ['bilibili 8903802_part1'], + 'heatmap': [], }, 'params': { 'skip_download': True, @@ -476,8 +517,9 @@ class BiliBiliIE(BilibiliBaseIE): 'comment_count': int, 'view_count': int, 'like_count': int, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 463665680_part1'], + 'heatmap': 'count:96', }, 'params': {'skip_download': True}, }, { @@ -495,8 +537,9 @@ class BiliBiliIE(BilibiliBaseIE): 'uploader_id': '528182630', 'view_count': int, 'like_count': int, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 893839363_part1'], + 'heatmap': [], }, }, { 'note': 'newer festival video', @@ -513,8 +556,9 @@ class BiliBiliIE(BilibiliBaseIE): 'uploader_id': '8469526', 'view_count': int, 'like_count': int, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 778246196_part1'], + 'heatmap': 'count:93', }, }, { 'note': 'legacy flv/mp4 video', @@ -532,8 +576,9 @@ class BiliBiliIE(BilibiliBaseIE): 'comment_count': int, 'like_count': int, 'tags': list, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 4120229_part4'], + 'heatmap': [], }, 'params': {'extractor_args': {'bilibili': {'prefer_multi_flv': ['32']}}}, 'playlist_count': 19, @@ -562,8 +607,9 @@ class BiliBiliIE(BilibiliBaseIE): 'view_count': int, 'like_count': int, 'tags': list, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 15700301_part1'], + 'heatmap': [], }, }, { 'note': 'interactive/split-path video', @@ -581,7 +627,7 @@ class BiliBiliIE(BilibiliBaseIE): 'comment_count': int, 'view_count': int, 'like_count': int, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 292734508_part1'], }, 'playlist_count': 33, @@ -600,8 +646,9 @@ class BiliBiliIE(BilibiliBaseIE): 'comment_count': int, 'view_count': int, 'like_count': int, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 292734508_part1'], + 'heatmap': [], }, }], }, { @@ -623,6 +670,7 @@ class BiliBiliIE(BilibiliBaseIE): 'description': 'md5:acfd7360b96547f031f7ebead9e66d9e', 'like_count': int, 'duration': 199.4, + 'heatmap': 'count:68', }, 'params': {'format': 'sb', 'playlist_items': '1'}, }, { @@ -643,7 +691,8 @@ class BiliBiliIE(BilibiliBaseIE): 'duration': 1183.957, 'timestamp': 1571648124, 'upload_date': '20191021', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', + 'heatmap': [], }, }, { 'note': 'video has subtitles, which requires login', @@ -662,7 +711,7 @@ class BiliBiliIE(BilibiliBaseIE): 'comment_count': int, 'view_count': int, 'like_count': int, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'subtitles': 'count:2', # login required for CC subtitle '_old_archive_ids': ['bilibili 898179753_part1'], }, @@ -842,6 +891,7 @@ class BiliBiliIE(BilibiliBaseIE): '__post_extractor': self.extract_comments(aid) if idx == 0 else None, } for idx, fragment in enumerate(formats[0]['fragments'])], 'duration': float_or_none(play_info.get('timelength'), scale=1000), + 'heatmap': list(self._extract_heatmap(cid)), } else: return { @@ -851,6 +901,7 @@ class BiliBiliIE(BilibiliBaseIE): 'chapters': self._get_chapters(aid, cid), 'subtitles': self.extract_subtitles(video_id, cid), '__post_extractor': self.extract_comments(aid), + 'heatmap': list(self._extract_heatmap(cid)), } @@ -874,7 +925,8 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'duration': 1420.791, 'timestamp': 1320412200, 'upload_date': '20111104', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', + 'heatmap': 'count:96', }, }, { 'url': 'https://www.bilibili.com/bangumi/play/ep267851', @@ -893,7 +945,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'duration': 1425.256, 'timestamp': 1554566400, 'upload_date': '20190406', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', }, 'skip': 'Geo-restricted', }, { @@ -914,7 +966,8 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'duration': 1922.129, 'timestamp': 1602853860, 'upload_date': '20201016', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', + 'heatmap': 'count:97', }, }] @@ -982,6 +1035,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'subtitles': self.extract_subtitles(episode_id, cid, aid=aid), '__post_extractor': self.extract_comments(aid), 'http_headers': {'Referer': url}, + 'heatmap': list(self._extract_heatmap(cid)), } @@ -1019,7 +1073,8 @@ class BiliBiliBangumiMediaIE(BilibiliBaseIE): 'duration': 1525.777, 'timestamp': 1425074413, 'upload_date': '20150227', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', + 'heatmap': 'count:96', }, }], }] @@ -1074,7 +1129,8 @@ class BiliBiliBangumiSeasonIE(BilibiliBaseIE): 'duration': 1436.992, 'timestamp': 1343185080, 'upload_date': '20120725', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', + 'heatmap': 'count:96', }, }], }] @@ -1132,6 +1188,7 @@ class BilibiliCheeseBaseIE(BilibiliBaseIE): 'subtitles': self.extract_subtitles(ep_id, cid, aid=aid), '__post_extractor': self.extract_comments(aid), 'http_headers': self._HEADERS, + 'heatmap': list(self._extract_heatmap(cid)), } def _download_season_info(self, query_key, video_id): @@ -1157,8 +1214,9 @@ class BilibiliCheeseIE(BilibiliCheeseBaseIE): 'duration': 221, 'timestamp': 1695549606, 'upload_date': '20230924', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'view_count': int, + 'heatmap': 'count:74', }, }] @@ -1190,8 +1248,9 @@ class BilibiliCheeseSeasonIE(BilibiliCheeseBaseIE): 'duration': 221, 'timestamp': 1695549606, 'upload_date': '20230924', - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'view_count': int, + 'heatmap': 'count:74', }, }], 'params': {'playlist_items': '1'}, @@ -1563,6 +1622,7 @@ class BilibiliPlaylistIE(BilibiliSpaceListBaseIE): 'view_count': int, 'like_count': int, '_old_archive_ids': ['bilibili 687146339_part1'], + 'heatmap': [], }, 'params': {'noplaylist': True}, }, { @@ -1757,8 +1817,9 @@ class BiliBiliSearchIE(SearchInfoExtractor): 'comment_count': int, 'view_count': int, 'like_count': int, - 'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$', + 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 988222410_part1'], + 'heatmap': [], }, }], }] From 43c6c434aefa7da2fb75ba870e201df2be4aa11a Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:40:14 +1300 Subject: [PATCH 07/11] make storyboard extraction non-fatal --- yt_dlp/extractor/bilibili.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 5fa4c08894..90505029df 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -113,13 +113,15 @@ class BilibiliBaseIE(InfoExtractor): 'aid': aid, 'bvid': bvid, 'cid': cid, - })), ('data', {lambda v: v if v['image'] and v['index'] else None})): - rows, cols = traverse_obj(storyboard_info, (('img_x_len', 'img_y_len'),)) + })), ('data', {lambda v: v if v.get('image') and v.get('index') else None})): + rows, cols = storyboard_info.get('img_x_len'), storyboard_info.get('img_y_len') fragments = [] last_duration = 0.0 for i, url in enumerate(storyboard_info['image'], start=1): - duration_index = i * rows * cols - 1 - if duration_index < len(storyboard_info['index']) - 1: + if not rows or not cols: + fragments.append({'url': sanitize_url(url)}) + continue + elif (duration_index := i * rows * cols - 1) < len(storyboard_info['index']) - 1: current_duration = traverse_obj(storyboard_info, ('index', duration_index)) else: current_duration = duration @@ -127,7 +129,7 @@ class BilibiliBaseIE(InfoExtractor): break fragments.append({ 'url': sanitize_url(url), - 'duration': current_duration - last_duration, + 'duration': current_duration - last_duration if current_duration is not None else None, }) if fragments: return { From 6f5a908dffa7a0ed51998355da7126fce69718ec Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:45:03 +1300 Subject: [PATCH 08/11] fix heatmap extraction --- yt_dlp/extractor/bilibili.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 90505029df..3ac9c7cd00 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -74,11 +74,8 @@ class BilibiliBaseIE(InfoExtractor): query={'cid': cid}) if not isinstance(heatmap_json, dict): return - try: - duration = self._parse_json(heatmap_json['debug'])['max_time'] - except Exception: - duration = None - step_sec = heatmap_json.get('step_sec', {int}) + duration = self._parse_json(heatmap_json['debug']).get('max_time') + step_sec = traverse_obj(heatmap_json, ('step_sec', {int})) heatmap_data = traverse_obj(heatmap_json, ('events', 'default', {list})) if not step_sec or not heatmap_data: return From eb03632cc7fd5f27c48cff0ee76fedf7d0d598e8 Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:10:05 +1300 Subject: [PATCH 09/11] do not extract storyboard in extract_formats --- yt_dlp/extractor/bilibili.py | 39 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 3ac9c7cd00..45183a8c7a 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -42,6 +42,7 @@ from ..utils import ( unsmuggle_url, url_or_none, urlencode_postdata, + value, variadic, ) @@ -145,7 +146,7 @@ class BilibiliBaseIE(InfoExtractor): 'fragments': fragments, } - def extract_formats(self, play_info, aid=None, bvid=None, cid=None): + def extract_formats(self, play_info): format_names = { r['quality']: traverse_obj(r, 'new_description', 'display_desc') for r in traverse_obj(play_info, ('support_formats', lambda _, v: v['quality'])) @@ -207,9 +208,6 @@ class BilibiliBaseIE(InfoExtractor): }), **parse_resolution(format_names.get(play_info.get('quality'))), }) - if storyboard_format := self._extract_storyboard( - float_or_none(play_info.get('timelength'), scale=1000), aid=aid, bvid=bvid, cid=cid): - formats.append(storyboard_format) return formats def _get_wbi_key(self, video_id): @@ -369,13 +367,19 @@ class BilibiliBaseIE(InfoExtractor): cid_edges = self._get_divisions(video_id, graph_version, {1: {'cid': cid}}, 1) for cid, edges in cid_edges.items(): play_info = self._download_playinfo(video_id, cid, headers=headers) + formats = self.extract_formats(play_info) + duration = float_or_none(play_info.get('timelength'), scale=1000) + if storyboard_format := self._extract_storyboard( + duration=duration, + bvid=video_id, cid=cid): + formats.append(storyboard_format) yield { **metainfo, 'id': f'{video_id}_{cid}', 'title': f'{metainfo.get("title")} - {next(iter(edges.values())).get("title")}', - 'formats': self.extract_formats(play_info, bvid=video_id, cid=cid), + 'formats': formats, 'description': f'{json.dumps(edges, ensure_ascii=False)}\n{metainfo.get("description", "")}', - 'duration': float_or_none(play_info.get('timelength'), scale=1000), + 'duration': duration, 'subtitles': self.extract_subtitles(video_id, cid), 'heatmap': list(self._extract_heatmap(cid)), } @@ -845,14 +849,17 @@ class BiliBiliIE(BilibiliBaseIE): duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})), __post_extractor=self.extract_comments(aid)) else: - formats = self.extract_formats(play_info, bvid=video_id, cid=cid) + formats = self.extract_formats(play_info) + formats.append(self._extract_storyboard( + duration=float_or_none(play_info.get('timelength'), scale=1000), + bvid=video_id, cid=cid)) if not traverse_obj(play_info, ('dash')): # we only have legacy formats and need additional work has_qn = lambda x: x in traverse_obj(formats, (..., 'quality')) for qn in traverse_obj(play_info, ('accept_quality', lambda _, v: not has_qn(v), {int})): formats.extend(traverse_obj( - self.extract_formats(self._download_playinfo(video_id, cid, headers=headers, qn=qn), bvid=video_id, cid=cid), + self.extract_formats(self._download_playinfo(video_id, cid, headers=headers, qn=qn)), lambda _, v: not has_qn(v['quality']))) self._check_missing_formats(play_info, formats) flv_formats = traverse_obj(formats, lambda _, v: v['fragments']) @@ -990,7 +997,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): aid, cid = episode_info.get('aid'), episode_info.get('cid') play_info = traverse_obj(play_info, ('result', 'video_info', {dict})) or {} - formats = self.extract_formats(play_info, aid=aid, cid=cid) + formats = self.extract_formats(play_info) if not formats and (premium_only or '成为大会员抢先看' in webpage or '开通大会员观看' in webpage): self.raise_login_required('This video is for premium members only') @@ -1011,7 +1018,9 @@ class BiliBiliBangumiIE(BilibiliBaseIE): ), (None, None)) aid, cid = episode_info.get('aid', aid), episode_info.get('cid', cid) - + duration = float_or_none(play_info.get('timelength'), scale=1000) + if storyboard_format := self._extract_storyboard(duration=duration, aid=aid, cid=cid): + formats.append(storyboard_format) return { 'id': episode_id, 'formats': formats, @@ -1030,7 +1039,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'season': str_or_none(season_title), 'season_id': str_or_none(season_id), 'season_number': season_number, - 'duration': float_or_none(play_info.get('timelength'), scale=1000), + 'duration': duration, 'subtitles': self.extract_subtitles(episode_id, cid, aid=aid), '__post_extractor': self.extract_comments(aid), 'http_headers': {'Referer': url}, @@ -1163,10 +1172,14 @@ class BilibiliCheeseBaseIE(BilibiliBaseIE): query={'avid': aid, 'cid': cid, 'ep_id': ep_id, 'fnval': 16, 'fourk': 1}, headers=self._HEADERS, note='Downloading playinfo')['data'] + formats = self.extract_formats(play_info) + duration = traverse_obj(episode_info, ('duration', {int_or_none})) + if storyboard_format := self._extract_storyboard(duration=duration, aid=aid, cid=cid): + formats.append(storyboard_format) return { 'id': str_or_none(ep_id), 'episode_id': str_or_none(ep_id), - 'formats': self.extract_formats(play_info, aid=aid, cid=cid), + 'formats': formats, 'extractor_key': BilibiliCheeseIE.ie_key(), 'extractor': BilibiliCheeseIE.IE_NAME, 'webpage_url': f'https://www.bilibili.com/cheese/play/ep{ep_id}', @@ -1174,7 +1187,7 @@ class BilibiliCheeseBaseIE(BilibiliBaseIE): 'episode': ('title', {str}), 'title': {lambda v: v and join_nonempty('index', 'title', delim=' - ', from_dict=v)}, 'alt_title': ('subtitle', {str}), - 'duration': ('duration', {int_or_none}), + 'duration': {value(duration)}, 'episode_number': ('index', {int_or_none}), 'thumbnail': ('cover', {url_or_none}), 'timestamp': ('release_date', {int_or_none}), From 8631ff86d93f6fe8fb43580c71d772a3d1b30c6b Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:50:34 +1300 Subject: [PATCH 10/11] fix: _parse_json needs `video_id` --- yt_dlp/extractor/bilibili.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 45183a8c7a..41684751ab 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -75,7 +75,7 @@ class BilibiliBaseIE(InfoExtractor): query={'cid': cid}) if not isinstance(heatmap_json, dict): return - duration = self._parse_json(heatmap_json['debug']).get('max_time') + duration = self._parse_json(heatmap_json['debug'], cid).get('max_time') step_sec = traverse_obj(heatmap_json, ('step_sec', {int})) heatmap_data = traverse_obj(heatmap_json, ('events', 'default', {list})) if not step_sec or not heatmap_data: @@ -455,7 +455,7 @@ class BiliBiliIE(BilibiliBaseIE): 'description': 'md5:e3c401cf7bc363118d1783dd74068a68', 'duration': 90.314, '_old_archive_ids': ['bilibili 498159642_part1'], - 'heatmap': 'count:90', + 'heatmap': list, }, }], }, { @@ -477,7 +477,7 @@ class BiliBiliIE(BilibiliBaseIE): 'description': 'md5:e3c401cf7bc363118d1783dd74068a68', 'duration': 90.314, '_old_archive_ids': ['bilibili 498159642_part1'], - 'heatmap': 'count:90', + 'heatmap': list, }, }, { 'url': 'https://www.bilibili.com/video/av8903802/', @@ -522,7 +522,7 @@ class BiliBiliIE(BilibiliBaseIE): 'like_count': int, 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 463665680_part1'], - 'heatmap': 'count:96', + 'heatmap': list, }, 'params': {'skip_download': True}, }, { @@ -561,7 +561,7 @@ class BiliBiliIE(BilibiliBaseIE): 'like_count': int, 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', '_old_archive_ids': ['bilibili 778246196_part1'], - 'heatmap': 'count:93', + 'heatmap': list, }, }, { 'note': 'legacy flv/mp4 video', @@ -673,7 +673,7 @@ class BiliBiliIE(BilibiliBaseIE): 'description': 'md5:acfd7360b96547f031f7ebead9e66d9e', 'like_count': int, 'duration': 199.4, - 'heatmap': 'count:68', + 'heatmap': list, }, 'params': {'format': 'sb', 'playlist_items': '1'}, }, { @@ -932,7 +932,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'timestamp': 1320412200, 'upload_date': '20111104', 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', - 'heatmap': 'count:96', + 'heatmap': list, }, }, { 'url': 'https://www.bilibili.com/bangumi/play/ep267851', @@ -973,7 +973,7 @@ class BiliBiliBangumiIE(BilibiliBaseIE): 'timestamp': 1602853860, 'upload_date': '20201016', 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', - 'heatmap': 'count:97', + 'heatmap': list, }, }] @@ -1082,7 +1082,7 @@ class BiliBiliBangumiMediaIE(BilibiliBaseIE): 'timestamp': 1425074413, 'upload_date': '20150227', 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', - 'heatmap': 'count:96', + 'heatmap': list, }, }], }] @@ -1138,7 +1138,7 @@ class BiliBiliBangumiSeasonIE(BilibiliBaseIE): 'timestamp': 1343185080, 'upload_date': '20120725', 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', - 'heatmap': 'count:96', + 'heatmap': list, }, }], }] @@ -1228,7 +1228,7 @@ class BilibiliCheeseIE(BilibiliCheeseBaseIE): 'upload_date': '20230924', 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'view_count': int, - 'heatmap': 'count:74', + 'heatmap': list, }, }] @@ -1262,7 +1262,7 @@ class BilibiliCheeseSeasonIE(BilibiliCheeseBaseIE): 'upload_date': '20230924', 'thumbnail': r're:https?://.*\.(?:jpg|jpeg|png)$', 'view_count': int, - 'heatmap': 'count:74', + 'heatmap': list, }, }], 'params': {'playlist_items': '1'}, From fe0541845dd455e4e0ee15d91c6f663d17ece439 Mon Sep 17 00:00:00 2001 From: grqx_wsl <173253225+grqx@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:36:08 +1300 Subject: [PATCH 11/11] extract storyboard formats after the flv check --- yt_dlp/extractor/bilibili.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yt_dlp/extractor/bilibili.py b/yt_dlp/extractor/bilibili.py index 0c4b888acc..242c332b5c 100644 --- a/yt_dlp/extractor/bilibili.py +++ b/yt_dlp/extractor/bilibili.py @@ -857,10 +857,6 @@ class BiliBiliIE(BilibiliBaseIE): f'This is a supporter-only video, only the preview will be extracted: {msg}', video_id=video_id) - formats.append(self._extract_storyboard( - duration=float_or_none(play_info.get('timelength'), scale=1000), - bvid=video_id, cid=cid)) - if not traverse_obj(play_info, 'dash'): # we only have legacy formats and need additional work has_qn = lambda x: x in traverse_obj(formats, (..., 'quality')) @@ -906,6 +902,10 @@ class BiliBiliIE(BilibiliBaseIE): 'duration': float_or_none(play_info.get('timelength'), scale=1000), } + formats.append(self._extract_storyboard( + duration=float_or_none(play_info.get('timelength'), scale=1000), + bvid=video_id, cid=cid)) + return { **metainfo, 'formats': formats,