Files
snowy-yang 8c02fb5c15 refactor(sdk): 审计 P2 卫生项清理——请求层收敛与死代码清除
- 删除旧 sdk/request.ts,全部 import 收敛到 sdk/common/request.ts(85 文件)
- 注释死代码 630+ 行、裸 console.log 吞错 6 处、未使用符号 26 处清零
- bili 状态校验去重;utils DNS 预解析失败日志改 warn 带上下文
- CODE-AUDIT.md:第四节 P2 清理记录 + 第八节线索逐条复核结论(kw/kg/tx/wy parse 骨架相似实质各异,不抽公共层)
2026-10-03 20:48:54 +08:00

217 lines
6.6 KiB
TypeScript

import { httpFetch } from '../common/request.ts'
import { eapi } from './utils/crypto.ts'
import type { HttpRequestObject } from '../types.ts'
const eapiRequest = (url: string, data: object) => {
return httpFetch('https://interface3.music.163.com/eapi/song/lyric/v1', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
origin: 'https://music.163.com',
},
form: eapi(url, data),
})
}
const parseTools = {
rxps: {
info: /^{"/,
lineTime: /^\[(\d+),\d+\]/,
wordTime: /\(\d+,\d+,\d+\)/,
wordTimeAll: /(\(\d+,\d+,\d+\))/g,
},
msFormat(timeMs) {
if (Number.isNaN(timeMs)) return ''
let ms = timeMs % 1000
timeMs /= 1000
let m = parseInt((timeMs / 60) as any).toString().padStart(2, '0')
timeMs %= 60
let s = parseInt(timeMs as any).toString().padStart(2, '0')
return `[${m}:${s}.${ms}]`
},
parseLyric(lines) {
const yrcLines: string[] = []
const lrcLines: string[] = []
for (let line of lines) {
line = line.trim()
let result = this.rxps.lineTime.exec(line)
if (!result) {
if (line.startsWith('[offset')) {
yrcLines.push(line)
lrcLines.push(line)
}
continue
}
const startMsTime = parseInt(result[1])
const startTimeStr = this.msFormat(startMsTime)
if (!startTimeStr) continue
let words = line.replace(this.rxps.lineTime, '')
lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`)
let times = words.match(this.rxps.wordTimeAll)
if (!times) continue
times = times.map(time => {
const result = /\((\d+),(\d+),\d+\)/.exec(time)
return `<${Math.max(parseInt(result![1]) - startMsTime, 0)},${result![2]}>`
})
const wordArr = words.split(this.rxps.wordTime)
wordArr.shift()
const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')
yrcLines.push(`${startTimeStr}${newWords}`)
}
return {
lyric: lrcLines.join('\n'),
yrc: yrcLines.join('\n'),
}
},
parseHeaderInfo(str) {
str = str.trim()
str = str.replace(/\r/g, '')
if (!str) return null
const lines = str.split('\n')
return lines.map(line => {
if (!this.rxps.info.test(line)) return line
try {
const info = JSON.parse(line)
const timeTag = this.msFormat(info.t)
return timeTag ? `${timeTag}${info.c.map(t => t.tx).join('')}` : ''
} catch {
return ''
}
})
},
getIntv(interval) {
if (!interval) return 0
if (!interval.includes('.')) interval += '.0'
let arr = interval.split(/:|\./)
while (arr.length < 3) arr.unshift('0')
const [m, s, ms] = arr
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
},
fixTimeTag(lrc, targetlrc) {
let lrcLines = lrc.split('\n')
const targetlrcLines = targetlrc.split('\n')
const timeRxp = /^\[([\d:.]+)\]/
let temp: string[] = []
let newLrc: string[] = []
targetlrcLines.forEach((line) => {
const result = timeRxp.exec(line)
if (!result) return
const words = line.replace(timeRxp, '')
if (!words.trim()) return
const t1 = this.getIntv(result[1])
while (lrcLines.length) {
const lrcLine = lrcLines.shift()!
const lrcLineResult = timeRxp.exec(lrcLine)
if (!lrcLineResult) continue
const t2 = this.getIntv(lrcLineResult[1])
if (Math.abs(t1 - t2) < 100) {
const lrc = line.replace(timeRxp, lrcLineResult[0]).trim()
if (!lrc) continue
newLrc.push(lrc)
break
}
temp.push(lrcLine)
}
lrcLines = [...temp, ...lrcLines]
temp = []
})
return newLrc.join('\n')
},
parse(ylrc, ytlrc, yrlrc, lrc, tlrc, rlrc) {
const info = {
lyric: '',
tlyric: '',
rlyric: '',
yrc: '',
}
if (ylrc) {
let lines = this.parseHeaderInfo(ylrc)
if (lines) {
const result = this.parseLyric(lines)
if (ytlrc) {
const lines = this.parseHeaderInfo(ytlrc)
if (lines) {
// if (lines.length == result.lyricLines.length) {
info.tlyric = this.fixTimeTag(result.lyric, lines.join('\n'))
// } else info.tlyric = lines.join('\n')
}
}
if (yrlrc) {
const lines = this.parseHeaderInfo(yrlrc)
if (lines) {
// if (lines.length == result.lyricLines.length) {
info.rlyric = this.fixTimeTag(result.lyric, lines.join('\n'))
// } else info.rlyric = lines.join('\n')
}
}
const timeRxp = /^\[[\d:.]+\]/
const headers = lines.filter(l => timeRxp.test(l)).join('\n')
info.lyric = `${headers}\n${result.lyric}`
info.yrc = result.yrc
return info
}
}
if (lrc) {
const lines = this.parseHeaderInfo(lrc)
if (lines) info.lyric = lines.join('\n')
}
if (tlrc) {
const lines = this.parseHeaderInfo(tlrc)
if (lines) info.tlyric = lines.join('\n')
}
if (rlrc) {
const lines = this.parseHeaderInfo(rlrc)
if (lines) info.rlyric = lines.join('\n')
}
return info
},
}
// 参考社区通用的歌词时间标签修正思路
const fixTimeLabel = (lrc: string, tlrc?: string, romalrc?: string) => {
if (lrc) {
let newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]')
let newTlrc = tlrc?.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') ?? tlrc
if (newLrc != lrc || newTlrc != tlrc) {
lrc = newLrc
tlrc = newTlrc
if (romalrc) romalrc = romalrc.replace(/\[(\d{2}:\d{2}):(\d{2,3})]/g, '[$1.$2]').replace(/\[(\d{2}:\d{2}\.\d{2})0]/g, '[$1]')
}
}
return { lrc, tlrc, romalrc }
}
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/lyric_new.js
export default (songmid: string): HttpRequestObject => {
const requestObj = eapiRequest('/api/song/lyric/v1', {
id: songmid,
cp: false,
tv: 0,
lv: 0,
rv: 0,
kv: 0,
yv: 0,
ytv: 0,
yrv: 0,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))
const fixTimeLabelLrc = fixTimeLabel(body.lrc.lyric, body.tlyric?.lyric, body.romalrc?.lyric)
const info = parseTools.parse(body.yrc?.lyric, body.ytlrc?.lyric, body.yromalrc?.lyric, fixTimeLabelLrc.lrc, fixTimeLabelLrc.tlrc, fixTimeLabelLrc.romalrc)
if (!info.lyric) return Promise.reject(new Error('Get lyric failed'))
return info
}) as any
return requestObj
}