feat(qs): 汽水音乐平台接入——匿名搜索/试听、抖音扫码登录、解密流代理
- sdk/qs 平台模块(安卓参数指纹匿名搜索)+ sdk/apis/qs 取流: 匿名 30s 试听,登录态走签名 track_v2 + spade key AES-CTR 解密 - /login/qs/qr 抖音扫码登录(授权后 sessionid 解锁完整曲目) - /stream/qs/:id 解密流代理,/url 返回的相对地址按请求 host 补全 - 回归测试覆盖 kw 双通道(VIP 真音频/降级链)与 qs 试听;start.bat 去掉 SOURCE_ENABLED 直启
This commit is contained in:
1 parent
aa4f2eec67
commit
0f9cff2a08
26 files changed
+2172
-9
No files matched your search
@@ -0,0 +1,46 @@
|
||||
// 汽水音乐歌词(逐字时间轴 → LRC/YRC)
|
||||
// 来源: seo_track 的 lyric.content,行格式 `[起(ms),长(ms)]<行内偏移(ms),长(ms),0>字...`
|
||||
// 与网易云 yrc 同族(wy/lyric.ts 的 parseTools 处理 (a,b,c) 括号变体,这里是尖括号)
|
||||
import musicInfo from './musicInfo.ts'
|
||||
|
||||
const lineRe = /^\[(\d+),\d+\]/
|
||||
const wordRe = /<(\d+),(\d+),\d+>([^<]*)/g
|
||||
|
||||
const msFormat = (timeMs: number) => {
|
||||
if (Number.isNaN(timeMs)) return ''
|
||||
let ms = timeMs % 1000
|
||||
timeMs /= 1000
|
||||
const m = parseInt(String(timeMs / 60)).toString().padStart(2, '0')
|
||||
timeMs %= 60
|
||||
const s = parseInt(String(timeMs)).toString().padStart(2, '0')
|
||||
return `[${m}:${s}.${ms}]`
|
||||
}
|
||||
|
||||
const parseTimedLyric = (content: string) => {
|
||||
const lrcLines: string[] = []
|
||||
const yrcLines: string[] = []
|
||||
for (const rawLine of String(content || '').split('\n')) {
|
||||
const line = rawLine.trim()
|
||||
const lineMatch = lineRe.exec(line)
|
||||
if (!lineMatch) continue
|
||||
const timeTag = msFormat(parseInt(lineMatch[1]))
|
||||
if (!timeTag) continue
|
||||
const words = [...line.slice(lineMatch[0].length).matchAll(wordRe)]
|
||||
if (!words.length) continue
|
||||
const text = words.map(word => word[3]).join('')
|
||||
if (!text.trim()) continue // 间奏空行
|
||||
lrcLines.push(`${timeTag}${text}`)
|
||||
yrcLines.push(`${timeTag}${words.map(word => `<${word[1]},${word[2]}>${word[3]}`).join('')}`)
|
||||
}
|
||||
return { lyric: lrcLines.join('\n'), yrc: yrcLines.join('\n') }
|
||||
}
|
||||
|
||||
export default (trackId: string) => {
|
||||
const requestObj = musicInfo(trackId)
|
||||
requestObj.promise = requestObj.promise.then(body => {
|
||||
const info = parseTimedLyric(body?.lyric?.content)
|
||||
if (!info.lyric) return Promise.reject(new Error('获取歌词失败'))
|
||||
return { lyric: info.lyric, tlyric: '', yrc: info.yrc }
|
||||
}) as any
|
||||
return requestObj
|
||||
}
|
||||
Reference in new issue
Block a user