@dan-uni/dan-any 0.0.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.
@@ -0,0 +1,42 @@
1
+ syntax = "proto3";
2
+
3
+ package danuni.danmaku.v1;
4
+
5
+ import public "google/protobuf/timestamp.proto";
6
+
7
+ enum Mode {
8
+ Normal = 0;
9
+ Bottom = 1;
10
+ Top = 2;
11
+ Reverse = 3;
12
+ Ext = 4;
13
+ }
14
+
15
+ enum Pool {
16
+ Def = 0;
17
+ Sub = 1;
18
+ Adv = 2;
19
+ Ix = 3;
20
+ }
21
+
22
+ message Danmaku {
23
+ string FCID = 1;
24
+ string DMID = 2;
25
+ int32 progress = 3;
26
+ Mode mode = 4;
27
+ int32 fontsize = 5;
28
+ int32 color = 6;
29
+ string senderID = 7;
30
+ string content = 8;
31
+ google.protobuf.Timestamp ctime = 9;
32
+ int32 weight = 10;
33
+ Pool pool = 11;
34
+ repeated string attr = 12;
35
+ string platform = 13;
36
+ optional string SPMO = 14;
37
+ optional string extra = 15;
38
+ }
39
+
40
+ message DanmakuReply {
41
+ repeated Danmaku danmakus = 1;
42
+ }
@@ -0,0 +1,638 @@
1
+ import type { DM_JSON_BiliCommandGrpc } from '..'
2
+ import type { platfrom } from './id-gen'
3
+
4
+ import { createDMID, domainPreset, UniID as ID, platforms } from './id-gen'
5
+
6
+ const BigIntSerializer = (k: string, v: any) =>
7
+ typeof v === 'bigint' ? v.toString() : v
8
+
9
+ class SetBin {
10
+ constructor(public bin: number) {}
11
+ set1(bit: number) {
12
+ this.bin |= 1 << bit
13
+ }
14
+ set0(bit: number) {
15
+ this.bin &= ~(1 << bit)
16
+ }
17
+ }
18
+ const toBits = (number: number) => {
19
+ // 低速方案
20
+ // return [...number.toString(2)].map(Number)
21
+ // 高速方案,位运算允许范围内更快
22
+ const bits: boolean[] = []
23
+ do {
24
+ bits.unshift(!!(number & 1)) // boolean[]
25
+ // bits.unshift(number & 1) // (0|1)[]
26
+ number >>= 1
27
+ } while (number)
28
+ return bits
29
+ }
30
+
31
+ export type DMAttr =
32
+ | 'Protect'
33
+ | 'FromLive'
34
+ | 'HighLike'
35
+ | 'Compatible'
36
+ | 'Reported'
37
+ const DMAttrUtils = {
38
+ fromBin(bin: number = 0, format?: platfrom) {
39
+ const array = toBits(bin),
40
+ attr: DMAttr[] = []
41
+ if (format === 'bili') {
42
+ if (array[0]) attr.push('Protect')
43
+ if (array[1]) attr.push('FromLive')
44
+ if (array[2]) attr.push('HighLike')
45
+ }
46
+ return attr
47
+ },
48
+ toBin(
49
+ attr: DMAttr[] = [],
50
+ /**
51
+ * 对于二进制格式的读取,应该分别读取各位,
52
+ * 但由于不知道B站及其它使用该参数程序的读取逻辑,
53
+ * 所以单独提供 bili 格式
54
+ */
55
+ format?: platfrom,
56
+ ) {
57
+ const bin = new SetBin(0)
58
+ if (format === 'bili') {
59
+ if (attr.includes('Protect')) bin.set1(0)
60
+ if (attr.includes('FromLive')) bin.set1(1)
61
+ if (attr.includes('HighLike')) bin.set1(2)
62
+ }
63
+ return bin.bin
64
+ },
65
+ }
66
+ // class DMAttr {
67
+ // constructor(
68
+ // /**
69
+ // * 保护
70
+ // */
71
+ // public protect: boolean = false,
72
+ // /**
73
+ // * 直播
74
+ // */
75
+ // public live: boolean = false,
76
+ // /**
77
+ // * 高赞
78
+ // */
79
+ // public highpraise: boolean = false,
80
+ // ) {}
81
+ // toBin(
82
+ // /**
83
+ // * 对于二进制格式的读取,应该分别读取各位,
84
+ // * 但由于不知道B站及其它使用该参数程序的读取逻辑,
85
+ // * 所以单独提供 bili 格式
86
+ // */
87
+ // format?: platfrom,
88
+ // ) {
89
+ // const bin = new SetBin(0)
90
+ // if (this.protect) bin.set1(0)
91
+ // if (this.live) bin.set1(1)
92
+ // if (this.highpraise) bin.set1(2)
93
+ // if (format === 'bili') return bin.bin
94
+ // return bin.bin
95
+ // }
96
+ // static fromBin(bin: number = 0, format?: platfrom) {
97
+ // const array = toBits(bin)
98
+ // if (format === 'bili') {
99
+ // return new DMAttr(array[0], array[1], array[2])
100
+ // } else {
101
+ // return new DMAttr(array[0], array[1], array[2])
102
+ // }
103
+ // }
104
+ // }
105
+
106
+ interface DMBili {
107
+ id: bigint // xml 7
108
+ progress: number // xml 0
109
+ mode: number // xml 1
110
+ fontsize: number // xml 2
111
+ color: number // xml 3
112
+ midHash: string // xml 6
113
+ /**
114
+ * 特殊类型解析:
115
+ * - [ohh] : /oh{2,}/gi
116
+ * - [前方高能]
117
+ * - [...] (JS数组) : 高级弹幕
118
+ */
119
+ content: string // xml content
120
+ ctime: bigint // xml 4
121
+ pool: number // xml 5
122
+ weight?: number // xml 8
123
+ action?: string
124
+ idStr?: string
125
+ attr?: number
126
+ animation?: string
127
+ extra?: string
128
+ colorful?: number
129
+ type?: number
130
+ oid?: bigint
131
+ }
132
+ interface DMBiliCommand extends DM_JSON_BiliCommandGrpc {}
133
+ interface DMDplayer {
134
+ /**
135
+ * 进度(秒)
136
+ */
137
+ progress: number
138
+ mode: number
139
+ color: number
140
+ midHash: string
141
+ content: string
142
+ }
143
+ interface DMArtplayer {
144
+ /**
145
+ * 进度(秒)
146
+ */
147
+ progress: number
148
+ mode: number
149
+ color: number
150
+ content: string
151
+ border?: boolean
152
+ style?: object
153
+ }
154
+ interface DMDDplay {
155
+ cid: number
156
+ /**
157
+ * content
158
+ */
159
+ m: string
160
+ /**
161
+ * p[0]
162
+ */
163
+ progress: number
164
+ /**
165
+ * p[1]
166
+ */
167
+ mode: number
168
+ /**
169
+ * p[2]
170
+ */
171
+ color: number
172
+ /**
173
+ * p[3]
174
+ */
175
+ uid: string
176
+ }
177
+
178
+ export interface Extra {
179
+ artplayer?: ExtraArtplayer
180
+ bili?: ExtraBili
181
+ danuni?: ExtraDanUni
182
+ }
183
+ interface ExtraArtplayer {
184
+ style?: object
185
+ border?: boolean
186
+ }
187
+ interface ExtraBili {
188
+ mode: number //原弹幕类型
189
+ pool: number //原弹幕池
190
+ adv?: string
191
+ code?: string
192
+ bas?: string
193
+ command?: DMBiliCommand
194
+ }
195
+ export interface ExtraDanUni {
196
+ chapter?: {
197
+ // seg: {
198
+ // st: number //开始时刻
199
+ // } //起止时间(ms)
200
+ duration: number //持续时间
201
+ type: ExtraDanUniChapterType
202
+ // action: ExtraDanUniChapterAction
203
+ }
204
+ }
205
+ export enum ExtraDanUniChapterType {
206
+ Chapter = 'ch', //其它片段(用于标记章节)
207
+ Review = 'rev', //回顾
208
+ OP = 'op', //片头
209
+ Intermission = 'int', //中场
210
+ ED = 'ed', //片尾
211
+ Preview = 'prvw', //预告
212
+ Cut = 'cut', //删减(删减版中提供删减说明,提供开始位置、长度)
213
+ Duplicates = 'dup', //补档(完整版中指明其它平台中删减位置)
214
+ AdBiz = 'biz', //商业广告
215
+ AdUnpaid = 'promo', //推广(无偿/公益)广告
216
+ }
217
+ export enum ExtraDanUniChapterAction {
218
+ Disabled = -1,
219
+ ShowOverlay,
220
+ ManualSkip,
221
+ AutoSkip,
222
+ }
223
+
224
+ export enum Modes {
225
+ Normal,
226
+ Bottom,
227
+ Top,
228
+ Reverse, //逆向弹幕
229
+ Ext, //需要读取extra的弹幕,用于兼容bili等复杂弹幕
230
+ }
231
+ export enum Pools {
232
+ Def, //默认池
233
+ Sub, //重要池,建议强制加载,含字幕、科普、空降等
234
+ Adv, //高级弹幕专用池,均需读取extra
235
+ Ix, //互动池
236
+ }
237
+
238
+ export type ctime = string | number | bigint | Date
239
+ // enum ctimeFmt {
240
+ // bigintStr = 'bigintStr',
241
+ // dateObj = 'dateObj',
242
+ // dateStr = 'dateStr',
243
+ // }
244
+
245
+ /**
246
+ * 所有 number/bigint 值设为0自动转换为默认
247
+ */
248
+ export class UniDM {
249
+ /**
250
+ * 同步时确认位置的参数
251
+ */
252
+ // syncAnchor = BigInt(Date.now())
253
+ constructor(
254
+ /**
255
+ * FCID
256
+ */
257
+ public FCID: string,
258
+ /**
259
+ * 弹幕出现位置(单位ms)
260
+ */
261
+ public progress: number = 0,
262
+ /**
263
+ * 类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)
264
+ */
265
+ public mode: Modes = Modes.Normal,
266
+ /**
267
+ * 字号
268
+ * @default 25
269
+ * - 18:小
270
+ * - 25:标准
271
+ * - 36:大
272
+ */
273
+ public fontsize: number = 25,
274
+ /**
275
+ * 颜色
276
+ * @description 为DEC值(十进制RGB888值),默认白色
277
+ * @default 16777215
278
+ */
279
+ public color: number = 16777215,
280
+ /**
281
+ * 发送者 senderID
282
+ */
283
+ public senderID: string = ID.fromNull().toString(),
284
+ /**
285
+ * 正文
286
+ */
287
+ public content: string = '',
288
+ /**
289
+ * 发送时间
290
+ */
291
+ // public ctime: bigint = BigInt(Date.now()),
292
+ public ctime: Date = new Date(),
293
+ /**
294
+ * 权重 用于屏蔽等级 区间:[1,10]
295
+ * @description 参考B站,源弹幕有该参数则直接利用,
296
+ * 本实现默认取5,再经过ruleset匹配加减分数
297
+ * @description 特殊情况下接受值为0,即设置0需转换为默认权重(5)
298
+ */
299
+ public weight: number = 5,
300
+ /**
301
+ * 弹幕池 0:普通池 1:字幕池 2:特殊池(代码/BAS弹幕) 3:互动池(互动弹幕中选择投票快速发送的弹幕)
302
+ */
303
+ public pool: Pools = Pools.Def,
304
+ /**
305
+ * 弹幕属性位(bin求AND)
306
+ * bit0:保护 bit1:直播 bit2:高赞
307
+ */
308
+ public attr: DMAttr[] = [],
309
+ /**
310
+ * 初始来源平台
311
+ * `danuni`与任意空值(可隐式转换为false的值)等价
312
+ */
313
+ public platform?: platfrom | string,
314
+ /**
315
+ * Same Platform Multiple Origin
316
+ * @description 解决B站等同一番剧存在港澳台站、多语言配音(不同一CID)的问题,同时方便过滤
317
+ * @description 示例:
318
+ * - main: 主站
319
+ * - hm: 仅港澳
320
+ * - t: 仅台
321
+ * - hmt: 仅港澳台
322
+ * - lang:{ISO语言代号}: 多语言
323
+ */
324
+ public SPMO?: string,
325
+ /**
326
+ * 弹幕原始数据(不推荐使用)
327
+ * @description 适用于无法解析的B站代码弹幕、Artplayer弹幕样式等
328
+ * @description 初步约定:
329
+ * - Artplayer: style不为空时,将其JSON.stringify()存入
330
+ */
331
+ public extraStr?: string,
332
+ public DMID?: string,
333
+ ) {
334
+ //TODO 引入class-validator
335
+ if (progress < 0) this.progress = 0
336
+ if (mode < Modes.Normal || mode > Modes.Ext) this.mode = Modes.Normal
337
+ if (fontsize <= 0) this.fontsize = 25
338
+ if (color <= 0) this.color = 16777215 //虽然不知道为0是否为可用值,但过为少见,利用其作为默认位
339
+ // if (ctime <= 0n) this.ctime = BigInt(Date.now())
340
+ if (weight <= 0 || weight > 10) this.weight = 5
341
+ if (pool < Pools.Def || pool > Pools.Ix) this.pool = Pools.Def
342
+ // if (attr < 0 || attr > 0b111) this.attr = 0
343
+ if (!DMID) DMID = this.toDMID()
344
+ }
345
+ get extra() {
346
+ const extra = JSON.parse(this.extraStr || '{}')
347
+ return (typeof extra === 'object' ? extra : {}) as Extra
348
+ }
349
+ get isFrom3rdPlatform() {
350
+ if (this.platform && platforms.includes(this.platform as platfrom))
351
+ return true
352
+ else return false
353
+ }
354
+ /**
355
+ * 弹幕id
356
+ * @description sha3-256(content+senderID+ctime)截取前8位
357
+ * @description 同一FCID下唯一
358
+ */
359
+ toDMID() {
360
+ return createDMID(this.content, this.senderID, this.ctime)
361
+ }
362
+ downgradeAdvcancedDan() {
363
+ if (!this.extra) return this
364
+ else {
365
+ // TODO 分别对 mode7/8/9 command artplayer等正常播放器无法绘制的弹幕做降级处理
366
+ }
367
+ }
368
+ static transCtime(oriCtime: ctime) {
369
+ if (typeof oriCtime === 'number') return new Date(oriCtime)
370
+ else if (typeof oriCtime === 'bigint') return new Date(Number(oriCtime))
371
+ else if (typeof oriCtime === 'string') {
372
+ if (/^\d+n$/.test(oriCtime))
373
+ return new Date(Number(oriCtime.slice(0, -1)))
374
+ // else return Date.parse(oriCtime)
375
+ else return new Date(oriCtime)
376
+ }
377
+ // } else if (typeof oriCtime === 'object') return BigInt(oriCtime.getTime())
378
+ else return oriCtime // BigInt(Date.now())
379
+ }
380
+ // static reviveCtime(ctime: bigint, fmt: ctimeFmt = ctimeFmt.bigintStr) {
381
+ // if (fmt === ctimeFmt.dateObj || fmt === ctimeFmt.dateStr) {
382
+ // const date = new Date(Number(ctime))
383
+ // if (fmt === ctimeFmt.dateStr) return date.toString()
384
+ // else return date
385
+ // } else return ctime.toString() + 'n'
386
+ // }
387
+ static transMode(
388
+ oriMode: number,
389
+ fmt: 'bili' | 'dplayer' | 'artplayer' | 'ddplay',
390
+ ): Modes {
391
+ let mode = Modes.Normal
392
+ switch (fmt) {
393
+ case 'bili':
394
+ switch (oriMode) {
395
+ case 4:
396
+ mode = Modes.Bottom
397
+ break
398
+ case 5:
399
+ mode = Modes.Top
400
+ break
401
+ case 6:
402
+ mode = Modes.Reverse
403
+ break
404
+ case 7:
405
+ mode = Modes.Ext
406
+ break
407
+ case 8:
408
+ mode = Modes.Ext
409
+ break
410
+ case 9:
411
+ mode = Modes.Ext
412
+ break
413
+ }
414
+ break
415
+
416
+ case 'dplayer':
417
+ if (oriMode === 1) mode = Modes.Top
418
+ else if (oriMode === 2) mode = Modes.Bottom
419
+ break
420
+
421
+ case 'artplayer':
422
+ if (oriMode === 1) mode = Modes.Top
423
+ else if (oriMode === 2) mode = Modes.Bottom
424
+ break
425
+
426
+ case 'ddplay':
427
+ // 弹幕模式:1-普通弹幕,4-底部弹幕,5-顶部弹幕
428
+ // 其适配为bili格式子集
429
+ mode = this.transMode(oriMode, 'bili')
430
+ break
431
+
432
+ default:
433
+ mode = Modes.Normal
434
+ break
435
+ }
436
+ return mode
437
+ }
438
+ static fromBili(args: DMBili, SPMO?: string, cid?: bigint) {
439
+ interface TExtra extends Extra {
440
+ bili: ExtraBili
441
+ }
442
+ if (args.oid && !cid) cid = args.oid
443
+ const FCID = ID.fromBili({ cid }),
444
+ senderID = ID.fromBili({ midHash: args.midHash })
445
+ let mode = Modes.Normal
446
+ const pool = args.pool, //暂时不做处理,兼容bili的pool格式
447
+ extra: TExtra = { bili: { mode: args.mode, pool: args.pool } }
448
+ //重复 transMode ,但此处有关于extra的额外处理
449
+ switch (args.mode) {
450
+ case 4:
451
+ mode = Modes.Bottom
452
+ break
453
+ case 5:
454
+ mode = Modes.Top
455
+ break
456
+ case 6:
457
+ mode = Modes.Reverse
458
+ break
459
+ case 7:
460
+ mode = Modes.Ext
461
+ extra.bili.adv = args.content
462
+ break
463
+ case 8:
464
+ mode = Modes.Ext
465
+ extra.bili.code = args.content
466
+ break
467
+ case 9:
468
+ mode = Modes.Ext
469
+ extra.bili.bas = args.content
470
+ break
471
+
472
+ default:
473
+ mode = Modes.Normal
474
+ break
475
+ }
476
+ // if (args.mode === 7) extra.bili.adv = args.content
477
+ // else if (args.mode === 8) extra.bili.code = args.content
478
+ // else if (args.mode === 9) extra.bili.bas = args.content
479
+ return new UniDM(
480
+ FCID.toString(),
481
+ args.progress,
482
+ mode,
483
+ args.fontsize,
484
+ args.color,
485
+ senderID.toString(),
486
+ args.content,
487
+ this.transCtime(args.ctime),
488
+ args.weight ? args.weight : pool === Pools.Ix ? 1 : 0,
489
+ pool,
490
+ DMAttrUtils.fromBin(args.attr, 'bili'),
491
+ domainPreset.bili,
492
+ SPMO,
493
+ // 需改进,7=>advanced 8=>code 9=>bas 互动=>command
494
+ // 同时塞进无法/无需直接解析的数据
495
+ // 另开一个解析器,为大部分播放器(无法解析该类dm)做文本类型降级处理
496
+ args.mode >= 7 ? JSON.stringify(extra, BigIntSerializer) : undefined,
497
+ )
498
+ }
499
+ static fromBiliCommand(args: DMBiliCommand, SPMO?: string, cid?: bigint) {
500
+ if (args.oid && !cid) cid = args.oid
501
+ const FCID = ID.fromBili({ cid }),
502
+ senderID = ID.fromBili({ mid: args.mid })
503
+ return new UniDM(
504
+ FCID.toString(),
505
+ args.progress,
506
+ Modes.Ext,
507
+ 0,
508
+ 0,
509
+ senderID.toString(),
510
+ args.content,
511
+ // BigInt(Date.parse(args.ctime + ' GMT+0800')), // 无视本地时区,按照B站的东8区计算时间
512
+ new Date(`${args.ctime} GMT+0800`), // 无视本地时区,按照B站的东8区计算时间
513
+ 10,
514
+ Pools.Adv,
515
+ ['Protect'],
516
+ domainPreset.bili,
517
+ SPMO,
518
+ JSON.stringify(
519
+ {
520
+ bili: {
521
+ command: args,
522
+ },
523
+ },
524
+ BigIntSerializer,
525
+ ),
526
+ )
527
+ }
528
+ static fromDplayer(args: DMDplayer, playerID: string, domain: string) {
529
+ const FCID = ID.fromUnknown(playerID, domain),
530
+ senderID = ID.fromUnknown(args.midHash, domain)
531
+ return new UniDM(
532
+ FCID.toString(),
533
+ args.progress,
534
+ this.transMode(args.mode, 'dplayer'),
535
+ 0,
536
+ args.color,
537
+ senderID.toString(),
538
+ args.content,
539
+ new Date(),
540
+ 0,
541
+ 0,
542
+ [],
543
+ domain,
544
+ )
545
+ }
546
+ toDplayer(): DMDplayer {
547
+ let mode = 0
548
+ if (this.mode === Modes.Top) mode = 1
549
+ else if (this.mode === Modes.Bottom) mode = 2
550
+ return {
551
+ mode,
552
+ progress: this.progress,
553
+ color: this.color,
554
+ midHash: this.senderID,
555
+ content: this.content,
556
+ }
557
+ }
558
+ static fromArtplayer(args: DMArtplayer, playerID: string, domain: string) {
559
+ const FCID = ID.fromUnknown(playerID, domain),
560
+ senderID = ID.fromUnknown('', domain)
561
+ let extra = args.border
562
+ ? ({ artplayer: { border: args.border, style: {} } } as Extra)
563
+ : undefined
564
+ if (args.style) {
565
+ if (extra)
566
+ extra = {
567
+ ...extra,
568
+ artplayer: { ...extra.artplayer, style: args.style },
569
+ }
570
+ else extra = { artplayer: { style: args.style } }
571
+ }
572
+ return new UniDM(
573
+ FCID.toString(),
574
+ args.progress,
575
+ this.transMode(args.mode, 'artplayer'),
576
+ 0,
577
+ args.color,
578
+ senderID.toString(),
579
+ args.content,
580
+ new Date(),
581
+ 0,
582
+ 0,
583
+ [],
584
+ domain,
585
+ undefined,
586
+ JSON.stringify(extra),
587
+ )
588
+ }
589
+ toArtplayer(): DMArtplayer {
590
+ let mode = 0
591
+ if (this.mode === Modes.Top) mode = 1
592
+ else if (this.mode === Modes.Bottom) mode = 2
593
+ return {
594
+ progress: this.progress,
595
+ mode,
596
+ color: this.color,
597
+ content: this.content,
598
+ style: this.extra.artplayer?.style,
599
+ }
600
+ }
601
+ static fromDDplay(
602
+ args: DMDDplay,
603
+ episodeId: string,
604
+ domain = domainPreset.ddplay,
605
+ ) {
606
+ const FCID = ID.fromUnknown(episodeId, domain)
607
+ return new UniDM(
608
+ FCID.toString(),
609
+ args.progress,
610
+ this.transMode(args.mode, 'ddplay'),
611
+ 0,
612
+ args.color,
613
+ args.uid,
614
+ args.m,
615
+ new Date(),
616
+ 0,
617
+ 0,
618
+ [],
619
+ domain,
620
+ undefined,
621
+ undefined,
622
+ args.cid.toString(), //无需 new ID() 获取带suffix的ID
623
+ )
624
+ }
625
+ toDDplay(): DMDDplay {
626
+ let mode = 1
627
+ if (this.mode === Modes.Top) mode = 5
628
+ else if (this.mode === Modes.Bottom) mode = 4
629
+ return {
630
+ progress: this.progress,
631
+ mode,
632
+ color: this.color,
633
+ uid: this.senderID,
634
+ m: this.content,
635
+ cid: Number(this.DMID) || 1,
636
+ }
637
+ }
638
+ }