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

+64
View File
@@ -0,0 +1,64 @@
// 喜马拉雅曲目信息(PC 桌面端 baseInfo 通道)
// 接口: https://www.ximalaya.com/mobile-playpage/track/v3/baseInfo/{ms_ts}?device=win&trackId=&trackQualityLevel=1
// 实测(2026-10-02)匿名免费曲目可用,但必须同时满足:
// 1) 携带 PC 设备 Cookie(install_id 与 1&_device 同一 uuid,channel=99&100001,客户端 4.0.14 抓包格式)
// 2) 携带 du_web_sdk 真实 xm-sign(hdaa 上报取 cadd/sid)
// 缺 Cookie 或换 device=www2 均返回 ret=1001 系统繁忙。
// 响应 trackInfo 顶层挂载(playUrlList/title/cover*/duration/isPaid/isAuthorized/isXimiUhqTrack),
// playUrlList[].url 为 AES-ECB 加密地址(key 取自官方客户端 4.0.14 asar),见 decoder.ts
import crypto from 'node:crypto'
import { httpFetch } from '../request.ts'
import { makeXmSign, XM_UA } from './sign.ts'
import { getCredentials } from '../auth/store.ts'
const BASE_INFO_URL = 'https://www.ximalaya.com/mobile-playpage/track/v3/baseInfo/'
// 进程级设备标识(install_id 与 1&_device 共用同一 uuid,稳定复用)
const deviceUuid = crypto.randomUUID()
const PC_DEVICE_COOKIE = `install_id=${deviceUuid}; channel=99&100001; 1&_device=win32&${deviceUuid}&4.0.14`
/** 登录态 Cookie(/login/xm 绑定,须含 1&_token);与 PC 设备 Cookie 合并后上送 */
export const buildRequestCookie = (): string => {
const loginCookie = (getCredentials('xm') as any)?.cookie || ''
return loginCookie ? `${loginCookie}; ${PC_DEVICE_COOKIE}` : PC_DEVICE_COOKIE
}
export const coverUrl = (trackInfo: any): string | null => {
const path = trackInfo?.coverMiddle || trackInfo?.coverLarge || trackInfo?.coverSmall
if (!path) return null
const s = String(path)
return s.startsWith('http') ? s : `https://imagev2.xmcdn.com/${s}`
}
/** 拉取曲目播放信息(免登免费曲可用;付费/VIP 曲 ret=1001 或 isAuthorized=false) */
export const getTrackInfo = async(trackId: string, trackQualityLevel = 1): Promise<any> => {
const xmSign = await makeXmSign()
const { statusCode, body } = await httpFetch(
`${BASE_INFO_URL}${Date.now()}?device=win&trackId=${trackId}&trackQualityLevel=${trackQualityLevel}`,
{
headers: {
'User-Agent': XM_UA,
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
Referer: `https://www.ximalaya.com/sound/${trackId}`,
Origin: 'https://www.ximalaya.com',
Cookie: buildRequestCookie(),
'xm-sign': xmSign,
},
timeout: 20000,
},
).promise as any
if (statusCode != 200) throw new Error(`baseInfo HTTP ${statusCode}`)
const b = body ?? {}
if (b.ret != 0) {
// 1001:匿名访问付费内容与风控共用;未登录时优先提示绑定登录
const logged = Boolean((getCredentials('xm') as any)?.cookie)
if (b.ret == 1001 && !logged) throw new Error('该内容需要登录(POST /login/xm 绑定 cookie 后重试)')
throw new Error(b.msg || `baseInfo ret ${b.ret}`)
}
const trackInfo = b.trackInfo ?? b.data?.trackInfo
if (!trackInfo) throw new Error('baseInfo 未返回 trackInfo')
return trackInfo
}
export default { getTrackInfo, coverUrl }