From 7d4573f84ee08234dd8049f64b0a77ed8be1c9d7 Mon Sep 17 00:00:00 2001 From: Loker Date: Tue, 26 May 2026 17:13:28 +0800 Subject: [PATCH 1/5] fix(youtube): support shorts urls --- backend/app/utils/url_parser.py | 4 +- backend/app/validators/video_url_validator.py | 2 +- backend/tests/test_video_url_support.py | 50 +++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_video_url_support.py diff --git a/backend/app/utils/url_parser.py b/backend/app/utils/url_parser.py index 8f76a16..3cbf38e 100644 --- a/backend/app/utils/url_parser.py +++ b/backend/app/utils/url_parser.py @@ -23,8 +23,8 @@ def extract_video_id(url: str, platform: str) -> Optional[str]: return f"BV{match.group(1)}" if match else None elif platform == "youtube": - # 匹配 v=xxxxx 或 youtu.be/xxxxx,ID 长度通常为 11 - match = re.search(r"(?:v=|youtu\.be/)([0-9A-Za-z_-]{11})", url) + # 匹配 v=xxxxx、youtu.be/xxxxx 或 shorts/xxxxx,ID 长度通常为 11 + match = re.search(r"(?:v=|youtu\.be/|shorts/)([0-9A-Za-z_-]{11})", url) return match.group(1) if match else None elif platform == "douyin": diff --git a/backend/app/validators/video_url_validator.py b/backend/app/validators/video_url_validator.py index 205f786..cf0a240 100644 --- a/backend/app/validators/video_url_validator.py +++ b/backend/app/validators/video_url_validator.py @@ -4,7 +4,7 @@ from urllib.parse import urlparse SUPPORTED_PLATFORMS = { "bilibili": r"(https?://)?(www\.)?bilibili\.com/video/[a-zA-Z0-9]+", - "youtube": r"(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)[\w\-]+", + "youtube": r"(https?://)?(www\.)?(youtube\.com/(watch\?v=|shorts/)|youtu\.be/)[\w\-]+", "douyin": "douyin", "kuaishou": "kuaishou" } diff --git a/backend/tests/test_video_url_support.py b/backend/tests/test_video_url_support.py new file mode 100644 index 0000000..b402127 --- /dev/null +++ b/backend/tests/test_video_url_support.py @@ -0,0 +1,50 @@ +import importlib.util +import pathlib +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def _load_module(name, relative_path): + module_path = ROOT / relative_path + spec = importlib.util.spec_from_file_location(name, module_path) + if spec is None or spec.loader is None: + raise ImportError(f"{name} module spec not found") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +url_parser = _load_module("url_parser", pathlib.Path("app") / "utils" / "url_parser.py") +video_url_validator = _load_module( + "video_url_validator", + pathlib.Path("app") / "validators" / "video_url_validator.py", +) + + +class TestVideoUrlSupport(unittest.TestCase): + def test_extract_youtube_video_id_from_supported_url_shapes(self): + expected_id = "dQw4w9WgXcQ" + + cases = [ + f"https://www.youtube.com/watch?v={expected_id}", + f"https://youtu.be/{expected_id}", + f"https://www.youtube.com/shorts/{expected_id}", + ] + + for url in cases: + with self.subTest(url=url): + self.assertEqual( + url_parser.extract_video_id(url, "youtube"), + expected_id, + ) + + def test_accepts_youtube_shorts_url(self): + url = "https://www.youtube.com/shorts/dQw4w9WgXcQ" + + self.assertTrue(video_url_validator.is_supported_video_url(url)) + + +if __name__ == "__main__": + unittest.main() From 2ba409880e90fe465575bddb19209509f82f26c0 Mon Sep 17 00:00:00 2001 From: wmsdsb137 <3457346173@qq.com> Date: Thu, 11 Jun 2026 23:00:12 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(bilibili):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=88=86P=E8=A7=86=E9=A2=91=E5=AD=97=E5=B9=95=E4=BC=98?= =?UTF-8?q?=E5=85=88=E9=93=BE=E8=B7=AF=E6=9C=AA=E4=BC=A0p=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BC=E8=87=B4=E5=8F=96=E9=94=99=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:B站分P视频(如62集课程),提交?p=36链接时, 字幕优先链路通过x/web-interface/view API拿cid时未传p参数, 默认取第1集cid,导致生成的是第1集的笔记。 同时yt-dlp正确下载了p36音频,但被跳过。 修复: - url_parser新增extract_bilibili_p_number()提取URL中的p参数 - bilibili_subtitle的_get_cid()接收p参数,从data.pages[p-1]取对应分P的cid - fetch_subtitles()调用extract_bilibili_p_number()透传p --- backend/app/downloaders/bilibili_subtitle.py | 43 +++++++++++++++----- backend/app/utils/url_parser.py | 33 ++++++++++++++- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/backend/app/downloaders/bilibili_subtitle.py b/backend/app/downloaders/bilibili_subtitle.py index 9f3790e..b1cbe49 100644 --- a/backend/app/downloaders/bilibili_subtitle.py +++ b/backend/app/downloaders/bilibili_subtitle.py @@ -3,12 +3,13 @@ 流程: 1. 从 URL 提 BV id(已有 utils.url_parser.extract_video_id) -2. GET /x/web-interface/view?bvid=BVxxx → 拿 cid -3. GET /x/player/wbi/v2?bvid=...&cid=... → 返回 data.subtitle.subtitles[] +2. 从 URL 提 p 参数(分 P 序号,已有 utils.url_parser.extract_bilibili_p_number) +3. GET /x/web-interface/view?bvid=BVxxx&p=N → 拿第 N 集的 cid +4. GET /x/player/wbi/v2?bvid=...&cid=... → 返回 data.subtitle.subtitles[] 每条带 subtitle_url(B 站后端已经签好 auth_key 的完整地址) -4. 按优先级(人工 zh-CN > AI zh-CN > 任意 zh > 任意非空)选一条 -5. fetch subtitle_url → JSON {body:[{from,to,content,...}]} -6. 解析为 TranscriptResult +5. 按优先级(人工 zh-CN > AI zh-CN > 任意 zh > 任意非空)选一条 +6. fetch subtitle_url → JSON {body:[{from,to,content,...}]} +7. 解析为 TranscriptResult AI 字幕需要登录态 cookie(SESSDATA);通过 CookieConfigManager 注入。 """ @@ -20,7 +21,7 @@ import requests from app.models.transcriber_model import TranscriptResult, TranscriptSegment from app.services.cookie_manager import CookieConfigManager from app.utils.logger import get_logger -from app.utils.url_parser import extract_video_id +from app.utils.url_parser import extract_video_id, extract_bilibili_p_number logger = get_logger(__name__) @@ -45,10 +46,13 @@ class BilibiliSubtitleFetcher: h["Cookie"] = self._cookie return h - def _get_cid(self, bvid: str) -> Optional[int]: + def _get_cid(self, bvid: str, p: Optional[int] = None) -> Optional[int]: url = "https://api.bilibili.com/x/web-interface/view" + params = {"bvid": bvid} + if p is not None and p >= 1: + params["p"] = p try: - resp = requests.get(url, params={"bvid": bvid}, headers=self._headers(), timeout=10) + resp = requests.get(url, params=params, headers=self._headers(), timeout=10) data = resp.json() except Exception as e: logger.warning(f"获取 cid 失败: {e}") @@ -56,6 +60,19 @@ class BilibiliSubtitleFetcher: if data.get("code") != 0: logger.warning(f"view API 返回错误: code={data.get('code')}, msg={data.get('message')}") return None + # 分 P 视频:data.pages[N-1] 对应第 N 集 + pages = data.get("data", {}).get("pages", []) + if pages: + if p is not None and 1 <= p <= len(pages): + cid = pages[p - 1].get("cid") + logger.info(f"分 P 视频: bvid={bvid} p={p} 共 {len(pages)} 集, 取第 {p} 集 cid={cid}") + return int(cid) if cid else None + else: + # 没有 p 参数或 p 超出范围,取第 1 集 + cid = pages[0].get("cid") + logger.info(f"非分 P 或 p 无效: bvid={bvid} 取第 1 集 cid={cid}") + return int(cid) if cid else None + # 单集视频 cid = data.get("data", {}).get("cid") return int(cid) if cid else None @@ -114,9 +131,12 @@ class BilibiliSubtitleFetcher: logger.info("无法从 URL 提取 BV id") return None - cid = self._get_cid(bvid) + # 提取分 P 序号 + p = extract_bilibili_p_number(video_url) + + cid = self._get_cid(bvid, p) if not cid: - logger.info(f"{bvid} 没有取到 cid") + logger.info(f"{bvid} (p={p}) 没有取到 cid") return None subtitles = self._list_subtitles(bvid, cid) @@ -149,7 +169,7 @@ class BilibiliSubtitleFetcher: return None full_text = " ".join(s.text for s in segments) - logger.info(f"B站直拉字幕成功: {bvid} lan={lan} 共 {len(segments)} 段") + logger.info(f"B站直拉字幕成功: {bvid} p={p} lan={lan} 共 {len(segments)} 段") return TranscriptResult( language=lan, full_text=full_text, @@ -158,6 +178,7 @@ class BilibiliSubtitleFetcher: "source": "bilibili_player_api", "bvid": bvid, "cid": cid, + "p": p, "lan": lan, "ai_type": track.get("ai_type"), }, diff --git a/backend/app/utils/url_parser.py b/backend/app/utils/url_parser.py index 8f76a16..833b554 100644 --- a/backend/app/utils/url_parser.py +++ b/backend/app/utils/url_parser.py @@ -1,5 +1,5 @@ import re -from typing import Optional +from typing import Optional, Tuple import requests @@ -48,3 +48,34 @@ def resolve_bilibili_short_url(short_url: str) -> Optional[str]: except requests.RequestException as e: print(f"Error resolving short URL: {e}") return None + + +def extract_bilibili_p_number(url: str) -> Optional[int]: + """ + 从 B 站分 P 视频 URL 中提取 p 参数(分 P 序号)。 + + 支持格式: + - https://www.bilibili.com/video/BVxxx/?p=36 + - https://www.bilibili.com/video/BVxxx?p=5 + - https://b23.tv/xxxxx?p=10 + - https://www.bilibili.com/video/BVxxx/pN (尾缀形式) + + :param url: B 站视频链接 + :return: 分 P 序号(从 1 开始),非分 P 视频返回 None + """ + if "b23.tv" in url: + url = resolve_bilibili_short_url(url) or url + + # 匹配 ?p=NNN 或 &p=NNN + match = re.search(r'[?&]p=(\d+)', url) + if match: + p = int(match.group(1)) + if p >= 1: + return p + + # 匹配 /pN 尾缀形式(较少见) + match = re.search(r'/p(\d+)(?:/?$|\?|&)', url) + if match: + return int(match.group(1)) + + return None From ab9ca6a0267da622225be02aaa6cd54815bbbb7c Mon Sep 17 00:00:00 2001 From: wmsdsb137 <3457346173@qq.com> Date: Tue, 16 Jun 2026 21:01:27 +0800 Subject: [PATCH 3/5] fix: address Copilot review suggestions - Remove unused Tuple import - Validate /pN suffix p >= 1 - Reuse resolve_bilibili_short_url in fetch_subtitles - Deduplicate short URL resolution --- backend/app/downloaders/bilibili_subtitle.py | 6 +++++- backend/app/utils/url_parser.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/app/downloaders/bilibili_subtitle.py b/backend/app/downloaders/bilibili_subtitle.py index b1cbe49..b59b5ee 100644 --- a/backend/app/downloaders/bilibili_subtitle.py +++ b/backend/app/downloaders/bilibili_subtitle.py @@ -21,7 +21,7 @@ import requests from app.models.transcriber_model import TranscriptResult, TranscriptSegment from app.services.cookie_manager import CookieConfigManager from app.utils.logger import get_logger -from app.utils.url_parser import extract_video_id, extract_bilibili_p_number +from app.utils.url_parser import extract_video_id, extract_bilibili_p_number, resolve_bilibili_short_url logger = get_logger(__name__) @@ -126,6 +126,10 @@ class BilibiliSubtitleFetcher: return None def fetch_subtitles(self, video_url: str) -> Optional[TranscriptResult]: + # 统一 resolve 短链,避免 extract_video_id 和 extract_bilibili_p_number 各 resolve 一次 + if "b23.tv" in video_url: + video_url = resolve_bilibili_short_url(video_url) or video_url + bvid = extract_video_id(video_url, "bilibili") if not bvid: logger.info("无法从 URL 提取 BV id") diff --git a/backend/app/utils/url_parser.py b/backend/app/utils/url_parser.py index 833b554..12f8e1d 100644 --- a/backend/app/utils/url_parser.py +++ b/backend/app/utils/url_parser.py @@ -1,5 +1,5 @@ import re -from typing import Optional, Tuple +from typing import Optional import requests @@ -76,6 +76,8 @@ def extract_bilibili_p_number(url: str) -> Optional[int]: # 匹配 /pN 尾缀形式(较少见) match = re.search(r'/p(\d+)(?:/?$|\?|&)', url) if match: - return int(match.group(1)) + p_val = int(match.group(1)) + if p_val >= 1: + return p_val return None From f79dc612fbe222854130cd8a036fb35cd47b41cc Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Wed, 17 Jun 2026 09:55:54 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(bilibili):=20=E6=B3=A8=E5=85=A5=20dm=5F?= =?UTF-8?q?img=20=E9=A3=8E=E6=8E=A7=E5=8F=82=E6=95=B0=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?wbi/playurl=20412?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B 站 wbi/playurl 网关新增 dm_img_list/dm_img_str/dm_cover_img_str/ dm_img_inter + web_location 风控校验,缺失即返回 HTTP 412。对于网页不内嵌 playinfo、必须走 API 的视频(如 BV1X9L16oEgB),yt-dlp(含最新版)尚未适配, 导致下载失败,且刷新 cookie 无效。 通过猴补丁在 BilibiliBaseIE._download_playinfo 的 wbi 签名前注入哑值 dm_img 参数(取值形态对齐 yt-dlp 自身在 arc/search 端点的用法),即可恢复 200。 已验证补丁对固定版 2025.03.31 与最新 2026.06.09 签名一致、向前兼容;新增 4 个单元测试。 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/downloaders/bilibili_dm_patch.py | 71 ++++++++++++++ .../app/downloaders/bilibili_downloader.py | 6 ++ backend/tests/test_bilibili_dm_patch.py | 94 +++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 backend/app/downloaders/bilibili_dm_patch.py create mode 100644 backend/tests/test_bilibili_dm_patch.py diff --git a/backend/app/downloaders/bilibili_dm_patch.py b/backend/app/downloaders/bilibili_dm_patch.py new file mode 100644 index 0000000..cc4d2d6 --- /dev/null +++ b/backend/app/downloaders/bilibili_dm_patch.py @@ -0,0 +1,71 @@ +""" +Patch yt-dlp's Bilibili extractor to inject the dm_img_* / web_location +risk-control parameters required by Bilibili's wbi/playurl gateway. + +Background +---------- +Around 2026-06 Bilibili's ``x/player/wbi/playurl`` gateway began rejecting +requests that omit the browser fingerprint params +``dm_img_list`` / ``dm_img_str`` / ``dm_cover_img_str`` / ``dm_img_inter`` + +``web_location`` with **HTTP 412**. Current yt-dlp (incl. the latest release) +does not send these for the playurl endpoint, so any video whose web page does +*not* inline ``playinfo`` — forcing yt-dlp onto the API path — fails with 412. +Refreshing cookies does not help; the params themselves are missing. + +We inject dummy-but-well-formed values *before* wbi signing. The value shapes +deliberately mirror yt-dlp's own usage of the same fields for the +``x/space/wbi/arc/search`` endpoint (``BiliBiliSpaceIE``), which is the only +place upstream currently sends them. +""" +import base64 +import logging +import random +import string + +logger = logging.getLogger(__name__) + + +def build_dm_img_params() -> dict: + """Return dummy ``dm_img_*`` / ``web_location`` params the gateway expects.""" + return { + 'web_location': 1550101, + 'dm_img_list': '[]', + 'dm_img_str': base64.b64encode( + ''.join(random.choices(string.printable, k=random.randint(16, 64))).encode() + )[:-2].decode(), + 'dm_cover_img_str': base64.b64encode( + ''.join(random.choices(string.printable, k=random.randint(32, 128))).encode() + )[:-2].decode(), + 'dm_img_inter': '{"ds":[],"wh":[6093,6631,31],"of":[430,760,380]}', + } + + +def apply_bilibili_dm_img_patch() -> bool: + """ + Monkey-patch ``BilibiliBaseIE._download_playinfo`` to inject dm_img params. + + Idempotent and defensive: returns ``True`` if the patch is in place (whether + applied now or previously), ``False`` if yt-dlp's internals could not be + patched (logged, never raised — the caller stays functional). + """ + try: + from yt_dlp.extractor.bilibili import BilibiliBaseIE + except Exception as e: # yt-dlp missing or module layout changed upstream + logger.warning("Bilibili dm_img patch skipped, cannot import extractor: %s", e) + return False + + original = BilibiliBaseIE._download_playinfo + if getattr(original, '_bili_dm_patched', False): + return True + + def _patched_download_playinfo(self, bvid, cid, headers=None, query=None): + # dm_* are merged into the query that the original method signs via + # _sign_wbi; caller-supplied query params (e.g. try_look/qn) take + # precedence over the injected dummies. + merged_query = {**build_dm_img_params(), **(query or {})} + return original(self, bvid, cid, headers=headers, query=merged_query) + + _patched_download_playinfo._bili_dm_patched = True + BilibiliBaseIE._download_playinfo = _patched_download_playinfo + logger.info("Applied Bilibili wbi/playurl dm_img patch to yt-dlp BilibiliBaseIE") + return True diff --git a/backend/app/downloaders/bilibili_downloader.py b/backend/app/downloaders/bilibili_downloader.py index 0a94849..95f8e5f 100644 --- a/backend/app/downloaders/bilibili_downloader.py +++ b/backend/app/downloaders/bilibili_downloader.py @@ -8,6 +8,7 @@ from typing import Union, Optional, List import yt_dlp from app.downloaders.base import Downloader, DownloadQuality, QUALITY_MAP +from app.downloaders.bilibili_dm_patch import apply_bilibili_dm_img_patch from app.downloaders.bilibili_subtitle import BilibiliSubtitleFetcher from app.models.notes_model import AudioDownloadResult from app.models.transcriber_model import TranscriptResult, TranscriptSegment @@ -17,6 +18,11 @@ from app.services.cookie_manager import CookieConfigManager logger = logging.getLogger(__name__) +# Inject the dm_img_* / web_location risk-control params Bilibili's wbi/playurl +# gateway now requires; without them the API path returns HTTP 412. See +# app/downloaders/bilibili_dm_patch.py for details. +apply_bilibili_dm_img_patch() + class BilibiliDownloader(Downloader, ABC): def __init__(self): diff --git a/backend/tests/test_bilibili_dm_patch.py b/backend/tests/test_bilibili_dm_patch.py new file mode 100644 index 0000000..3731aa1 --- /dev/null +++ b/backend/tests/test_bilibili_dm_patch.py @@ -0,0 +1,94 @@ +""" +TDD coverage for the Bilibili wbi/playurl dm_img risk-control patch. + +Background: around 2026-06, Bilibili's `x/player/wbi/playurl` gateway began +rejecting requests that omit the browser fingerprint params +(dm_img_list / dm_img_str / dm_cover_img_str / dm_img_inter + web_location) +with HTTP 412. yt-dlp (incl. latest) does not yet send these for playurl, so +videos whose web page does not inline playinfo (forcing the API call) fail. + +These tests verify our yt-dlp monkey-patch injects those params *before* wbi +signing, and that caller-supplied query params still win. +""" +import importlib.util +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +MODULE_PATH = ROOT / "app" / "downloaders" / "bilibili_dm_patch.py" +spec = importlib.util.spec_from_file_location("bilibili_dm_patch", MODULE_PATH) +if spec is None or spec.loader is None: + raise ImportError("bilibili_dm_patch module spec not found") +bilibili_dm_patch = importlib.util.module_from_spec(spec) +spec.loader.exec_module(bilibili_dm_patch) + +REQUIRED_KEYS = { + "web_location", + "dm_img_list", + "dm_img_str", + "dm_cover_img_str", + "dm_img_inter", +} + + +class BuildDmImgParamsTest(unittest.TestCase): + def test_contains_all_required_risk_control_keys(self): + params = bilibili_dm_patch.build_dm_img_params() + self.assertTrue(REQUIRED_KEYS.issubset(params.keys())) + + def test_web_location_is_expected_sentinel(self): + self.assertEqual(bilibili_dm_patch.build_dm_img_params()["web_location"], 1550101) + + +class ApplyPatchTest(unittest.TestCase): + def setUp(self): + try: + import yt_dlp.extractor.bilibili # noqa: F401 + except Exception as exc: # pragma: no cover - env without yt-dlp + self.skipTest(f"yt-dlp not importable: {exc}") + + def test_patch_is_idempotent(self): + from yt_dlp.extractor.bilibili import BilibiliBaseIE + + self.assertTrue(bilibili_dm_patch.apply_bilibili_dm_img_patch()) + first = BilibiliBaseIE._download_playinfo + self.assertTrue(bilibili_dm_patch.apply_bilibili_dm_img_patch()) + self.assertIs(BilibiliBaseIE._download_playinfo, first) + + def test_dm_params_reach_wbi_signing_with_caller_query_preserved(self): + from yt_dlp import YoutubeDL + from yt_dlp.extractor.bilibili import BilibiliBaseIE + + bilibili_dm_patch.apply_bilibili_dm_img_patch() + + captured = {} + + def fake_sign_wbi(params, video_id): + # Capture the exact params handed to wbi signing (just before the + # HTTP request). dm_* must already be present here, pre-signature. + captured.update(params) + return params + + def fake_download_json(url, video_id, **kwargs): + # Avoid any network; the real playurl call would 412 without dm_*. + return {"data": {"ok": True}} + + ie = BilibiliBaseIE(YoutubeDL({"quiet": True})) + ie._sign_wbi = fake_sign_wbi + ie._download_json = fake_download_json + + ie._download_playinfo("BV1X9L16oEgB", 4242, headers={}, query={"qn": 64}) + + self.assertTrue( + REQUIRED_KEYS.issubset(captured.keys()), + f"missing dm_* keys, got: {sorted(captured)}", + ) + self.assertEqual(captured["web_location"], 1550101) + # caller-supplied query must survive the merge + self.assertEqual(captured["qn"], 64) + # the original method still builds its base params + self.assertEqual(captured["bvid"], "BV1X9L16oEgB") + + +if __name__ == "__main__": + unittest.main() From 67486c4d6684d81d8b346eea69e40eca59e0b44d Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Wed, 17 Jun 2026 10:00:34 +0800 Subject: [PATCH 5/5] chore(release): 2.4.1 --- BillNote_frontend/src-tauri/tauri.conf.json | 2 +- CHANGELOG.md | 11 +++++++++++ README.md | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/BillNote_frontend/src-tauri/tauri.conf.json b/BillNote_frontend/src-tauri/tauri.conf.json index 43ad17c..be18719 100644 --- a/BillNote_frontend/src-tauri/tauri.conf.json +++ b/BillNote_frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "BiliNote", - "version": "2.4.0", + "version": "2.4.1", "identifier": "com.jefferyhuang.bilinote", "build": { "frontendDist": "../dist", diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b3312..7340bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ 本项目所有重要变更记录于此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [2.4.1] - 2026-06-17 + +### Added + +- **YouTube Shorts 链接支持**:后端 URL 校验与 video id 提取支持 `youtube.com/shorts/` 形态,Shorts 链接可正常提交生成笔记(#381)。 + +### Fixed + +- **B 站 412(wbi/playurl 风控)**:B 站 `x/player/wbi/playurl` 网关新增 `dm_img_list`/`dm_img_str`/`dm_cover_img_str`/`dm_img_inter` + `web_location` 风控校验,缺失即返回 HTTP 412。多数视频网页内嵌 playinfo、yt-dlp 不调此 API;而网页不内嵌 playinfo、必须走 API 的视频(如 BV1X9L16oEgB)会撞上风控,刷新 cookie 无效、yt-dlp(含最新版)上游尚未适配。现于 wbi 签名前注入哑值 dm_img 风控参数(形态对齐 yt-dlp 自身 arc/search 用法)恢复 200(#410)。 +- **B 站分 P 视频字幕取错集**:分 P 视频提交 `?p=N` 时,字幕优先链路未透传 p 参数,始终取第 1 集 cid,导致笔记内容与实际下载的 p=N 音频不一致。现从 `data.pages[N-1]` 取对应分 P 的 cid(#409)。 + ## [2.4.0] - 2026-06-07 ### Added diff --git a/README.md b/README.md index 1b248d5..8618250 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

BiliNote Banner

-

BiliNote v2.4.0

+

BiliNote v2.4.1

AI 视频笔记生成工具 让 AI 为你的视频做笔记