mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-08-28 19:46:57 +08:00
Merge pull request #421 from pumpkinperson996/fix/normalize-bilibili-url
fix: 规范化含 BV 号的任意 B 站链接(稍后再看/收藏夹/追踪参数)
This commit is contained in:
@@ -7,7 +7,7 @@ from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks, UploadFile, File
|
||||
from pydantic import BaseModel, validator, field_validator
|
||||
from pydantic import BaseModel, validator, field_validator, model_validator
|
||||
from dataclasses import asdict
|
||||
|
||||
from app.db.video_task_dao import get_task_by_video
|
||||
@@ -17,7 +17,7 @@ from app.exceptions.note import NoteError
|
||||
from app.services.note import NoteGenerator, logger
|
||||
from app.services.task_serial_executor import task_serial_executor
|
||||
from app.utils.response import ResponseWrapper as R
|
||||
from app.utils.url_parser import extract_video_id
|
||||
from app.utils.url_parser import extract_video_id, normalize_video_url
|
||||
from app.validators.video_url_validator import is_supported_video_url
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
@@ -55,6 +55,15 @@ class VideoRequest(BaseModel):
|
||||
# {"language": "zh", "full_text": "...", "segments": [{"start","end","text"}, ...]}
|
||||
prefetched_transcript: Optional[dict] = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def normalize_url(cls, data):
|
||||
# 稍后再看/收藏夹/带追踪参数的 B 站链接先规范化成标准 /video/BVxxx 形式,
|
||||
# 后续校验和 yt-dlp 下载拿到的都是干净链接
|
||||
if isinstance(data, dict) and data.get("platform") == "bilibili" and data.get("video_url"):
|
||||
data["video_url"] = normalize_video_url(str(data["video_url"]))
|
||||
return data
|
||||
|
||||
@field_validator("video_url")
|
||||
def validate_supported_url(cls, v):
|
||||
url = str(v)
|
||||
|
||||
@@ -35,6 +35,29 @@ def extract_video_id(url: str, platform: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def normalize_video_url(url: str) -> str:
|
||||
"""
|
||||
将任意包含 BV 号的 B 站链接规范化为标准视频链接。
|
||||
|
||||
支持稍后再看(/list/watchlater/?bvid=BV...)、收藏夹播放页(/list/mlXXX?bvid=BV...)、
|
||||
带追踪参数的分享链接等。保留分 P 参数,丢弃其余查询参数。
|
||||
|
||||
b23.tv 短链与无 BV 号的链接原样返回(后者交由校验器拒绝)。
|
||||
"""
|
||||
if "b23.tv" in url:
|
||||
return url
|
||||
|
||||
match = re.search(r"BV([0-9A-Za-z]+)", url)
|
||||
if not match:
|
||||
return url
|
||||
|
||||
normalized = f"https://www.bilibili.com/video/BV{match.group(1)}"
|
||||
p = extract_bilibili_p_number(url)
|
||||
if p:
|
||||
normalized += f"?p={p}"
|
||||
return normalized
|
||||
|
||||
|
||||
def resolve_bilibili_short_url(short_url: str) -> Optional[str]:
|
||||
"""
|
||||
解析哔哩哔哩短链接以获取真实视频链接
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from pydantic import AnyUrl, validator, BaseModel, field_validator
|
||||
from pydantic import AnyUrl, validator, BaseModel, field_validator, model_validator
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from app.utils.url_parser import normalize_video_url
|
||||
|
||||
SUPPORTED_PLATFORMS = {
|
||||
"bilibili": r"(https?://)?(www\.)?bilibili\.com/video/[a-zA-Z0-9]+",
|
||||
"youtube": r"(https?://)?(www\.)?(youtube\.com/(watch\?v=|shorts/)|youtu\.be/)[\w\-]+",
|
||||
@@ -31,6 +33,13 @@ class VideoRequest(BaseModel):
|
||||
url: AnyUrl
|
||||
platform: str
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def normalize_url(cls, data):
|
||||
if isinstance(data, dict) and data.get("platform") == "bilibili" and data.get("url"):
|
||||
data["url"] = normalize_video_url(str(data["url"]))
|
||||
return data
|
||||
|
||||
@field_validator("url")
|
||||
def validate_video_url(cls, v):
|
||||
if not is_supported_video_url(str(v)):
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import pytest
|
||||
|
||||
from app.utils.url_parser import normalize_video_url
|
||||
from app.validators.video_url_validator import VideoRequest
|
||||
|
||||
|
||||
def test_watchlater_url():
|
||||
url = ("https://www.bilibili.com/list/watchlater/?bvid=BV1CPXpBYEui"
|
||||
"&oid=116294762371214&spm_id_from=333.881.0.0&vd_source=abc")
|
||||
assert normalize_video_url(url) == "https://www.bilibili.com/video/BV1CPXpBYEui"
|
||||
|
||||
|
||||
def test_favlist_url():
|
||||
url = "https://www.bilibili.com/list/ml123456?bvid=BV1xx411c7mD&oid=987"
|
||||
assert normalize_video_url(url) == "https://www.bilibili.com/video/BV1xx411c7mD"
|
||||
|
||||
|
||||
def test_tracking_params_stripped():
|
||||
url = "https://www.bilibili.com/video/BV1xx411c7mD/?spm_id_from=333.881&vd_source=abc"
|
||||
assert normalize_video_url(url) == "https://www.bilibili.com/video/BV1xx411c7mD"
|
||||
|
||||
|
||||
def test_p_number_preserved():
|
||||
url = "https://www.bilibili.com/video/BV1xx411c7mD?p=36&spm_id_from=333.881"
|
||||
assert normalize_video_url(url) == "https://www.bilibili.com/video/BV1xx411c7mD?p=36"
|
||||
|
||||
|
||||
def test_no_bv_returned_unchanged():
|
||||
url = "https://www.bilibili.com/anime/timeline"
|
||||
assert normalize_video_url(url) == url
|
||||
|
||||
|
||||
def test_b23_short_url_unchanged():
|
||||
url = "https://b23.tv/abc123"
|
||||
assert normalize_video_url(url) == url
|
||||
|
||||
|
||||
def test_video_request_accepts_watchlater():
|
||||
req = VideoRequest(
|
||||
url="https://www.bilibili.com/list/watchlater/?bvid=BV1CPXpBYEui&oid=116294762371214",
|
||||
platform="bilibili",
|
||||
)
|
||||
assert str(req.url) == "https://www.bilibili.com/video/BV1CPXpBYEui"
|
||||
|
||||
|
||||
def test_video_request_rejects_no_bv():
|
||||
with pytest.raises(ValueError):
|
||||
VideoRequest(url="https://www.bilibili.com/anime/timeline", platform="bilibili")
|
||||
|
||||
|
||||
def test_note_router_request_accepts_watchlater():
|
||||
# note.py 里的 VideoRequest 才是 /generate_note 实际使用的请求模型
|
||||
from app.routers.note import VideoRequest as NoteVideoRequest
|
||||
|
||||
req = NoteVideoRequest(
|
||||
video_url="https://www.bilibili.com/list/watchlater/?bvid=BV1CPXpBYEui&oid=116294762371214&spm_id_from=333.881.0.0",
|
||||
platform="bilibili",
|
||||
quality="fast",
|
||||
model_name="test-model",
|
||||
provider_id="test-provider",
|
||||
)
|
||||
assert req.video_url == "https://www.bilibili.com/video/BV1CPXpBYEui"
|
||||
|
||||
|
||||
def test_video_request_youtube_unaffected():
|
||||
req = VideoRequest(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", platform="youtube")
|
||||
assert str(req.url) == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
Reference in New Issue
Block a user