Skip to content

Commit 2b35920

Browse files
authored
✨ feat: use protobuf danmaku source when login (#340)
1 parent b5d510c commit 2b35920

File tree

13 files changed

+133
-18
lines changed

13 files changed

+133
-18
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,11 @@ https://github.com/orgs/community/discussions/16925#discussioncomment-7571187
274274
- 可选值 `"ass" | "xml" | "protobuf"`
275275
- 默认值 `"ass"`
276276

277-
B 站提供了 `xml``protobuf` 两种弹幕数据接口,yutto 会自动下载 `xml` 格式弹幕并转换为 `ass` 格式,如果你不喜欢 yutto 自动转换的效果,可以选择输出格式为 `xml``protobuf`,手动通过一些工具进行转换,比如 yutto 和 bilili 所使用的 [biliass](https://github.com/yutto-dev/yutto/tree/main/packages/biliass),或者使用 [us-danmaku](https://tiansh.github.io/us-danmaku/bilibili/) 进行在线转换。
277+
B 站提供了 `xml``protobuf` 两种弹幕数据接口,`xml` 接口为旧接口,弹幕数上限较低,`protobuf` 接口相对较高,但不登录情况下只能获取很少的弹幕
278+
279+
为了确保无论是否登录都能获取最多的弹幕,yutto 在登录时会下载 `protobuf` 源数据,在未登录时会下载 `xml` 源数据,并将其转换为主流播放器支持的 `ass` 格式
280+
281+
如果你不喜欢 yutto 自动转换的效果,可以选择输出格式为 `xml``protobuf`,手动通过一些工具进行转换,比如 yutto 和 bilili 所使用的 [biliass](https://github.com/yutto-dev/yutto/tree/main/packages/biliass),或者使用 [us-danmaku](https://tiansh.github.io/us-danmaku/bilibili/) 进行在线转换。
278282

279283
如果你不想下载弹幕,只需要使用参数 `--no-danmaku` 即可。
280284

packages/biliass/rust/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fn biliass_pyo3(m: &Bound<'_, PyModule>) -> PyResult<()> {
2424
m.add_class::<python::PyDanmakuElem>()?;
2525
m.add_class::<python::PyComment>()?;
2626
m.add_class::<python::PyCommentPosition>()?;
27+
m.add_function(wrap_pyfunction!(python::py_get_danmaku_meta_size, m)?)?;
2728
m.add_function(wrap_pyfunction!(python::py_read_comments_from_xml, m)?)?;
2829
m.add_function(wrap_pyfunction!(python::py_read_comments_from_protobuf, m)?)?;
2930
m.add_function(wrap_pyfunction!(python::py_parse_special_comment, m)?)?;

packages/biliass/rust/src/python/convert.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ pub fn py_xml_to_ass(
3838
#[allow(clippy::too_many_arguments)]
3939
#[pyfunction(name = "protobuf_to_ass")]
4040
pub fn py_protobuf_to_ass(
41-
// inputs: Vec<Py<PyAny>>,
4241
inputs: Vec<PyBackedBytes>,
4342
stage_width: u32,
4443
stage_height: u32,

packages/biliass/rust/src/python/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ mod reader;
55

66
pub use comment::{PyComment, PyCommentPosition};
77
pub use convert::{py_protobuf_to_ass, py_xml_to_ass};
8-
pub use proto::{PyDanmakuElem, PyDmSegMobileReply};
8+
pub use proto::{py_get_danmaku_meta_size, PyDanmakuElem, PyDmSegMobileReply};
99
pub use reader::{
1010
py_parse_special_comment, py_read_comments_from_protobuf, py_read_comments_from_xml,
1111
};

packages/biliass/rust/src/python/proto.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,13 @@ impl PyDmSegMobileReply {
130130
))
131131
}
132132
}
133+
134+
#[pyfunction(name = "get_danmaku_meta_size")]
135+
pub fn py_get_danmaku_meta_size(buffer: &[u8]) -> PyResult<usize> {
136+
let dm_sge_opt = proto::DmWebViewReply::decode(&mut Cursor::new(buffer))
137+
.map(|reply| reply.dm_sge)
138+
.map_err(error::DecodeError::from)
139+
.map_err(error::BiliassError::from)?;
140+
141+
Ok(dm_sge_opt.map_or(0, |dm_sge| dm_sge.total as usize))
142+
}

packages/biliass/src/biliass/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from biliass._core import get_danmaku_meta_size as get_danmaku_meta_size
4+
35
from .biliass import (
46
Danmaku2ASS as Danmaku2ASS,
57
read_comments_bilibili_protobuf as read_comments_bilibili_protobuf,

packages/biliass/src/biliass/_core.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ def protobuf_to_ass(
8686
comment_filter: list[str],
8787
is_reduce_comments: bool,
8888
) -> str: ...
89+
def get_danmaku_meta_size(buffer: bytes) -> int: ...

src/yutto/_typing.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,57 @@ class AvId(BilibiliId):
5656
```
5757
"""
5858

59+
# id conversion based on
60+
# https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/bvid_desc.md
61+
62+
XOR_CODE = 23442827791579
63+
MASK_CODE = 2251799813685247
64+
MAX_AID = 1 << 51
65+
ALPHABET = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"
66+
ENCODE_MAP = 8, 7, 0, 5, 1, 3, 2, 4, 6
67+
DECODE_MAP = tuple(reversed(ENCODE_MAP))
68+
69+
BASE = len(ALPHABET)
70+
PREFIX = "BV1"
71+
PREFIX_LEN = len(PREFIX)
72+
CODE_LEN = len(ENCODE_MAP)
73+
74+
@staticmethod
75+
def av2bv(aid: int) -> str:
76+
bvid = [""] * 9
77+
tmp = (AvId.MAX_AID | aid) ^ AvId.XOR_CODE
78+
for i in range(AvId.CODE_LEN):
79+
bvid[AvId.ENCODE_MAP[i]] = AvId.ALPHABET[tmp % AvId.BASE]
80+
tmp //= AvId.BASE
81+
return AvId.PREFIX + "".join(bvid)
82+
83+
@staticmethod
84+
def bv2av(bvid: str) -> int:
85+
assert bvid[:3] == AvId.PREFIX
86+
87+
bvid = bvid[3:]
88+
tmp = 0
89+
for i in range(AvId.CODE_LEN):
90+
idx = AvId.ALPHABET.index(bvid[AvId.DECODE_MAP[i]])
91+
tmp = tmp * AvId.BASE + idx
92+
return (tmp & AvId.MASK_CODE) ^ AvId.XOR_CODE
93+
5994
def to_dict(self) -> dict[str, str]:
6095
raise NotImplementedError("请不要直接使用 AvId")
6196

6297
def to_url(self) -> str:
6398
raise NotImplementedError("请不要直接使用 AvId")
6499

100+
def as_aid(self) -> AId:
101+
if isinstance(self, AId):
102+
return self
103+
return AId(str(self.bv2av(self.value)))
104+
105+
def as_bvid(self) -> BvId:
106+
if isinstance(self, BvId):
107+
return self
108+
return BvId(self.av2bv(int(self.value)))
109+
65110

66111
class AId(AvId):
67112
"""AID"""

src/yutto/api/danmaku.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from __future__ import annotations
22

3+
import asyncio
34
from typing import TYPE_CHECKING
45

6+
from biliass import get_danmaku_meta_size
7+
8+
from yutto.api.user_info import get_user_info
59
from yutto.utils.fetcher import Fetcher
610

711
if TYPE_CHECKING:
812
import httpx
913

10-
from yutto._typing import CId
14+
from yutto._typing import AvId, CId
1115
from yutto.utils.danmaku import DanmakuData, DanmakuSaveType
1216

1317

@@ -18,21 +22,34 @@ async def get_xml_danmaku(client: httpx.AsyncClient, cid: CId) -> str:
1822
return results
1923

2024

21-
async def get_protobuf_danmaku(client: httpx.AsyncClient, cid: CId, segment_id: int = 1) -> bytes:
25+
async def get_protobuf_danmaku_segment(client: httpx.AsyncClient, cid: CId, segment_id: int = 1) -> bytes:
2226
danmaku_api = "http://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid={cid}&segment_index={segment_id}"
2327
results = await Fetcher.fetch_bin(client, danmaku_api.format(cid=cid, segment_id=segment_id))
2428
assert results is not None
2529
return results
2630

2731

32+
async def get_protobuf_danmaku(client: httpx.AsyncClient, avid: AvId, cid: CId) -> list[bytes]:
33+
danmaku_meta_api = "https://api.bilibili.com/x/v2/dm/web/view?type=1&oid={cid}&pid={aid}"
34+
aid = avid.as_aid()
35+
meta_results = await Fetcher.fetch_bin(client, danmaku_meta_api.format(cid=cid, aid=aid.value))
36+
assert meta_results is not None
37+
size = get_danmaku_meta_size(meta_results)
38+
39+
results = await asyncio.gather(
40+
*[get_protobuf_danmaku_segment(client, cid, segment_id) for segment_id in range(1, size + 1)]
41+
)
42+
return results
43+
44+
2845
async def get_danmaku(
2946
client: httpx.AsyncClient,
3047
cid: CId,
48+
avid: AvId,
3149
save_type: DanmakuSaveType,
32-
last_n_segments: int = 2,
3350
) -> DanmakuData:
34-
# 暂时默认使用 XML 源
35-
source_type = "xml" if save_type == "xml" or save_type == "ass" else "protobuf"
51+
# 在已经登录的情况下,使用 protobuf,因为未登录时 protobuf 弹幕会少非常多
52+
source_type = "xml" if save_type == "xml" or not (await get_user_info(client))["is_login"] else "protobuf"
3653
danmaku_data: DanmakuData = {
3754
"source_type": source_type,
3855
"save_type": save_type,
@@ -42,6 +59,5 @@ async def get_danmaku(
4259
if source_type == "xml":
4360
danmaku_data["data"].append(await get_xml_danmaku(client, cid))
4461
else:
45-
for i in range(1, last_n_segments + 1):
46-
danmaku_data["data"].append(await get_protobuf_danmaku(client, cid, i))
62+
danmaku_data["data"].extend(await get_protobuf_danmaku(client, avid, cid))
4763
return danmaku_data

src/yutto/api/user_info.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import TYPE_CHECKING, Any, TypedDict
1111

1212
from yutto._typing import UserInfo
13+
from yutto.utils.asynclib import async_cache
1314
from yutto.utils.fetcher import Fetcher
1415

1516
if TYPE_CHECKING:
@@ -26,6 +27,7 @@ class WbiImg(TypedDict):
2627
dm_cover_img_str_cache: str = base64.b64encode("".join(random.choices(string.printable, k=random.randint(32, 128))).encode())[:-2].decode() # fmt: skip
2728

2829

30+
@async_cache(lambda _: "user_info")
2931
async def get_user_info(client: AsyncClient) -> UserInfo:
3032
info_api = "https://api.bilibili.com/x/web-interface/nav"
3133
res_json = await Fetcher.fetch_json(client, info_api)

0 commit comments

Comments
 (0)