@dan-uni/dan-any 0.1.0 → 0.2.0

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.
Files changed (54) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +5956 -1
  3. package/dist/index.umd.min.js +19951 -1
  4. package/dist/index.umd.min.js.LICENSE.txt +17 -0
  5. package/dist/src/ass-gen/__tests__/generate.test.d.ts +1 -0
  6. package/dist/src/ass-gen/ass/create.d.ts +4 -0
  7. package/dist/src/ass-gen/ass/dialogue.d.ts +17 -0
  8. package/dist/src/ass-gen/ass/event.d.ts +3 -0
  9. package/dist/src/ass-gen/ass/info.d.ts +8 -0
  10. package/dist/src/ass-gen/ass/raw.d.ts +10 -0
  11. package/dist/src/ass-gen/ass/style.d.ts +3 -0
  12. package/dist/src/ass-gen/config.d.ts +3 -0
  13. package/dist/src/ass-gen/index.d.ts +26 -0
  14. package/dist/src/ass-gen/types.d.ts +47 -0
  15. package/dist/src/ass-gen/util/color.d.ts +18 -0
  16. package/dist/src/ass-gen/util/danconvert.d.ts +4 -0
  17. package/dist/src/ass-gen/util/index.d.ts +4 -0
  18. package/dist/src/ass-gen/util/lang.d.ts +3 -0
  19. package/dist/src/ass-gen/util/layout.d.ts +4 -0
  20. package/dist/{index.d.ts → src/index.d.ts} +15 -1
  21. package/dist/src/index.test.d.ts +1 -0
  22. package/dist/{utils → src/utils}/dm-gen.d.ts +13 -5
  23. package/dist/src/utils/dm-gen.test.d.ts +1 -0
  24. package/package.json +9 -7
  25. package/rslib.config.ts +81 -0
  26. package/src/ass-gen/__tests__/898651903.xml +1619 -0
  27. package/src/ass-gen/__tests__/898651903.xml.ass +1392 -0
  28. package/src/ass-gen/__tests__/canvas.test.ts +11 -0
  29. package/src/ass-gen/__tests__/generate.test.ts +20 -0
  30. package/src/ass-gen/ass/create.ts +26 -0
  31. package/src/ass-gen/ass/dialogue.ts +91 -0
  32. package/src/ass-gen/ass/event.ts +58 -0
  33. package/src/ass-gen/ass/info.ts +28 -0
  34. package/src/ass-gen/ass/raw.ts +69 -0
  35. package/src/ass-gen/ass/style.ts +67 -0
  36. package/src/ass-gen/config.ts +45 -0
  37. package/src/ass-gen/index.ts +52 -0
  38. package/src/ass-gen/types.ts +52 -0
  39. package/src/ass-gen/util/color.ts +55 -0
  40. package/src/ass-gen/util/danconvert.ts +36 -0
  41. package/src/ass-gen/util/index.ts +10 -0
  42. package/src/ass-gen/util/lang.ts +35 -0
  43. package/src/ass-gen/util/layout.ts +238 -0
  44. package/src/index.test.ts +9 -0
  45. package/src/index.ts +111 -0
  46. package/src/utils/dm-gen.test.ts +66 -0
  47. package/src/utils/dm-gen.ts +91 -11
  48. package/tsconfig.json +7 -7
  49. package/dist/index.min.js +0 -1
  50. package/rollup.config.mjs +0 -51
  51. /package/dist/{index.test.d.ts → src/ass-gen/__tests__/canvas.test.d.ts} +0 -0
  52. /package/dist/{proto → src/proto}/gen/bili/dm_pb.d.ts +0 -0
  53. /package/dist/{proto → src/proto}/gen/danuni_pb.d.ts +0 -0
  54. /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
  }
@@ -342,6 +447,12 @@ export class UniPool {
342
447
  }),
343
448
  }
344
449
  }
450
+ static fromASS(ass: string) {
451
+ return parseAssRawField(ass)
452
+ }
453
+ toASS(options: AssGenOptions = { substyle: {} }): string {
454
+ return generateASS(this, options)
455
+ }
345
456
  }
346
457
 
347
458
  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
+ })
@@ -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
- // seg: {
198
- // st: number //开始时刻
199
- // } //起止时间(ms)
200
- duration: number //持续时间
201
- type: ExtraDanUniChapterType
202
- // action: ExtraDanUniChapterAction
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
- return (typeof extra === 'object' ? extra : {}) as Extra
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
- // "resolveJsonModule": true, /* Enable importing .json files. */
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 */