feat(xm): 喜马拉雅平台接入——匿名搜索/专辑/免费曲直链(hdaa xm-sign+AES-ECB 解密)、cookie 绑定解锁付费

This commit is contained in:
snowy-yang committed 2026-10-02 19:43:09 +08:00
1 parent 44852864e2
commit 5a9915b74c
8 files changed
+717

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
// 喜马拉雅 URL 获取(PC 桌面端 baseInfo 通道)
// 链路:makeXmSign(hdaa 上报) → baseInfo device=win(带 PC 设备 Cookie) → playUrlList
// → decodeWinUrl(AES-ECB, 客户端 4.0.14 asar 密钥) → a.xmcdn.com 带签名直链
// 实测(2026-10-02)匿名免费曲目 HTTP 200 audio/mp4 可播;付费/VIP 曲 ret=1001(需登录,暂未接)
// 档位命名如 M4A_64/MP3_64/M4A_24/MP3_32/AAC_24(音乐内容另有 MP3_128/M4A_128);
// 喜马拉雅以有声内容为主,码率低于音乐平台,音质映射到 '64k'/'128k' 桶
// 统一接口约定:apis('xm').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url }(type 为实际命中档位,可能低于请求值)
import { getTrackInfo } from '../xm/musicInfo.ts'
import { decodeWinUrl } from '../xm/decoder.ts'
// 优先 M4A(同码率体积更小),128 档优先 MP3_128
const QUALITY_PREFERENCE: Record<string, string[]> = {
'128k': ['MP3_128', 'M4A_128', 'M4A_64', 'MP3_64', 'M4A_24', 'MP3_32', 'AAC_24'],
'64k': ['M4A_64', 'MP3_64', 'M4A_24', 'MP3_32', 'AAC_24'],
}
export const supportQualitys = ['64k', '128k']
const bitrateOf = (type: string): number => Number(/(\d+)/.exec(type)?.[1] ?? 0) || 0
const bucketOf = (bitrate: number): string => (bitrate >= 128 ? '128k' : '64k')
export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null as any }
const trackId = String(songInfo.songmid ?? '')
requestObj.promise = (async() => {
const trackInfo = await getTrackInfo(trackId)
const playList: any[] = trackInfo.playUrlList ?? []
if (!playList.length) throw new Error('无可用播放资源(付费/版权内容需登录)')
const preference = QUALITY_PREFERENCE[type] ?? QUALITY_PREFERENCE['128k']
let picked: { type: string, url: string } | null = null
for (const want of preference) {
const hit = playList.find(item => item?.type === want && item.url)
if (hit) {
picked = { type: hit.type, url: hit.url.startsWith('http') ? hit.url : decodeWinUrl(hit.url) }
break
}
}
if (!picked) {
// 兜底:未知档位命名时取码率最高的一条
const best = [...playList].filter(item => item?.url).sort((a, b) => bitrateOf(b.type) - bitrateOf(a.type))[0]
if (!best) throw new Error('无可用播放资源')
picked = { type: best.type, url: best.url.startsWith('http') ? best.url : decodeWinUrl(best.url) }
}
return { type: bucketOf(bitrateOf(picked.type)), url: picked.url }
})()
return requestObj
}
export default { getMusicUrl }