@dan-uni/dan-any 0.1.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/index.js +5963 -1
- package/dist/index.umd.min.js +19958 -1
- package/dist/index.umd.min.js.LICENSE.txt +17 -0
- package/dist/src/ass-gen/__tests__/generate.test.d.ts +1 -0
- package/dist/src/ass-gen/ass/create.d.ts +4 -0
- package/dist/src/ass-gen/ass/dialogue.d.ts +17 -0
- package/dist/src/ass-gen/ass/event.d.ts +3 -0
- package/dist/src/ass-gen/ass/info.d.ts +8 -0
- package/dist/src/ass-gen/ass/raw.d.ts +10 -0
- package/dist/src/ass-gen/ass/style.d.ts +3 -0
- package/dist/src/ass-gen/config.d.ts +3 -0
- package/dist/src/ass-gen/index.d.ts +26 -0
- package/dist/src/ass-gen/types.d.ts +47 -0
- package/dist/src/ass-gen/util/color.d.ts +18 -0
- package/dist/src/ass-gen/util/danconvert.d.ts +4 -0
- package/dist/src/ass-gen/util/index.d.ts +4 -0
- package/dist/src/ass-gen/util/lang.d.ts +3 -0
- package/dist/src/ass-gen/util/layout.d.ts +4 -0
- package/dist/{index.d.ts → src/index.d.ts} +15 -1
- package/dist/src/index.test.d.ts +1 -0
- package/dist/{utils → src/utils}/dm-gen.d.ts +13 -5
- package/dist/src/utils/dm-gen.test.d.ts +1 -0
- package/package.json +6 -5
- package/rslib.config.ts +81 -0
- package/src/ass-gen/__tests__/898651903.xml +1619 -0
- package/src/ass-gen/__tests__/898651903.xml.ass +1516 -0
- package/src/ass-gen/__tests__/canvas.test.ts +11 -0
- package/src/ass-gen/__tests__/generate.test.ts +20 -0
- package/src/ass-gen/ass/create.ts +26 -0
- package/src/ass-gen/ass/dialogue.ts +91 -0
- package/src/ass-gen/ass/event.ts +58 -0
- package/src/ass-gen/ass/info.ts +28 -0
- package/src/ass-gen/ass/raw.ts +69 -0
- package/src/ass-gen/ass/style.ts +67 -0
- package/src/ass-gen/config.ts +45 -0
- package/src/ass-gen/index.ts +52 -0
- package/src/ass-gen/types.ts +52 -0
- package/src/ass-gen/util/color.ts +55 -0
- package/src/ass-gen/util/danconvert.ts +36 -0
- package/src/ass-gen/util/index.ts +10 -0
- package/src/ass-gen/util/lang.ts +35 -0
- package/src/ass-gen/util/layout.ts +238 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +114 -0
- package/src/utils/dm-gen.test.ts +66 -0
- package/src/utils/dm-gen.ts +91 -11
- package/tsconfig.json +7 -7
- package/dist/index.min.js +0 -1
- package/rollup.config.mjs +0 -51
- /package/dist/{index.test.d.ts → src/ass-gen/__tests__/canvas.test.d.ts} +0 -0
- /package/dist/{proto → src/proto}/gen/bili/dm_pb.d.ts +0 -0
- /package/dist/{proto → src/proto}/gen/danuni_pb.d.ts +0 -0
- /package/dist/{utils → src/utils}/id-gen.d.ts +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createCanvas } from 'canvas'
|
|
2
|
+
import type { UniPool } from '../..'
|
|
3
|
+
import type { Danmaku, SubtitleStyle } from '../types'
|
|
4
|
+
|
|
5
|
+
import { DanmakuType, FontSize } from '../types'
|
|
6
|
+
import { DanmakuList2UniPool, UniPool2DanmakuLists } from './danconvert'
|
|
7
|
+
import { arrayOfLength } from './lang'
|
|
8
|
+
|
|
9
|
+
// 计算一个矩形移进屏幕的时间(头进屏幕到尾巴进屏幕)
|
|
10
|
+
const computeScrollInTime = (
|
|
11
|
+
rectWidth: number,
|
|
12
|
+
screenWidth: number,
|
|
13
|
+
scrollTime: number,
|
|
14
|
+
) => {
|
|
15
|
+
const speed = (screenWidth + rectWidth) / scrollTime
|
|
16
|
+
return rectWidth / speed
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 计算一个矩形在屏幕上的时间(头进屏幕到头离开屏幕)
|
|
20
|
+
const computeScrollOverTime = (
|
|
21
|
+
rectWidth: number,
|
|
22
|
+
screenWidth: number,
|
|
23
|
+
scrollTime: number,
|
|
24
|
+
) => {
|
|
25
|
+
const speed = (screenWidth + rectWidth) / scrollTime
|
|
26
|
+
return screenWidth / speed
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ScrollGrid {
|
|
30
|
+
start: number
|
|
31
|
+
end: number
|
|
32
|
+
width: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FixGrid = number
|
|
36
|
+
|
|
37
|
+
interface DanmakuGrids {
|
|
38
|
+
// [DanmakuType.SCROLL]: ScrollGrid[];
|
|
39
|
+
// [DanmakuType.TOP]: FixGrid[];
|
|
40
|
+
// [DanmakuType.BOTTOM]: FixGrid[];
|
|
41
|
+
1: ScrollGrid[] // DanmakuType.SCROLL
|
|
42
|
+
3: FixGrid[] // DanmakuType.TOP
|
|
43
|
+
2: FixGrid[] // DanmakuType.BOTTOM
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const splitGrids = ({
|
|
47
|
+
fontSize,
|
|
48
|
+
padding,
|
|
49
|
+
playResY,
|
|
50
|
+
bottomSpace,
|
|
51
|
+
}: {
|
|
52
|
+
fontSize: number[]
|
|
53
|
+
padding: number[]
|
|
54
|
+
playResY: number
|
|
55
|
+
bottomSpace: number
|
|
56
|
+
}): DanmakuGrids => {
|
|
57
|
+
const defaultFontSize = fontSize[FontSize.NORMAL]
|
|
58
|
+
const paddingTop = padding[0]
|
|
59
|
+
const paddingBottom = padding[2]
|
|
60
|
+
const linesCount = Math.floor(
|
|
61
|
+
(playResY - bottomSpace) / (defaultFontSize + paddingTop + paddingBottom),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// 首先以通用的字号把屏幕的高度分成若干行,字幕只允许落在一个行里
|
|
65
|
+
return {
|
|
66
|
+
// 每一行里的数字是当前在这一行里的最后一条弹幕区域(算入padding)的右边离开屏幕的时间,
|
|
67
|
+
// 这个时间和下一条弹幕的左边离开屏幕的时间相比较,能确定在整个弹幕的飞行过程中是否会相撞(不同长度弹幕飞行速度不同)|,
|
|
68
|
+
// 当每一条弹幕加到一行里时,就会把这个时间算出来,获取新的弹幕时就可以判断哪一行是允许放的就放进去
|
|
69
|
+
1: arrayOfLength(linesCount, {
|
|
70
|
+
start: 0,
|
|
71
|
+
end: 0,
|
|
72
|
+
width: 0,
|
|
73
|
+
}),
|
|
74
|
+
// 对于固定的弹幕,每一行里都存放弹幕的消失时间,只要这行的弹幕没消失就不能放新弹幕进来
|
|
75
|
+
3: arrayOfLength(linesCount, 0),
|
|
76
|
+
2: arrayOfLength(linesCount, 0),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const measureTextWidth = (() => {
|
|
81
|
+
const canvasContext = createCanvas(50, 50).getContext('2d')
|
|
82
|
+
const supportTextMeasure = !!canvasContext.measureText('中')
|
|
83
|
+
|
|
84
|
+
if (supportTextMeasure) {
|
|
85
|
+
return (
|
|
86
|
+
fontName: string,
|
|
87
|
+
fontSize: number,
|
|
88
|
+
bold: boolean,
|
|
89
|
+
text: string,
|
|
90
|
+
) => {
|
|
91
|
+
canvasContext.font = `${bold ? 'bold' : 'normal'} ${fontSize}px ${fontName}`
|
|
92
|
+
const textWidth = canvasContext.measureText(text).width
|
|
93
|
+
return Math.round(textWidth)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.warn(
|
|
98
|
+
'[Warn] node-canvas is installed without text measure support, layout may not be correct',
|
|
99
|
+
)
|
|
100
|
+
return (_fontName: string, fontSize: number, _bold: boolean, text: string) =>
|
|
101
|
+
text.length * fontSize
|
|
102
|
+
})()
|
|
103
|
+
|
|
104
|
+
// 找到能用的行
|
|
105
|
+
const resolveAvailableFixGrid = (grids: FixGrid[], time: number) => {
|
|
106
|
+
for (const [i, grid] of grids.entries()) {
|
|
107
|
+
if (grid <= time) {
|
|
108
|
+
return i
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return -1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resolveAvailableScrollGrid = (
|
|
116
|
+
grids: ScrollGrid[],
|
|
117
|
+
rectWidth: number,
|
|
118
|
+
screenWidth: number,
|
|
119
|
+
time: number,
|
|
120
|
+
duration: number,
|
|
121
|
+
) => {
|
|
122
|
+
for (const [i, previous] of grids.entries()) {
|
|
123
|
+
// 对于滚动弹幕,要算两个位置:
|
|
124
|
+
//
|
|
125
|
+
// 1. 前一条弹幕的尾巴进屏幕之前,后一条弹幕不能开始出现
|
|
126
|
+
// 2. 前一条弹幕的尾巴离开屏幕之前,后一条弹幕的头不能离开屏幕
|
|
127
|
+
const previousInTime =
|
|
128
|
+
previous.start +
|
|
129
|
+
computeScrollInTime(previous.width, screenWidth, duration)
|
|
130
|
+
const currentOverTime =
|
|
131
|
+
time + computeScrollOverTime(rectWidth, screenWidth, duration)
|
|
132
|
+
|
|
133
|
+
if (time >= previousInTime && currentOverTime >= previous.end) {
|
|
134
|
+
return i
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return -1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const initializeLayout = (config: SubtitleStyle) => {
|
|
142
|
+
const {
|
|
143
|
+
playResX,
|
|
144
|
+
playResY,
|
|
145
|
+
fontName,
|
|
146
|
+
fontSize,
|
|
147
|
+
bold,
|
|
148
|
+
padding,
|
|
149
|
+
scrollTime,
|
|
150
|
+
fixTime,
|
|
151
|
+
bottomSpace,
|
|
152
|
+
} = config
|
|
153
|
+
const [paddingTop, paddingRight, paddingBottom, paddingLeft] = padding
|
|
154
|
+
|
|
155
|
+
const defaultFontSize = fontSize[FontSize.NORMAL]
|
|
156
|
+
const grids = splitGrids(config)
|
|
157
|
+
const gridHeight = defaultFontSize + paddingTop + paddingBottom
|
|
158
|
+
|
|
159
|
+
return (danmaku: Danmaku) => {
|
|
160
|
+
const targetGrids = grids[danmaku.type as keyof DanmakuGrids]
|
|
161
|
+
const danmakuFontSize = fontSize[danmaku.fontSizeType]
|
|
162
|
+
const rectWidth =
|
|
163
|
+
measureTextWidth(fontName, danmakuFontSize, bold, danmaku.content) +
|
|
164
|
+
paddingLeft +
|
|
165
|
+
paddingRight
|
|
166
|
+
const verticalOffset =
|
|
167
|
+
paddingTop + Math.round((defaultFontSize - danmakuFontSize) / 2)
|
|
168
|
+
|
|
169
|
+
if (danmaku.type === DanmakuType.SCROLL) {
|
|
170
|
+
const scrollGrids = targetGrids as ScrollGrid[]
|
|
171
|
+
const gridNumber = resolveAvailableScrollGrid(
|
|
172
|
+
scrollGrids,
|
|
173
|
+
rectWidth,
|
|
174
|
+
playResX,
|
|
175
|
+
danmaku.time,
|
|
176
|
+
scrollTime,
|
|
177
|
+
)
|
|
178
|
+
if (gridNumber < 0) {
|
|
179
|
+
// console.warn(`[Warn] Collision ${danmaku.time}: ${danmaku.content}`)
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
targetGrids[gridNumber] = {
|
|
183
|
+
width: rectWidth,
|
|
184
|
+
start: danmaku.time,
|
|
185
|
+
end: danmaku.time + scrollTime,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const top = gridNumber * gridHeight + verticalOffset
|
|
189
|
+
const start = playResX + paddingLeft
|
|
190
|
+
const end = -rectWidth
|
|
191
|
+
|
|
192
|
+
return { ...danmaku, top, start, end }
|
|
193
|
+
} else {
|
|
194
|
+
const gridNumber = resolveAvailableFixGrid(
|
|
195
|
+
targetGrids as FixGrid[],
|
|
196
|
+
danmaku.time,
|
|
197
|
+
)
|
|
198
|
+
if (gridNumber < 0) {
|
|
199
|
+
// console.warn(`[Warn] Collision ${danmaku.time}: ${danmaku.content}`)
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
if (danmaku.type === DanmakuType.TOP) {
|
|
203
|
+
targetGrids[gridNumber] = danmaku.time + fixTime
|
|
204
|
+
|
|
205
|
+
const top = gridNumber * gridHeight + verticalOffset
|
|
206
|
+
// 固定弹幕横向按中心点计算
|
|
207
|
+
const left = Math.round(playResX / 2)
|
|
208
|
+
|
|
209
|
+
return { ...danmaku, top, left }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
targetGrids[gridNumber] = danmaku.time + fixTime
|
|
213
|
+
|
|
214
|
+
// 底部字幕的格子是留出`bottomSpace`的位置后从下往上算的
|
|
215
|
+
const top =
|
|
216
|
+
playResY -
|
|
217
|
+
bottomSpace -
|
|
218
|
+
gridHeight * gridNumber -
|
|
219
|
+
gridHeight +
|
|
220
|
+
verticalOffset
|
|
221
|
+
const left = Math.round(playResX / 2)
|
|
222
|
+
|
|
223
|
+
return { ...danmaku, top, left }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const layoutDanmaku = (
|
|
229
|
+
inputList: UniPool,
|
|
230
|
+
config: SubtitleStyle,
|
|
231
|
+
): UniPool => {
|
|
232
|
+
const list = [...UniPool2DanmakuLists(inputList)].sort(
|
|
233
|
+
(x, y) => x.time - y.time,
|
|
234
|
+
)
|
|
235
|
+
const layout = initializeLayout(config)
|
|
236
|
+
|
|
237
|
+
return DanmakuList2UniPool(list.map(layout).filter((danmaku) => !!danmaku))
|
|
238
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -48,6 +48,12 @@ describe('转化自', () => {
|
|
|
48
48
|
console.info(json)
|
|
49
49
|
console.info(pool)
|
|
50
50
|
})
|
|
51
|
+
it('ass(双向)', () => {
|
|
52
|
+
const pool = UniPool.fromBiliXML(xml)
|
|
53
|
+
const ass = pool.toASS()
|
|
54
|
+
console.info(ass)
|
|
55
|
+
console.info(UniPool.fromASS(ass))
|
|
56
|
+
})
|
|
51
57
|
})
|
|
52
58
|
|
|
53
59
|
describe('共通值', () => {
|
|
@@ -65,4 +71,7 @@ describe('其它', () => {
|
|
|
65
71
|
it('最小化', () => {
|
|
66
72
|
console.info(pool.minify())
|
|
67
73
|
})
|
|
74
|
+
it('合并范围内重复', () => {
|
|
75
|
+
console.info(pool.merge(10).minify())
|
|
76
|
+
})
|
|
68
77
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { XMLParser } from 'fast-xml-parser'
|
|
2
|
+
import type { Options as AssGenOptions } from './ass-gen'
|
|
2
3
|
import type { CommandDm as DM_JSON_BiliCommandGrpc } from './proto/gen/bili/dm_pb'
|
|
3
4
|
// import type * as UniDMType from './utils/dm-gen'
|
|
4
5
|
import type { platfrom } from './utils/id-gen'
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
timestampNow,
|
|
11
12
|
} from '@bufbuild/protobuf/wkt'
|
|
12
13
|
|
|
14
|
+
import { generateASS, parseAssRawField } from './ass-gen'
|
|
13
15
|
import {
|
|
14
16
|
// DanmakuElem as DM_JSON_BiliGrpc,
|
|
15
17
|
DmSegMobileReplySchema,
|
|
@@ -71,6 +73,7 @@ export type DM_format =
|
|
|
71
73
|
| 'dplayer.json'
|
|
72
74
|
| 'artplayer.json'
|
|
73
75
|
| 'ddplay.json'
|
|
76
|
+
| 'common.ass'
|
|
74
77
|
|
|
75
78
|
type shareItems = Partial<
|
|
76
79
|
Pick<
|
|
@@ -95,6 +98,9 @@ export class UniPool {
|
|
|
95
98
|
static create() {
|
|
96
99
|
return new UniPool([])
|
|
97
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* 合并弹幕/弹幕库
|
|
103
|
+
*/
|
|
98
104
|
assign(dans: UniPool | UniDM | UniDM[]) {
|
|
99
105
|
if (dans instanceof UniPool) {
|
|
100
106
|
return new UniPool([...this.dans, ...dans.dans])
|
|
@@ -104,6 +110,9 @@ export class UniPool {
|
|
|
104
110
|
return new UniPool([...this.dans, ...dans])
|
|
105
111
|
} else return this
|
|
106
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* 按共通属性拆分弹幕库
|
|
115
|
+
*/
|
|
107
116
|
split(key: keyof shareItems) {
|
|
108
117
|
if (this.shared[key]) return [this]
|
|
109
118
|
const set = new Set(this.dans.map((d) => d[key]))
|
|
@@ -111,6 +120,102 @@ export class UniPool {
|
|
|
111
120
|
return new UniPool(this.dans.filter((d) => d[key] === v))
|
|
112
121
|
})
|
|
113
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* 合并一定时间段内的重复弹幕,防止同屏出现过多
|
|
125
|
+
* @param lifetime 查重时间区段,单位秒 (默认为0,表示不查重)
|
|
126
|
+
*/
|
|
127
|
+
merge(lifetime = 0) {
|
|
128
|
+
if (!this.shared.FCID) {
|
|
129
|
+
console.error(
|
|
130
|
+
"本功能仅支持同弹幕库内使用,可先 .split('FCID') 在分别使用",
|
|
131
|
+
)
|
|
132
|
+
return this
|
|
133
|
+
}
|
|
134
|
+
if (lifetime <= 0) return this
|
|
135
|
+
const mergeContext = this.dans.reduce<
|
|
136
|
+
[
|
|
137
|
+
UniDM[],
|
|
138
|
+
Record<string, UniDM>,
|
|
139
|
+
Record<string, UniDMTools.ExtraDanUniMerge>,
|
|
140
|
+
]
|
|
141
|
+
>(
|
|
142
|
+
([result, cache, mergeObj], danmaku) => {
|
|
143
|
+
const key = ['content', 'mode', 'platform', 'pool', 'SPMO']
|
|
144
|
+
.map((k) => danmaku[k as keyof UniDM])
|
|
145
|
+
.join('|')
|
|
146
|
+
const cached = cache[key]
|
|
147
|
+
const lastAppearTime = cached?.progress || 0
|
|
148
|
+
if (
|
|
149
|
+
cached &&
|
|
150
|
+
danmaku.progress - lastAppearTime <= lifetime &&
|
|
151
|
+
danmaku.isSameAs(cached)
|
|
152
|
+
) {
|
|
153
|
+
const senders = mergeObj[key].senders
|
|
154
|
+
senders.push(danmaku.senderID)
|
|
155
|
+
const extra = danmaku.extra
|
|
156
|
+
extra.danuni = extra.danuni || {}
|
|
157
|
+
extra.danuni.merge = {
|
|
158
|
+
count: senders.length,
|
|
159
|
+
duration: danmaku.progress - cached.progress,
|
|
160
|
+
senders,
|
|
161
|
+
}
|
|
162
|
+
danmaku.extraStr = JSON.stringify(extra)
|
|
163
|
+
cache[key] = danmaku
|
|
164
|
+
mergeObj[key] = extra.danuni.merge
|
|
165
|
+
return [result, cache, mergeObj]
|
|
166
|
+
} else {
|
|
167
|
+
mergeObj[key] = {
|
|
168
|
+
count: 1,
|
|
169
|
+
duration: 0,
|
|
170
|
+
senders: [danmaku.senderID],
|
|
171
|
+
}
|
|
172
|
+
cache[key] = danmaku
|
|
173
|
+
// 初始化merge信息,包含第一个sender
|
|
174
|
+
const extra = danmaku.extra
|
|
175
|
+
extra.danuni = extra.danuni || {}
|
|
176
|
+
extra.danuni.merge = mergeObj[key]
|
|
177
|
+
danmaku.extraStr = JSON.stringify(extra)
|
|
178
|
+
result.push(danmaku)
|
|
179
|
+
return [result, cache, mergeObj]
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
[[], {}, {}],
|
|
183
|
+
)
|
|
184
|
+
// 处理结果,删除senders<=1的merge字段
|
|
185
|
+
const [result, _cache, mergeObj] = mergeContext
|
|
186
|
+
result.forEach((danmaku, i) => {
|
|
187
|
+
const key = ['content', 'mode', 'platform', 'pool', 'SPMO']
|
|
188
|
+
.map((k) => danmaku[k as keyof UniDM])
|
|
189
|
+
.join('|')
|
|
190
|
+
const extra = result[i].extra,
|
|
191
|
+
mergeData = mergeObj[key]
|
|
192
|
+
result[i].extraStr = JSON.stringify({
|
|
193
|
+
...extra,
|
|
194
|
+
danuni: {
|
|
195
|
+
...extra.danuni,
|
|
196
|
+
merge: mergeData,
|
|
197
|
+
},
|
|
198
|
+
} satisfies UniDMTools.Extra)
|
|
199
|
+
if (mergeData?.count) {
|
|
200
|
+
if (mergeData.count <= 1) {
|
|
201
|
+
const updatedExtra = { ...extra }
|
|
202
|
+
if (updatedExtra.danuni) {
|
|
203
|
+
delete updatedExtra.danuni.merge
|
|
204
|
+
if (Object.keys(updatedExtra.danuni).length === 0) {
|
|
205
|
+
delete updatedExtra.danuni
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
result[i].extraStr =
|
|
209
|
+
Object.keys(updatedExtra).length > 0
|
|
210
|
+
? JSON.stringify(updatedExtra)
|
|
211
|
+
: undefined
|
|
212
|
+
} else {
|
|
213
|
+
result[i].senderID = 'merge@bot'
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
return new UniPool(result)
|
|
218
|
+
}
|
|
114
219
|
minify() {
|
|
115
220
|
return this.dans.map((d) => d.minify())
|
|
116
221
|
}
|
|
@@ -132,6 +237,8 @@ export class UniPool {
|
|
|
132
237
|
return this.toArtplayer()
|
|
133
238
|
case 'ddplay.json':
|
|
134
239
|
return this.toDDplay()
|
|
240
|
+
case 'common.ass':
|
|
241
|
+
return this.toASS()
|
|
135
242
|
default:
|
|
136
243
|
throw new Error('unknown format or unsupported now!')
|
|
137
244
|
}
|
|
@@ -342,6 +449,13 @@ export class UniPool {
|
|
|
342
449
|
}),
|
|
343
450
|
}
|
|
344
451
|
}
|
|
452
|
+
static fromASS(ass: string) {
|
|
453
|
+
return parseAssRawField(ass)
|
|
454
|
+
}
|
|
455
|
+
toASS(options: AssGenOptions = { substyle: {} }): string {
|
|
456
|
+
const fn = this.shared.FCID
|
|
457
|
+
return generateASS(this, { filename: fn, title: fn, ...options })
|
|
458
|
+
}
|
|
345
459
|
}
|
|
346
460
|
|
|
347
461
|
export {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//基于以下注释,根据vitest生成测试用例
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import type { UniDMObj } from './dm-gen'
|
|
4
|
+
|
|
5
|
+
import { UniDM, UniPool } from '..'
|
|
6
|
+
|
|
7
|
+
const xml = `<i>
|
|
8
|
+
<chatserver>chat.bilibili.com</chatserver>
|
|
9
|
+
<chatid>1156756312</chatid>
|
|
10
|
+
<mission>0</mission>
|
|
11
|
+
<maxlimit>2947</maxlimit>
|
|
12
|
+
<state>0</state>
|
|
13
|
+
<real_name>0</real_name>
|
|
14
|
+
<source>k-v</source>
|
|
15
|
+
<d p="13.213,1,25,16777215,1686314041,3,ff41173d,1335658005672492032">喜欢</d>
|
|
16
|
+
<d p="13.331,1,25,16777215,1686948453,3,56a3c5d5,1340979831550069760">不喜欢</d>
|
|
17
|
+
<d p="13.374,1,25,16777215,1686300770,3,647fe355,1335546672880933888">不喜欢</d>
|
|
18
|
+
<d p="13.499,1,25,16777215,1686301548,3,2848bf1c,1335553202649003264">不喜欢</d>
|
|
19
|
+
</i>`
|
|
20
|
+
|
|
21
|
+
describe('其它', () => {
|
|
22
|
+
const pool = UniPool.fromBiliXML(xml)
|
|
23
|
+
it('比较(常规)', () => {
|
|
24
|
+
// 确保测试用例为预期值
|
|
25
|
+
expect(pool.dans[0].content).toBe('喜欢')
|
|
26
|
+
expect(pool.dans[1].content).toBe('不喜欢')
|
|
27
|
+
// 正式测试
|
|
28
|
+
const a = pool.dans[0].isSameAs(pool.dans[1]),
|
|
29
|
+
b = pool.dans[1].isSameAs(pool.dans[2]),
|
|
30
|
+
c = pool.dans[1].isSameAs(pool.dans[3])
|
|
31
|
+
console.info(a, b, c)
|
|
32
|
+
expect(a).toBe(false)
|
|
33
|
+
expect(b).toBe(true)
|
|
34
|
+
expect(c).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
it('比较(extra)', () => {
|
|
37
|
+
const commonSample = {
|
|
38
|
+
FCID: 'test@du',
|
|
39
|
+
content: 'T Sample',
|
|
40
|
+
extra: {
|
|
41
|
+
danuni: {
|
|
42
|
+
merge: {
|
|
43
|
+
count: 1,
|
|
44
|
+
duration: 0,
|
|
45
|
+
senders: ['test@du'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
} satisfies Partial<UniDMObj>
|
|
50
|
+
const pool2 = [
|
|
51
|
+
UniDM.create({ ...commonSample, extra: undefined }),
|
|
52
|
+
UniDM.create({ ...commonSample, extra: {} }),
|
|
53
|
+
UniDM.create({ ...commonSample, extra: { danuni: {} } }),
|
|
54
|
+
UniDM.create({ ...commonSample }),
|
|
55
|
+
UniDM.create({ ...commonSample, extra: { artplayer: { border: true } } }),
|
|
56
|
+
]
|
|
57
|
+
for (const pool of pool2) {
|
|
58
|
+
console.info(pool.extraStr)
|
|
59
|
+
console.info(pool2[0].isSameAs(pool))
|
|
60
|
+
}
|
|
61
|
+
expect(pool2[0].isSameAs(pool2[1])).toBe(true)
|
|
62
|
+
expect(pool2[0].isSameAs(pool2[2])).toBe(true)
|
|
63
|
+
expect(pool2[0].isSameAs(pool2[3])).toBe(true)
|
|
64
|
+
expect(pool2[0].isSameAs(pool2[4])).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
package/src/utils/dm-gen.ts
CHANGED
|
@@ -6,6 +6,29 @@ import { createDMID, domainPreset, UniID as ID, platforms } from './id-gen'
|
|
|
6
6
|
const BigIntSerializer = (k: string, v: any) =>
|
|
7
7
|
typeof v === 'bigint' ? v.toString() : v
|
|
8
8
|
|
|
9
|
+
function cleanEmptyObjects(obj: object): object {
|
|
10
|
+
if (obj === null || typeof obj !== 'object') {
|
|
11
|
+
return obj
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(obj)) {
|
|
14
|
+
return obj
|
|
15
|
+
}
|
|
16
|
+
const cleaned: any = {}
|
|
17
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
+
const cleanedValue = cleanEmptyObjects(value)
|
|
19
|
+
if (
|
|
20
|
+
cleanedValue !== undefined &&
|
|
21
|
+
!(
|
|
22
|
+
typeof cleanedValue === 'object' &&
|
|
23
|
+
Object.keys(cleanedValue).length === 0
|
|
24
|
+
)
|
|
25
|
+
) {
|
|
26
|
+
cleaned[key] = cleanedValue
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Object.keys(cleaned).length > 0 ? cleaned : {}
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
class SetBin {
|
|
10
33
|
constructor(public bin: number) {}
|
|
11
34
|
set1(bit: number) {
|
|
@@ -193,14 +216,21 @@ interface ExtraBili {
|
|
|
193
216
|
command?: DMBiliCommand
|
|
194
217
|
}
|
|
195
218
|
export interface ExtraDanUni {
|
|
196
|
-
chapter?:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
chapter?: ExtraDanUniChapter
|
|
220
|
+
merge?: ExtraDanUniMerge
|
|
221
|
+
}
|
|
222
|
+
export interface ExtraDanUniChapter {
|
|
223
|
+
// seg: {
|
|
224
|
+
// st: number //开始时刻
|
|
225
|
+
// } //起止时间(ms)
|
|
226
|
+
duration: number //持续时间
|
|
227
|
+
type: ExtraDanUniChapterType
|
|
228
|
+
// action: ExtraDanUniChapterAction
|
|
229
|
+
}
|
|
230
|
+
export interface ExtraDanUniMerge {
|
|
231
|
+
duration: number //持续时间(重复内容第一次出现时间开始到合并了的弹幕中最后一次出现的时间)
|
|
232
|
+
count: number //重复次数
|
|
233
|
+
senders: string[] //发送者
|
|
204
234
|
}
|
|
205
235
|
export enum ExtraDanUniChapterType {
|
|
206
236
|
Chapter = 'ch', //其它片段(用于标记章节)
|
|
@@ -362,6 +392,9 @@ export class UniDM {
|
|
|
362
392
|
if (!DMID) DMID = this.toDMID()
|
|
363
393
|
|
|
364
394
|
this.progress = Number.parseFloat(progress.toFixed(3))
|
|
395
|
+
if (extraStr)
|
|
396
|
+
this.extraStr = JSON.stringify(cleanEmptyObjects(JSON.parse(extraStr)))
|
|
397
|
+
if (extraStr === '{}') this.extraStr = undefined
|
|
365
398
|
}
|
|
366
399
|
static create(args?: Partial<UniDMObj>) {
|
|
367
400
|
return args
|
|
@@ -386,9 +419,11 @@ export class UniDM {
|
|
|
386
419
|
)
|
|
387
420
|
: new UniDM(ID.fromNull().toString())
|
|
388
421
|
}
|
|
389
|
-
get extra() {
|
|
422
|
+
get extra(): Extra {
|
|
390
423
|
const extra = JSON.parse(this.extraStr || '{}')
|
|
391
|
-
|
|
424
|
+
// this.extraStr = JSON.stringify(cleanEmptyObjects(extra))
|
|
425
|
+
return extra
|
|
426
|
+
// return cleanEmptyObjects(extra) as Extra
|
|
392
427
|
}
|
|
393
428
|
get isFrom3rdPlatform() {
|
|
394
429
|
if (this.platform && platforms.includes(this.platform as platfrom))
|
|
@@ -403,6 +438,51 @@ export class UniDM {
|
|
|
403
438
|
toDMID() {
|
|
404
439
|
return createDMID(this.content, this.senderID, this.ctime)
|
|
405
440
|
}
|
|
441
|
+
isSameAs(dan: UniDM, _check2 = false): boolean {
|
|
442
|
+
const isSame = (k: keyof UniDMObj) => this[k] === dan[k],
|
|
443
|
+
checks = (
|
|
444
|
+
[
|
|
445
|
+
'FCID',
|
|
446
|
+
'content',
|
|
447
|
+
'mode',
|
|
448
|
+
'platform',
|
|
449
|
+
'pool',
|
|
450
|
+
'SPMO',
|
|
451
|
+
] satisfies (keyof UniDMObj)[]
|
|
452
|
+
).every((k) => isSame(k))
|
|
453
|
+
// 如果两个对象的extra都是空对象,只检查基本字段
|
|
454
|
+
if (
|
|
455
|
+
JSON.stringify(this.extra) === '{}' &&
|
|
456
|
+
JSON.stringify(dan.extra) === '{}'
|
|
457
|
+
) {
|
|
458
|
+
return checks
|
|
459
|
+
}
|
|
460
|
+
// 特殊情况:只包含danuni.merge的情况
|
|
461
|
+
const thisHasOnlyMerge =
|
|
462
|
+
this.extra.danuni?.merge &&
|
|
463
|
+
!this.extra.artplayer &&
|
|
464
|
+
!this.extra.bili &&
|
|
465
|
+
!this.extra.danuni.chapter
|
|
466
|
+
const danHasOnlyMerge =
|
|
467
|
+
dan.extra.danuni?.merge &&
|
|
468
|
+
!dan.extra.artplayer &&
|
|
469
|
+
!dan.extra.bili &&
|
|
470
|
+
!dan.extra.danuni.chapter
|
|
471
|
+
if (thisHasOnlyMerge && danHasOnlyMerge) {
|
|
472
|
+
return checks
|
|
473
|
+
}
|
|
474
|
+
if (_check2) {
|
|
475
|
+
return isSame('extraStr') && checks
|
|
476
|
+
}
|
|
477
|
+
const a = { ...this.extra }
|
|
478
|
+
const b = { ...dan.extra }
|
|
479
|
+
if (a.danuni) delete a.danuni.merge
|
|
480
|
+
if (b.danuni) delete b.danuni.merge
|
|
481
|
+
return UniDM.create({ ...a, extraStr: JSON.stringify(a) }).isSameAs(
|
|
482
|
+
UniDM.create({ ...b, extraStr: JSON.stringify(b) }),
|
|
483
|
+
true,
|
|
484
|
+
)
|
|
485
|
+
}
|
|
406
486
|
minify() {
|
|
407
487
|
type UObj = Partial<UniDMObj> & Pick<UniDMObj, 'FCID'>
|
|
408
488
|
const def: UObj = UniDM.create(),
|
|
@@ -433,7 +513,7 @@ export class UniDM {
|
|
|
433
513
|
* @param oriCtime
|
|
434
514
|
* @param tsUnit 当`oriCtime`为数字类型表时间戳时的单位;
|
|
435
515
|
* 为 毫秒(ms)/秒(s)
|
|
436
|
-
* @returns {Date}
|
|
516
|
+
* @returns {Date} Date格式时间
|
|
437
517
|
*/
|
|
438
518
|
static transCtime(oriCtime: ctime, tsUnit?: 'ms' | 's'): Date {
|
|
439
519
|
function isMsTs(ts: number | bigint) {
|
package/tsconfig.json
CHANGED
|
@@ -24,12 +24,6 @@
|
|
|
24
24
|
/* Modules */
|
|
25
25
|
"module": "ESNext", /* Skip type checking all .d.ts files. */
|
|
26
26
|
"moduleResolution": "Node",
|
|
27
|
-
"allowImportingTsExtensions": true, /* Ensure that casing is correct in imports. */
|
|
28
|
-
/* Type Checking */
|
|
29
|
-
"strict": true, /* Enable all strict type-checking options. */
|
|
30
|
-
"noImplicitAny": true,
|
|
31
|
-
"declaration": true,
|
|
32
|
-
"emitDeclarationOnly": true, /* Specify what module code is generated. */
|
|
33
27
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
|
34
28
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
35
29
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
@@ -43,7 +37,13 @@
|
|
|
43
37
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
|
44
38
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
|
45
39
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
|
46
|
-
|
|
40
|
+
"resolveJsonModule": true, /* Enable importing .json files. */
|
|
41
|
+
"allowImportingTsExtensions": true, /* Ensure that casing is correct in imports. */
|
|
42
|
+
/* Type Checking */
|
|
43
|
+
"strict": true, /* Enable all strict type-checking options. */
|
|
44
|
+
"noImplicitAny": true,
|
|
45
|
+
"declaration": true,
|
|
46
|
+
"emitDeclarationOnly": true, /* Specify what module code is generated. */
|
|
47
47
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
|
48
48
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
|
49
49
|
/* JavaScript Support */
|