Skip to main content

bpi_rs/danmaku/
danmaku_xml.rs

1//! XML 弹幕
2//!
3//! [文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
4
5use crate::{ BpiClient, BpiError };
6use flate2::read::DeflateDecoder;
7use quick_xml::de::from_str;
8use reqwest::Client;
9use std::io::Read;
10
11use serde::{ Deserialize, Serialize };
12
13// 用于解析 <d> 标签的 p 属性的元数据
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DanmakuMeta {
16    pub time: f32, // 视频内弹幕出现时间(秒)
17    pub danmaku_type: i32, // 弹幕类型
18    pub font_size: i32, // 字号
19    pub color: i32, // 颜色(十进制RGB888值)
20    pub send_time: i64, // 发送时间戳
21    pub pool_type: i32, // 弹幕池类型
22    pub user_hash: String, // 发送者mid的HASH
23    pub dmid: i64, // 弹幕dmid(唯一标识)
24    pub block_level: i32, // 屏蔽等级
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(rename = "d")]
29pub struct Danmaku {
30    #[serde(rename = "$value")]
31    pub content: String, // 弹幕内容
32
33    #[serde(rename = "@p")]
34    pub p_value: String, // 原始的 p 属性字符串
35
36    #[serde(skip_serializing)]
37    pub meta: Option<DanmakuMeta>,
38}
39
40impl Danmaku {
41    /// 解析 p 属性并返回 DanmakuMeta
42    pub fn parse_p(&mut self) -> Result<(), BpiError> {
43        let parts: Vec<&str> = self.p_value.split(',').collect();
44        if parts.len() < 8 {
45            return Err(BpiError::parse("解析xml失败 弹幕参数不足8"));
46        }
47
48        let time: f32 = parts[0].parse().unwrap_or(0.0);
49        let danmaku_type: i32 = parts[1].parse().unwrap_or(1);
50        let font_size: i32 = parts[2].parse().unwrap_or(25);
51        let color: i32 = parts[3].parse().unwrap_or(16777215); // 默认白色
52        let send_time: i64 = parts[4].parse().unwrap_or(0);
53        let pool_type: i32 = parts[5].parse().unwrap_or(0);
54        let user_hash = parts[6].to_string();
55        let dmid: i64 = parts[7].parse().unwrap_or(0);
56
57        let block_level = parts[8].parse().unwrap_or(0);
58        self.meta = Some(DanmakuMeta {
59            time,
60            danmaku_type,
61            font_size,
62            color,
63            send_time,
64            pool_type,
65            user_hash,
66            dmid,
67            block_level,
68        });
69        Ok(())
70    }
71}
72
73// 根标签 i
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename = "i")]
76pub struct DanmakuXml {
77    pub chatserver: String,
78    pub chatid: String,
79    pub mission: i32,
80    pub maxlimit: i32,
81    pub state: i32, // 0: 正常, 1: 弹幕已关闭
82    pub real_name: i32,
83    pub source: String,
84    #[serde(rename = "d", default)]
85    pub danmakus: Vec<Danmaku>,
86}
87
88impl BpiClient {
89    /// 获取实时弹幕(接口1)
90    ///
91    /// # 文档
92    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
93    ///
94    /// # 参数
95    ///
96    /// | 名称 | 类型 | 说明 |
97    /// | ---- | ---- | ---- |
98    /// | `oid` | i64 | 视频 oid/cid |
99    pub async fn danmaku_xml_list_so(&self, oid: i64) -> Result<DanmakuXml, BpiError> {
100        let client = Client::builder()
101            .gzip(false)
102            .brotli(false)
103            .deflate(false) // 禁用自动解压
104            .build()?;
105
106        let bytes = client
107            .get("https://api.bilibili.com/x/v1/dm/list.so")
108            // .header(ACCEPT, "application/xml, text/xml, */*")
109            .query(&[("oid", oid.to_string())])
110            .send().await
111            .map_err(BpiError::from)?
112            .bytes().await
113            .map_err(|e| BpiError::network(format!("获取响应体失败: {}", e)))?;
114
115        let mut d = DeflateDecoder::new(&bytes[..]);
116        let mut xml = String::new();
117        d.read_to_string(&mut xml).map_err(|_| BpiError::parse("读取xml失败"))?;
118
119        let mut parsed: DanmakuXml = from_str(&xml).map_err(|_| BpiError::parse("解析xml失败"))?;
120
121        parsed.danmakus.iter_mut().try_for_each(|dm| dm.parse_p())?;
122
123        Ok(parsed)
124    }
125
126    /// 获取实时弹幕(接口2)
127    /// 使用 deflate 压缩(reqwest 会自动解压),返回 XML 文本
128    ///
129    /// # 文档
130    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku)
131    ///
132    /// # 参数
133    ///
134    /// | 名称 | 类型 | 说明 |
135    /// | ---- | ---- | ---- |
136    /// | `cid` | i64 | 视频 cid |
137    pub async fn danmaku_xml_list(&self, cid: i64) -> Result<DanmakuXml, BpiError> {
138        let url = format!("https://comment.bilibili.com/{}.xml", cid);
139
140        let client = Client::builder()
141            .gzip(false)
142            .brotli(false)
143            .deflate(false) // 禁用自动解压
144            .build()?;
145
146        let bytes = client.get(url).send().await?.bytes().await?;
147
148        let mut d = DeflateDecoder::new(&bytes[..]);
149        let mut xml = String::new();
150        d.read_to_string(&mut xml).map_err(|_| BpiError::parse("读取xml失败"))?;
151
152        let mut parsed: DanmakuXml = from_str(&xml).map_err(|_| BpiError::parse("解析xml失败"))?;
153
154        parsed.danmakus.iter_mut().try_for_each(|dm| dm.parse_p())?;
155
156        Ok(parsed)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use tokio::time::Instant;
164    use tracing::info;
165
166    #[tokio::test]
167    async fn test_get_danmaku_xml_api() -> Result<(), Box<BpiError>> {
168        let bpi = BpiClient::new();
169        let start = Instant::now();
170
171        let data = bpi.danmaku_xml_list_so(16546).await?;
172        let duration = start.elapsed();
173
174        info!("耗时1 {:?} 弹幕装填个数: {:?} ", duration, data.danmakus.len());
175        Ok(())
176    }
177
178    #[tokio::test]
179    async fn test_get_danmaku_xml_cid() -> Result<(), Box<BpiError>> {
180        let bpi = BpiClient::new();
181        let start = Instant::now();
182
183        let data = bpi.danmaku_xml_list(16546).await?;
184        let duration = start.elapsed();
185
186        info!("耗时2 {:?} 弹幕装填个数: {:?} ", duration, data.danmakus.len());
187
188        Ok(())
189    }
190}