1const timeReg = /\[[\d:.]+\]/g; 2const metaReg = /\[(.+):(.+)\]/g; 3 4type LyricMeta = Record<string, any>; 5 6interface IOptions { 7 musicItem?: IMusic.IMusicItem; 8 lyricSource?: ILyric.ILyricSource; 9 translation?: string; 10 extra?: Record<string, any>; 11} 12 13export interface IParsedLrcItem { 14 /** 时间 s */ 15 time: number; 16 /** 歌词 */ 17 lrc: string; 18 /** 翻译 */ 19 translation?: string; 20 /** 位置 */ 21 index: number; 22} 23 24export default class LyricParser { 25 private _musicItem?: IMusic.IMusicItem; 26 27 private meta: LyricMeta; 28 private lrcItems: Array<IParsedLrcItem>; 29 30 private extra: Record<string, any>; 31 32 private lastSearchIndex = 0; 33 34 public hasTranslation = false; 35 public lyricSource?: ILyric.ILyricSource; 36 37 get musicItem() { 38 return this._musicItem; 39 } 40 41 constructor(raw: string, options?: IOptions) { 42 // init 43 this._musicItem = options?.musicItem; 44 this.extra = options?.extra || {}; 45 this.lyricSource = options?.lyricSource; 46 47 let translation = options?.translation; 48 if (!raw && translation) { 49 raw = translation; 50 translation = undefined; 51 } 52 53 const {lrcItems, meta} = this.parseLyricImpl(raw); 54 if (this.extra.offset) { 55 meta.offset = (meta.offset ?? 0) + this.extra.offset; 56 } 57 this.meta = meta; 58 this.lrcItems = lrcItems; 59 60 if (translation) { 61 this.hasTranslation = true; 62 const transLrcItems = this.parseLyricImpl(translation).lrcItems; 63 64 // 2 pointer 65 let p1 = 0; 66 let p2 = 0; 67 68 while (p1 < this.lrcItems.length) { 69 const lrcItem = this.lrcItems[p1]; 70 while ( 71 transLrcItems[p2].time < lrcItem.time && 72 p2 < transLrcItems.length - 1 73 ) { 74 ++p2; 75 } 76 if (transLrcItems[p2].time === lrcItem.time) { 77 lrcItem.translation = transLrcItems[p2].lrc; 78 } else { 79 lrcItem.translation = ''; 80 } 81 82 ++p1; 83 } 84 } 85 } 86 87 getPosition(position: number): IParsedLrcItem | null { 88 position = position - (this.meta?.offset ?? 0); 89 let index; 90 /** 最前面 */ 91 if (!this.lrcItems[0] || position < this.lrcItems[0].time) { 92 this.lastSearchIndex = 0; 93 return null; 94 } 95 for ( 96 index = this.lastSearchIndex; 97 index < this.lrcItems.length - 1; 98 ++index 99 ) { 100 if ( 101 position >= this.lrcItems[index].time && 102 position < this.lrcItems[index + 1].time 103 ) { 104 this.lastSearchIndex = index; 105 return this.lrcItems[index]; 106 } 107 } 108 109 for (index = 0; index < this.lastSearchIndex; ++index) { 110 if ( 111 position >= this.lrcItems[index].time && 112 position < this.lrcItems[index + 1].time 113 ) { 114 this.lastSearchIndex = index; 115 return this.lrcItems[index]; 116 } 117 } 118 119 index = this.lrcItems.length - 1; 120 this.lastSearchIndex = index; 121 return this.lrcItems[index]; 122 } 123 124 getLyricItems() { 125 return this.lrcItems; 126 } 127 128 getMeta() { 129 return this.meta; 130 } 131 132 toString(options?: { 133 withTimestamp?: boolean; 134 type?: 'raw' | 'translation'; 135 }) { 136 const {type = 'raw', withTimestamp = true} = options || {}; 137 138 if (withTimestamp) { 139 return this.lrcItems 140 .map( 141 item => 142 `${this.timeToLrctime(item.time)} ${ 143 type === 'raw' ? item.lrc : item.translation 144 }`, 145 ) 146 .join('\r\n'); 147 } else { 148 return this.lrcItems 149 .map(item => (type === 'raw' ? item.lrc : item.translation)) 150 .join('\r\n'); 151 } 152 } 153 154 /** [xx:xx.xx] => x s */ 155 private parseTime(timeStr: string): number { 156 let result = 0; 157 const nums = timeStr.slice(1, timeStr.length - 1).split(':'); 158 for (let i = 0; i < nums.length; ++i) { 159 result = result * 60 + +nums[i]; 160 } 161 return result; 162 } 163 /** x s => [xx:xx.xx] */ 164 private timeToLrctime(sec: number) { 165 const min = Math.floor(sec / 60); 166 sec = sec - min * 60; 167 const secInt = Math.floor(sec); 168 const secFloat = sec - secInt; 169 return `[${min.toFixed(0).padStart(2, '0')}:${secInt 170 .toString() 171 .padStart(2, '0')}.${secFloat.toFixed(2).slice(2)}]`; 172 } 173 174 private parseMetaImpl(metaStr: string) { 175 if (metaStr === '') { 176 return {}; 177 } 178 const metaArr = metaStr.match(metaReg) ?? []; 179 const meta: any = {}; 180 let k, v; 181 for (const m of metaArr) { 182 k = m.substring(1, m.indexOf(':')); 183 v = m.substring(k.length + 2, m.length - 1); 184 if (k === 'offset') { 185 meta[k] = +v / 1000; 186 } else { 187 meta[k] = v; 188 } 189 } 190 return meta; 191 } 192 193 private parseLyricImpl(raw: string) { 194 raw = raw.trim(); 195 const rawLrcItems: Array<IParsedLrcItem> = []; 196 const rawLrcs = raw.split(timeReg) ?? []; 197 const rawTimes = raw.match(timeReg) ?? []; 198 const len = rawTimes.length; 199 200 const meta = this.parseMetaImpl(rawLrcs[0].trim()); 201 rawLrcs.shift(); 202 203 let counter = 0; 204 let j, lrc; 205 for (let i = 0; i < len; ++i) { 206 counter = 0; 207 while (rawLrcs[0] === '') { 208 ++counter; 209 rawLrcs.shift(); 210 } 211 lrc = rawLrcs[0]?.trim?.() ?? ''; 212 for (j = i; j < i + counter; ++j) { 213 rawLrcItems.push({ 214 time: this.parseTime(rawTimes[j]), 215 lrc, 216 index: j, 217 }); 218 } 219 i += counter; 220 if (i < len) { 221 rawLrcItems.push({ 222 time: this.parseTime(rawTimes[i]), 223 lrc, 224 index: j, 225 }); 226 } 227 rawLrcs.shift(); 228 } 229 let lrcItems = rawLrcItems.sort((a, b) => a.time - b.time); 230 if (lrcItems.length === 0 && raw.length) { 231 lrcItems = raw.split('\n').map((_, index) => ({ 232 time: 0, 233 lrc: _, 234 index, 235 })); 236 } 237 238 return { 239 lrcItems, 240 meta, 241 }; 242 } 243} 244