use crate::{ BpiClient, BpiError };
use flate2::read::DeflateDecoder;
use quick_xml::de::from_str;
use reqwest::Client;
use std::io::Read;
use serde::{ Deserialize, Serialize };
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DanmakuMeta {
pub time: f32, pub danmaku_type: i32, pub font_size: i32, pub color: i32, pub send_time: i64, pub pool_type: i32, pub user_hash: String, pub dmid: i64, pub block_level: i32, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "d")]
pub struct Danmaku {
#[serde(rename = "$value")]
pub content: String,
#[serde(rename = "@p")]
pub p_value: String,
#[serde(skip_serializing)]
pub meta: Option<DanmakuMeta>,
}
impl Danmaku {
pub fn parse_p(&mut self) -> Result<(), BpiError> {
let parts: Vec<&str> = self.p_value.split(',').collect();
if parts.len() < 8 {
return Err(BpiError::parse("解析xml失败 弹幕参数不足8"));
}
let time: f32 = parts[0].parse().unwrap_or(0.0);
let danmaku_type: i32 = parts[1].parse().unwrap_or(1);
let font_size: i32 = parts[2].parse().unwrap_or(25);
let color: i32 = parts[3].parse().unwrap_or(16777215); let send_time: i64 = parts[4].parse().unwrap_or(0);
let pool_type: i32 = parts[5].parse().unwrap_or(0);
let user_hash = parts[6].to_string();
let dmid: i64 = parts[7].parse().unwrap_or(0);
let block_level = parts[8].parse().unwrap_or(0);
self.meta = Some(DanmakuMeta {
time,
danmaku_type,
font_size,
color,
send_time,
pool_type,
user_hash,
dmid,
block_level,
});
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "i")]
pub struct DanmakuXml {
pub chatserver: String,
pub chatid: String,
pub mission: i32,
pub maxlimit: i32,
pub state: i32, pub real_name: i32,
pub source: String,
#[serde(rename = "d", default)]
pub danmakus: Vec<Danmaku>,
}
impl BpiClient {
pub async fn danmaku_xml_list_so(&self, oid: i64) -> Result<DanmakuXml, BpiError> {
let client = Client::builder()
.gzip(false)
.brotli(false)
.deflate(false) .build()?;
let bytes = client
.get("https://api.bilibili.com/x/v1/dm/list.so")
.query(&[("oid", oid.to_string())])
.send().await
.map_err(BpiError::from)?
.bytes().await
.map_err(|e| BpiError::network(format!("获取响应体失败: {}", e)))?;
let mut d = DeflateDecoder::new(&bytes[..]);
let mut xml = String::new();
d.read_to_string(&mut xml).map_err(|_| BpiError::parse("读取xml失败"))?;
let mut parsed: DanmakuXml = from_str(&xml).map_err(|_| BpiError::parse("解析xml失败"))?;
parsed.danmakus.iter_mut().try_for_each(|dm| dm.parse_p())?;
Ok(parsed)
}
pub async fn danmaku_xml_list(&self, cid: i64) -> Result<DanmakuXml, BpiError> {
let url = format!("https://comment.bilibili.com/{}.xml", cid);
let client = Client::builder()
.gzip(false)
.brotli(false)
.deflate(false) .build()?;
let bytes = client.get(url).send().await?.bytes().await?;
let mut d = DeflateDecoder::new(&bytes[..]);
let mut xml = String::new();
d.read_to_string(&mut xml).map_err(|_| BpiError::parse("读取xml失败"))?;
let mut parsed: DanmakuXml = from_str(&xml).map_err(|_| BpiError::parse("解析xml失败"))?;
parsed.danmakus.iter_mut().try_for_each(|dm| dm.parse_p())?;
Ok(parsed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::Instant;
use tracing::info;
#[tokio::test]
async fn test_get_danmaku_xml_api() -> Result<(), Box<BpiError>> {
let bpi = BpiClient::new();
let start = Instant::now();
let data = bpi.danmaku_xml_list_so(16546).await?;
let duration = start.elapsed();
info!("耗时1 {:?} 弹幕装填个数: {:?} ", duration, data.danmakus.len());
Ok(())
}
#[tokio::test]
async fn test_get_danmaku_xml_cid() -> Result<(), Box<BpiError>> {
let bpi = BpiClient::new();
let start = Instant::now();
let data = bpi.danmaku_xml_list(16546).await?;
let duration = start.elapsed();
info!("耗时2 {:?} 弹幕装填个数: {:?} ", duration, data.danmakus.len());
Ok(())
}
}