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    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku
92    ///
93    /// 参数
94    ///
95    /// | 名称 | 类型 | 说明 |
96    /// | ---- | ---- | ---- |
97    /// | `oid` | i64 | 视频 oid/cid |
98    pub async fn danmaku_xml_list_so(&self, oid: i64) -> Result<DanmakuXml, BpiError> {
99        let client = Client::builder()
100            .gzip(false)
101            .brotli(false)
102            .deflate(false) // 禁用自动解压
103            .build()?;
104
105        let bytes = client
106            .get("https://api.bilibili.com/x/v1/dm/list.so")
107            // .header(ACCEPT, "application/xml, text/xml, */*")
108            .query(&[("oid", oid.to_string())])
109            .send()
110            .await
111            .map_err(BpiError::from)?
112            .bytes()
113            .await
114            .map_err(|e| BpiError::network(format!("获取响应体失败: {}", e)))?;
115
116        let mut d = DeflateDecoder::new(&bytes[..]);
117        let mut xml = String::new();
118        d.read_to_string(&mut xml)
119            .map_err(|_| BpiError::parse("读取xml失败"))?;
120
121        let mut parsed: DanmakuXml = from_str(&xml).map_err(|_| BpiError::parse("解析xml失败"))?;
122
123        parsed.danmakus.iter_mut().try_for_each(|dm| dm.parse_p())?;
124
125        Ok(parsed)
126    }
127
128    /// 获取实时弹幕(接口2)
129    /// 使用 deflate 压缩(reqwest 会自动解压),返回 XML 文本
130    ///
131    /// 文档: https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/danmaku
132    ///
133    /// 参数
134    ///
135    /// | 名称 | 类型 | 说明 |
136    /// | ---- | ---- | ---- |
137    /// | `cid` | i64 | 视频 cid |
138    pub async fn danmaku_xml_list(&self, cid: i64) -> Result<DanmakuXml, BpiError> {
139        let url = format!("https://comment.bilibili.com/{}.xml", cid);
140
141        let client = Client::builder()
142            .gzip(false)
143            .brotli(false)
144            .deflate(false) // 禁用自动解压
145            .build()?;
146
147        let bytes = client.get(url).send().await?.bytes().await?;
148
149        let mut d = DeflateDecoder::new(&bytes[..]);
150        let mut xml = String::new();
151        d.read_to_string(&mut xml)
152            .map_err(|_| BpiError::parse("读取xml失败"))?;
153
154        let mut parsed: DanmakuXml = from_str(&xml).map_err(|_| BpiError::parse("解析xml失败"))?;
155
156        parsed.danmakus.iter_mut().try_for_each(|dm| dm.parse_p())?;
157
158        Ok(parsed)
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use tokio::time::Instant;
166    use tracing::info;
167
168    #[tokio::test]
169    async fn test_get_danmaku_xml_api() -> Result<(), Box<BpiError>> {
170        let bpi = BpiClient::new();
171        let start = Instant::now();
172
173        let data = bpi.danmaku_xml_list_so(16546).await?;
174        let duration = start.elapsed();
175
176        info!(
177            "耗时1 {:?} 弹幕装填个数: {:?} ",
178            duration,
179            data.danmakus.len()
180        );
181        Ok(())
182    }
183
184    #[tokio::test]
185    async fn test_get_danmaku_xml_cid() -> Result<(), Box<BpiError>> {
186        let bpi = BpiClient::new();
187        let start = Instant::now();
188
189        let data = bpi.danmaku_xml_list(16546).await?;
190        let duration = start.elapsed();
191
192        info!(
193            "耗时2 {:?} 弹幕装填个数: {:?} ",
194            duration,
195            data.danmakus.len()
196        );
197
198        Ok(())
199    }
200}