1+ // BV/AV互转算法常量
2+ // 参考 https://github.com/TGSAN/bv2av.js/blob/master/bv2av.js
3+ const TABLE = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf' ;
4+ const MAX_AVID = 1n << 51n ;
5+ const BASE = 58n ;
6+ const BVID_LEN = 12n ;
7+ const XOR = 23442827791579n ;
8+ const MASK = 2251799813685247n ;
9+
10+ // 构建字符索引
11+ const charIndex : Record < string , number > = { } ;
12+ for ( let i = 0 ; i < Number ( BASE ) ; i ++ ) {
13+ charIndex [ TABLE [ i ] ] = i ;
14+ }
15+
16+ /**
17+ * BV号转AV号
18+ * @param bvid BV号 (如: BVfx411c7Z8)
19+ * @returns AV号 (如: av114514)
20+ */
21+ export function bvToAv ( bvid : string ) : string {
22+ if ( ! bvid || typeof bvid !== 'string' ) {
23+ throw new Error ( 'Invalid BV ID' ) ;
24+ }
25+
26+ // 移除可能的前缀并验证格式
27+ const cleanBvid = bvid . replace ( / ^ ( b v | B V ) / i, '' ) ;
28+ if ( cleanBvid . length !== 10 ) {
29+ throw new Error ( 'Invalid BV ID format' ) ;
30+ }
31+
32+ const fullBvid = 'BV' + cleanBvid ;
33+ const chars = fullBvid . split ( '' ) ;
34+
35+ // 交换字符位置
36+ [ chars [ 3 ] , chars [ 9 ] ] = [ chars [ 9 ] , chars [ 3 ] ] ;
37+ [ chars [ 4 ] , chars [ 7 ] ] = [ chars [ 7 ] , chars [ 4 ] ] ;
38+
39+ // 计算av号
40+ let temp = 0n ;
41+ for ( const char of chars . slice ( 3 ) ) {
42+ const idx = charIndex [ char ] ;
43+ if ( idx === undefined ) {
44+ throw new Error ( 'Invalid character in BV ID' ) ;
45+ }
46+ temp = temp * BASE + BigInt ( idx ) ;
47+ }
48+
49+ const avid = ( temp & MASK ) ^ XOR ;
50+ return `av${ avid } ` ;
51+ }
52+
53+ /**
54+ * AV号转BV号
55+ * @param avid AV号 (如: av114514 或 114514)
56+ * @returns BV号 (如: BVfx411c7Z8)
57+ */
58+ export function avToBv ( avid : string | number ) : string {
59+ let cleanAvid : string ;
60+
61+ if ( typeof avid === 'string' ) {
62+ cleanAvid = avid . replace ( / ^ a v / i, '' ) ;
63+ } else if ( typeof avid === 'number' ) {
64+ cleanAvid = avid . toString ( ) ;
65+ } else {
66+ throw new Error ( 'Invalid AV ID' ) ;
67+ }
68+
69+ const avidBigInt = BigInt ( cleanAvid ) ;
70+ if ( avidBigInt <= 0n || avidBigInt >= MAX_AVID ) {
71+ throw new Error ( 'AV ID out of range' ) ;
72+ }
73+
74+ const result = [ 'B' , 'V' , '1' , '' , '' , '' , '' , '' , '' , '' , '' , '' ] ;
75+ let idx = BVID_LEN - 1n ;
76+ let temp = ( MAX_AVID | avidBigInt ) ^ XOR ;
77+
78+ while ( temp !== 0n ) {
79+ result [ Number ( idx ) ] = TABLE [ Number ( temp % BASE ) ] ;
80+ temp /= BASE ;
81+ idx -= 1n ;
82+ }
83+
84+ // 交换字符位置
85+ [ result [ 3 ] , result [ 9 ] ] = [ result [ 9 ] , result [ 3 ] ] ;
86+ [ result [ 4 ] , result [ 7 ] ] = [ result [ 7 ] , result [ 4 ] ] ;
87+
88+ return result . join ( '' ) ;
89+ }
90+
91+ /**
92+ * 标准化视频ID为AV号格式,用于去重比较
93+ * @param videoId 视频ID (BV号或AV号)
94+ * @returns 标准化的AV号
95+ */
96+ export function normalizeVideoId ( videoId : string ) : string {
97+ if ( ! videoId || typeof videoId !== 'string' ) {
98+ return videoId ;
99+ }
100+
101+ try {
102+ // 如果是BV号,转换为AV号
103+ if ( / ^ b v / i. test ( videoId ) ) {
104+ return bvToAv ( videoId ) ;
105+ }
106+
107+ // 如果是AV号,确保格式统一
108+ if ( / ^ a v / i. test ( videoId ) ) {
109+ const cleanAv = videoId . replace ( / ^ a v / i, '' ) ;
110+ return `av${ cleanAv } ` ;
111+ }
112+
113+ // 如果是纯数字,当作AV号处理
114+ if ( / ^ \d + $ / . test ( videoId ) ) {
115+ return `av${ videoId } ` ;
116+ }
117+
118+ return videoId ;
119+ } catch ( error ) {
120+ // 转换失败时返回原值
121+ return videoId ;
122+ }
123+ }
0 commit comments