use super::danmu::{Danmu, DanmuType};
use anyhow::{bail, Context, Result};
use std::{
fs::File,
io::{BufRead, BufReader, Read, Seek},
path::Path,
};
#[cfg(feature = "quick_xml")]
use quick_xml::Reader;
#[cfg(feature = "xml_rs")]
use xml::reader::EventReader as Reader;
pub struct Parser<R: BufRead> {
count: usize,
reader: Reader<R>,
#[cfg(feature = "quick_xml")]
buf: Vec<u8>,
}
impl<R: BufRead> Parser<R> {
pub fn new(reader: R) -> Self {
#[cfg(feature = "xml_rs")]
let reader = Reader::new(reader);
#[cfg(feature = "quick_xml")]
let reader = Reader::from_reader(reader);
Self {
count: 0,
reader,
#[cfg(feature = "quick_xml")]
buf: Vec::new(),
}
}
}
impl Parser<BufReader<File>> {
pub fn from_path(path: &Path) -> Result<Self> {
let file = std::fs::File::open(path)?;
let mut reader = BufReader::new(file);
let mut bom_buf = [0u8; 3];
reader.read_exact(&mut bom_buf)?;
if bom_buf != [0xEF, 0xBB, 0xBF] {
reader.seek(std::io::SeekFrom::Start(0))?;
}
#[cfg(feature = "xml_rs")]
let reader = Reader::new(reader);
#[cfg(feature = "quick_xml")]
let reader = Reader::from_reader(reader);
Ok(Self {
count: 0,
reader,
#[cfg(feature = "quick_xml")]
buf: Vec::new(),
})
}
}
impl<R: BufRead> Iterator for Parser<R> {
type Item = Result<Danmu>;
#[cfg(feature = "xml_rs")]
fn next(&mut self) -> Option<Result<Danmu>> {
let mut danmu = Danmu::default();
loop {
let event = match self.reader.next().context("XML 文件解析错误") {
Ok(e) => e,
Err(e) => return Some(Err(e)),
};
match event {
xml::reader::XmlEvent::EndDocument => {
return None;
}
xml::reader::XmlEvent::StartElement {
name, attributes, ..
} if name.local_name == "d" => {
let p_attr = match attributes
.into_iter()
.find(|attr| attr.name.local_name == "p")
{
Some(p_attr) => p_attr,
None => {
return Some(Err(anyhow::anyhow!(
"弹幕 <d> 中没找到 p 属性,xml 文件可能有错误"
)))
}
};
match Danmu::from_xml_p_attr(&p_attr.value).context("p 属性解析错误") {
Ok(parsed) => {
danmu = parsed;
}
Err(e) => return Some(Err(e)),
};
}
xml::reader::XmlEvent::EndElement { name } if name.local_name == "d" => {
self.count += 1;
return Some(Ok(danmu));
}
xml::reader::XmlEvent::Characters(s) => {
#[cfg(debug_assertions)]
{
danmu.content = format!("{}-{}", self.count, s);
}
#[cfg(not(debug_assertions))]
{
danmu.content = s;
}
}
xml::reader::XmlEvent::StartDocument { .. }
| xml::reader::XmlEvent::Comment(_)
| xml::reader::XmlEvent::CData(_)
| xml::reader::XmlEvent::ProcessingInstruction { .. }
| xml::reader::XmlEvent::Whitespace(_)
| xml::reader::XmlEvent::StartElement { .. }
| xml::reader::XmlEvent::EndElement { .. } => {
continue;
}
}
}
}
#[cfg(feature = "quick_xml")]
fn next(&mut self) -> Option<Result<Danmu>> {
use quick_xml::events::Event;
let mut danmu = Danmu::default();
loop {
let event = match self
.reader
.read_event(&mut self.buf)
.context("XML 文件解析错误")
{
Ok(e) => e,
Err(e) => return Some(Err(e)),
};
match event {
Event::Eof => {
return None;
}
Event::Start(start) if start.local_name() == b"d" => {
let p_attr = start
.attributes()
.into_iter()
.filter_map(|r| r.ok())
.find(|attr| attr.key == b"p");
let p_attr = match p_attr {
Some(p_attr) => p_attr,
None => {
return Some(Err(anyhow::anyhow!(
"弹幕 <d> 中没找到 p 属性,xml 文件可能有错误"
)))
}
};
let p_attr_s = match std::str::from_utf8(p_attr.value.as_ref())
.context("非法 UTF-8 字符")
{
Ok(p_attr_s) => p_attr_s,
Err(e) => return Some(Err(e)),
};
match Danmu::from_xml_p_attr(p_attr_s).context("p 属性解析错误") {
Ok(parsed) => {
danmu = parsed;
}
Err(e) => return Some(Err(e)),
};
}
Event::End(end) if end.local_name() == b"d" => {
self.count += 1;
return Some(Ok(danmu));
}
Event::Text(text) => match std::str::from_utf8(&text).context("非法 UTF-8 字符")
{
Ok(s) => {
#[cfg(debug_assertions)]
{
danmu.content = format!("{}-{}", self.count, s);
}
#[cfg(not(debug_assertions))]
{
danmu.content = s.to_string();
}
}
Err(e) => return Some(Err(e)),
},
_ => {
continue;
}
}
}
}
}
impl Danmu {
/// 从哔哩哔哩的弹幕格式解析
///
/// <d p="p" user="user"> content </d>
/// 其中,p = 0.581,1,25,14893055,1647777083220,0,398452452,0
/// 分别为:
/// 1. 时间(秒),
/// 2. 弹幕类型,(1 为普通弹幕,4 为底部弹幕,5 对应顶部,6 对应反向弹幕)
/// 3. 字体大小(默认25)
/// 4. 弹幕颜色(如14893055)
/// 5. 弹幕毫秒级时间戳(如 1647777083220)
/// 6. 0
/// 7. 用户 UID(如 398452452)
/// 8. 0
pub fn from_xml_p_attr(p_attr: &str) -> Result<Self> {
let mut iter = p_attr.split(',');
let timeline_s = iter
.next()
.context("p 属性中没有时间")?
.parse()
.context("时间解析错误")?;
let r#type = iter
.next()
.context("p 属性中没有弹幕类型")?
.parse()
.context("弹幕类型解析错误")?;
let r#type = DanmuType::from_xml_num(r#type)?;
let fontsize: u32 = iter
.next()
.context("p 属性中没有字体大小")?
.parse()
.context("字体大小解析错误")?;
let rgb: u32 = iter
.next()
.context("p 属性中没有颜色")?
.parse()
.context("颜色解析错误")?;
// rgb 是个数字,类似 0x010203
if (rgb >> 24) != 0 {
bail!("颜色解析错误:高 8 位不为 0,颜色为 {:x}", rgb);
}
let r = (rgb >> 16) & 0xff;
let g = (rgb >> 8) & 0xff;
let b = rgb & 0xff;
Ok(Self {
timeline_s,
content: String::new(),
r#type,
fontsize,
rgb: (r as u8, g as u8, b as u8),
})
}
}
impl DanmuType {
pub fn from_xml_num(num: u32) -> Result<Self> {
Ok(match num {
1 => DanmuType::Float,
4 => DanmuType::Bottom,
5 => DanmuType::Top,
6 => DanmuType::Reverse,
_ => bail!("未知的弹幕类型:{}", num),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
static DATA: &str = r##"
<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="#s"?>
<i>
<!--
B站录播姬 1.3.11
本文件的弹幕信息兼容B站主站视频弹幕XML格式
本XML自带样式可以在浏览器里打开(推荐使用Chrome)
sc 为SuperChat
gift为礼物
guard为上船
attribute "raw" 为原始数据
-->
<chatserver>chat.bilibili.com</chatserver>
<chatid>0</chatid>
<mission>0</mission>
<maxlimit>1000</maxlimit>
<state>0</state>
<real_name>0</real_name>
<source>0</source>
<BililiveRecorder version="1.3.11" />
<BililiveRecorderRecordInfo roomid="22637261" shortid="0" name="嘉然今天吃什么" title="【B限】第一届枝江gamer争霸赛!" areanameparent="虚拟主播" areanamechild="虚拟主播" start_time="2022-03-20T19:51:23.6348295+08:00" />
<BililiveRecorderXmlStyle><z:stylesheet version="1.0" id="s" xml:id="s" xmlns:z="http://www.w3.org/1999/XSL/Transform"><z:output method="html"/><z:template match="/"><html><meta name="viewport" content="width=device-width"/><title>B站录播姬弹幕文件 - <z:value-of select="/i/BililiveRecorderRecordInfo/@name"/></title><style>body{margin:0}h1,h2,p,table{margin-left:5px}table{border-spacing:0}td,th{border:1px solid grey;padding:1px}th{position:sticky;top:0;background:#4098de}tr:hover{background:#d9f4ff}div{overflow:auto;max-height:80vh;max-width:100vw;width:fit-content}</style><h1>B站录播姬弹幕XML文件</h1><p>本文件的弹幕信息兼容B站主站视频弹幕XML格式,可以使用现有的转换工具把文件中的弹幕转为ass字幕文件</p><table><tr><td>录播姬版本</td><td><z:value-of select="/i/BililiveRecorder/@version"/></td></tr><tr><td>房间号</td><td><z:value-of select="/i/BililiveRecorderRecordInfo/@roomid"/></td></tr><tr><td>主播名</td><td><z:value-of select="/i/BililiveRecorderRecordInfo/@name"/></td></tr><tr><td>录制开始时间</td><td><z:value-of select="/i/BililiveRecorderRecordInfo/@start_time"/></td></tr><tr><td><a href="#d">弹幕</a></td><td>共 <z:value-of select="count(/i/d)"/> 条记录</td></tr><tr><td><a href="#guard">上船</a></td><td>共 <z:value-of select="count(/i/guard)"/> 条记录</td></tr><tr><td><a href="#sc">SC</a></td><td>共 <z:value-of select="count(/i/sc)"/> 条记录</td></tr><tr><td><a href="#gift">礼物</a></td><td>共 <z:value-of select="count(/i/gift)"/> 条记录</td></tr></table><h2 id="d">弹幕</h2><div><table><tr><th>用户名</th><th>弹幕</th><th>参数</th></tr><z:for-each select="/i/d"><tr><td><z:value-of select="@user"/></td><td><z:value-of select="."/></td><td><z:value-of select="@p"/></td></tr></z:for-each></table></div><h2 id="guard">舰长购买</h2><div><table><tr><th>用户名</th><th>舰长等级</th><th>购买数量</th><th>出现时间</th></tr><z:for-each select="/i/guard"><tr><td><z:value-of select="@user"/></td><td><z:value-of select="@level"/></td><td><z:value-of select="@count"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="sc">SuperChat 醒目留言</h2><div><table><tr><th>用户名</th><th>内容</th><th>显示时长</th><th>价格</th><th>出现时间</th></tr><z:for-each select="/i/sc"><tr><td><z:value-of select="@user"/></td><td><z:value-of select="."/></td><td><z:value-of select="@time"/></td><td><z:value-of select="@price"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div><h2 id="gift">礼物</h2><div><table><tr><th>用户名</th><th>礼物名</th><th>礼物数量</th><th>出现时间</th></tr><z:for-each select="/i/gift"><tr><td><z:value-of select="@user"/></td><td><z:value-of select="@giftname"/></td><td><z:value-of select="@giftcount"/></td><td><z:value-of select="@ts"/></td></tr></z:for-each></table></div></html></z:template></z:stylesheet></BililiveRecorderXmlStyle>
<gift ts="0.576" user="粉色羽毛球_Official" giftname="小心心" giftcount="1" raw="{"action":"投喂","batch_combo_id":"batch:gift:combo_id:197750709:672328094:30607:1647777082.1182","batch_combo_send":null,"beatId":"","biz_source":"live","blind_gift":null,"broadcast_id":0,"coin_type":"silver","combo_resources_id":1,"combo_send":null,"combo_stay_time":3,"combo_total_coin":1,"crit_prob":0,"demarcation":1,"discount_price":0,"dmscore":28,"draw":0,"effect":0,"effect_block":1,"face":"http://i0.hdslb.com/bfs/face/9bdc8d6e008efc52721b3056441f6edb27d575fb.jpg","float_sc_resource_id":0,"giftId":30607,"giftName":"小心心","giftType":5,"gold":0,"guard_level":0,"is_first":false,"is_special_batch":0,"magnification":1,"medal_info":{"anchor_roomid":0,"anchor_uname":"","guard_level":0,"icon_id":0,"is_lighted":1,"medal_color":9272486,"medal_color_border":9272486,"medal_color_end":9272486,"medal_color_start":9272486,"medal_level":10,"medal_name":"嘉心糖","special":"","target_id":672328094},"name_color":"","num":1,"original_gift_name":"","price":0,"rcost":200134843,"remain":10,"rnd":"1647777083120900002","send_master":null,"silver":0,"super":0,"super_batch_gift_num":5,"super_gift_num":5,"svga_block":0,"tag_image":"","tid":"1647777083120900002","timestamp":1647777083,"top_list":null,"total_coin":0,"uid":197750709,"uname":"粉色羽毛球_Official"}" />
<d p="0.581,1,25,14893055,1647777083220,0,398452452,0" user="小马368100" raw="[[0,1,25,14893055,1647777083220,1647776219,0,"1537d7c7",0,0,5,"#1453BAFF,#4C2263A2,#3353BAFF",0,"{}","{}",{"mode":0,"show_player_type":0,"extra":"{\"send_from_me\":false,\"mode\":0,\"color\":14893055,\"dm_type\":0,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"快快快\",\"user_hash\":\"355981255\",\"emoticon_unique\":\"\",\"bulge_display\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}"}],"快快快",[398452452,"小马368100",0,0,0,10000,1,"#00D1F1"],[22,"嘉心糖","嘉然今天吃什么",22637261,1725515,"",0,6809855,1725515,5414290,3,1,672328094],[5,0,9868950,">50000",0],["",""],0,3,null,{"ts":1647777083,"ct":"7581F4E3"},0,0,null,null,0,105]">快快快</d>
<d p="0.582,1,25,14893055,1647777083280,0,24755246,0" user="園田" raw="[[0,1,25,14893055,1647777083280,1647776175,0,"1ad255c8",0,0,5,"#1453BAFF,#4C2263A2,#3353BAFF",0,"{}","{}",{"mode":0,"show_player_type":0,"extra":"{\"send_from_me\":false,\"mode\":0,\"color\":14893055,\"dm_type\":0,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"快快快快快快\",\"user_hash\":\"449992136\",\"emoticon_unique\":\"\",\"bulge_display\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}"}],"快快快快快快",[24755246,"園田",0,0,0,10000,1,"#00D1F1"],[22,"贝极星","贝拉kira",22632424,1725515,"",0,6809855,1725515,5414290,3,1,672353429],[27,0,5805790,">50000",0],["",""],0,3,null,{"ts":1647777083,"ct":"BC30A1B8"},0,0,null,null,0,105]">快快快快快快</d>
<gift ts="0.582" user="粉色羽毛球_Official" giftname="小心心" giftcount="1" raw="{"action":"投喂","batch_combo_id":"batch:gift:combo_id:197750709:672328094:30607:1647777082.1182","batch_combo_send":null,"beatId":"","biz_source":"live","blind_gift":null,"broadcast_id":0,"coin_type":"silver","combo_resources_id":1,"combo_send":null,"combo_stay_time":3,"combo_total_coin":1,"crit_prob":0,"demarcation":1,"discount_price":0,"dmscore":56,"draw":0,"effect":0,"effect_block":1,"face":"http://i0.hdslb.com/bfs/face/9bdc8d6e008efc52721b3056441f6edb27d575fb.jpg","float_sc_resource_id":0,"giftId":30607,"giftName":"小心心","giftType":5,"gold":0,"guard_level":0,"is_first":false,"is_special_batch":0,"magnification":1,"medal_info":{"anchor_roomid":0,"anchor_uname":"","guard_level":0,"icon_id":0,"is_lighted":1,"medal_color":9272486,"medal_color_border":9272486,"medal_color_end":9272486,"medal_color_start":9272486,"medal_level":10,"medal_name":"嘉心糖","special":"","target_id":672328094},"name_color":"","num":1,"original_gift_name":"","price":0,"rcost":200134843,"remain":9,"rnd":"1647777083120900004","send_master":null,"silver":0,"super":0,"super_batch_gift_num":6,"super_gift_num":6,"svga_block":0,"tag_image":"","tid":"1647777083120900004","timestamp":1647777083,"top_list":null,"total_coin":0,"uid":197750709,"uname":"粉色羽毛球_Official"}" />
<gift ts="0.583" user="bili_105342487" giftname="小心心" giftcount="1" raw="{"action":"投喂","batch_combo_id":"batch:gift:combo_id:105342487:672328094:30607:1647777083.3650","batch_combo_send":null,"beatId":"","biz_source":"live","blind_gift":null,"broadcast_id":0,"coin_type":"silver","combo_resources_id":1,"combo_send":null,"combo_stay_time":3,"combo_total_coin":1,"crit_prob":0,"demarcation":1,"discount_price":0,"dmscore":12,"draw":0,"effect":0,"effect_block":1,"face":"http://i1.hdslb.com/bfs/face/56b139786beb080f666d283e14cd2b47755c8b93.jpg","float_sc_resource_id":0,"giftId":30607,"giftName":"小心心","giftType":5,"gold":0,"guard_level":0,"is_first":true,"is_special_batch":0,"magnification":1,"medal_info":{"anchor_roomid":0,"anchor_uname":"","guard_level":0,"icon_id":0,"is_lighted":1,"medal_color":6126494,"medal_color_border":6126494,"medal_color_end":6126494,"medal_color_start":6126494,"medal_level":7,"medal_name":"莴饱了","special":"","target_id":1773346},"name_color":"","num":1,"original_gift_name":"","price":0,"rcost":200134843,"remain":4,"rnd":"1647777083120700003","send_master":null,"silver":0,"super":0,"super_batch_gift_num":1,"super_gift_num":1,"svga_block":0,"tag_image":"","tid":"1647777083120700003","timestamp":1647777083,"top_list":null,"total_coin":0,"uid":105342487,"uname":"bili_105342487"}" />
<d p="0.583,1,25,14893055,1647777083474,0,215087720,0" user="含着王力口乐的麦克风" raw="[[0,1,25,14893055,1647777083474,1647776382,0,"47a886d6",0,0,5,"#1453BAFF,#4C2263A2,#3353BAFF",0,"{}","{}",{"mode":0,"show_player_type":0,"extra":"{\"send_from_me\":false,\"mode\":0,\"color\":14893055,\"dm_type\":0,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"快快快\",\"user_hash\":\"1202226902\",\"emoticon_unique\":\"\",\"bulge_display\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}"}],"快快快",[215087720,"含着王力口乐的麦克风",0,0,0,10000,1,"#00D1F1"],[21,"嘉心糖","嘉然今天吃什么",22637261,1725515,"",0,6809855,1725515,5414290,3,1,672328094],[4,0,9868950,">50000",0],["",""],0,3,null,{"ts":1647777083,"ct":"6D21C977"},0,0,null,null,0,105]">快快快</d>
<d p="0.583,1,25,16772431,1647777083579,0,5950232,0" user="凝华的领绳" raw="[[0,1,25,16772431,1647777083579,1647776961,0,"c6fbc9f7",0,0,5,"#1453BAFF,#4C2263A2,#3353BAFF",0,"{}","{}",{"mode":0,"show_player_type":0,"extra":"{\"send_from_me\":false,\"mode\":0,\"color\":16772431,\"dm_type\":0,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"好好好\",\"user_hash\":\"3338390007\",\"emoticon_unique\":\"\",\"bulge_display\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}"}],"好好好",[5950232,"凝华的领绳",0,0,0,10000,1,"#00D1F1"],[9,"纯路人","猫清六合丶犬扫八方",1238503,9272486,"",0,9272486,9272486,9272486,0,1,24979266],[44,0,16746162,19724,0],["",""],0,3,null,{"ts":1647777083,"ct":"7DD4EC71"},0,0,null,null,0,105]">好好好</d>
<d p="0.583,1,25,4546550,1647777083729,0,14675948,0" user="勇气花咲" raw="[[0,1,25,4546550,1647777083729,1647776966,0,"ce2c1008",0,0,0,"",0,"{}","{}",{"mode":0,"show_player_type":0,"extra":"{\"send_from_me\":false,\"mode\":0,\"color\":4546550,\"dm_type\":0,\"font_size\":25,\"player_mode\":1,\"show_player_type\":0,\"content\":\"快快快快快快\",\"user_hash\":\"3458994184\",\"emoticon_unique\":\"\",\"bulge_display\":0,\"direction\":0,\"pk_direction\":0,\"quartet_direction\":0,\"yeah_space_type\":\"\",\"yeah_space_url\":\"\",\"jump_to_url\":\"\",\"space_type\":\"\",\"space_url\":\"\"}"}],"快快快快快快",[14675948,"勇气花咲",0,0,0,10000,1,""],[17,"一个魂","A-SOUL_Official",22632157,13081892,"",0,13081892,13081892,13081892,0,1,703007996],[29,0,5805790,">50000",0],["",""],0,0,null,{"ts":1647777083,"ct":"D8428531"},0,0,null,null,0,56]">快快快快快快</d>
</i>
"##;
#[test]
fn iterator() {
let mut parser = Parser::new(DATA.as_bytes());
assert_eq!(
parser.next().unwrap().unwrap(),
Danmu {
timeline_s: 0.581,
content: "0-快快快".to_string(),
r#type: DanmuType::Float,
fontsize: 25,
rgb: (0xe3, 0x3f, 0xff),
}
);
}
#[test]
fn from_xml() {
let danmu =
Danmu::from_xml_p_attr("0.583,1,25,14893055,1647777083474,0,215087720,0").unwrap();
assert_eq!(
danmu,
Danmu {
timeline_s: 0.583,
content: String::new(),
r#type: DanmuType::Float,
fontsize: 25,
rgb: (0xe3, 0x3f, 0xff),
}
);
}
}