fix(local-danmu): 修正 B 站 XML 弹幕颜色解析 (#476)

标准 B 站 XML 的 p 为 8 段(时间,类型,字号,颜色,...),颜色应取第 4 段;旧 3/4 段格式仍取第 3 段。补充回归测试覆盖两种格式。
This commit is contained in:
lilixu3 authored and GitHub committed 2026-09-13 13:10:21 +08:00
1 parent cbb8f7bda9
commit 60bed960c5
2 files changed
+20 -2

No files matched your search

+7 -2
View File
@@ -11,6 +11,11 @@ const timeToSeconds = (value) => {
return (Number(m[1] || 0) * 3600) + Number(m[2]) * 60 + Number(m[3]) + Number(`0.${m[4] || 0}`);
};
const cleanText = (v) => String(v ?? '').replace(/<[^>]+>/g, '').trim();
// 标准 B 站 XML 为 8/9 段 p(时间,类型,字号,颜色,...),旧的 3/4 段格式颜色仍在第 3 段。
const pickColorFromP = (p) => {
const parts = String(p ?? '').split(',');
return parts.length >= 8 ? parts[3] : parts[2];
};
const normalize = (rows, format) => {
const errors = [];
const comments = [];
@@ -18,7 +23,7 @@ const normalize = (rows, format) => {
const row = rows[i] || {};
const pTime = typeof row.p === 'string' ? row.p.split(',')[0] : null;
const pMode = typeof row.p === 'string' ? row.p.split(',')[1] : null;
const pColor = typeof row.p === 'string' ? row.p.split(',')[2] : null;
const pColor = typeof row.p === 'string' ? pickColorFromP(row.p) : null;
const time = timeToSeconds(row.time ?? row.start ?? row.progress ?? row.timepoint ?? row.t ?? pTime);
const text = cleanText(row.text ?? row.content ?? row.m ?? row.message);
if (!Number.isFinite(time) || time < 0 || !text) { if (errors.length < 5) errors.push(`${format} 第 ${i + 1} 行时间或文本无效`); continue; }
@@ -42,7 +47,7 @@ function parseXml(text) {
const nodes = [];
const walk = (v) => { if (!v || typeof v !== 'object') return; if (v.p && (v['#text'] !== undefined || typeof v.p === 'string')) nodes.push({ p: v.p, text: v['#text'] ?? v.text ?? '' }); for (const x of Object.values(v)) Array.isArray(x) ? x.forEach(walk) : walk(x); };
walk(data);
return normalize(nodes.map(x => { const p = String(x.p).split(','); return { time: p[0], mode: p[1], color: p[2], text: x.text }; }), 'XML');
return normalize(nodes.map(x => { const p = String(x.p).split(','); return { time: p[0], mode: p[1], color: pickColorFromP(x.p), text: x.text }; }), 'XML');
}
function parseAss(text) {
const rows = [];
+13
View File
@@ -3176,6 +3176,19 @@ test('UTF-8 JSON preserves a literal replacement character without changing enco
assert.deepEqual(parseLocalDanmu(Buffer.from(json, 'utf8'), 'danmu.json'), expected);
});
test('local XML reads the Bilibili color field instead of the font size', () => {
const xml = '<i>'
+ '<d p="1.00,1,25,16777215,1700000000,0,abc,1001">白色弹幕</d>'
+ '<d p="2.00,1,25,16711680,1700000001,0,abc,1002">红色弹幕</d>'
+ '<d p="3.00,1,16711680,0">旧四段格式</d>'
+ '</i>';
assert.deepEqual(parseLocalDanmu(Buffer.from(xml, 'utf8'), 'bili.xml').comments, [
{ p: '1.00,1,16777215', m: '白色弹幕' },
{ p: '2.00,1,16711680', m: '红色弹幕' },
{ p: '3.00,1,16711680', m: '旧四段格式' },
]);
});
const encodings = [
['UTF-8', text => Buffer.from(text, 'utf8')],
['UTF-8 with BOM', text => Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), Buffer.from(text, 'utf8')])],