Files
MusicServer/sdk/xm/musicInfo.ts
T

65 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 喜马拉雅曲目信息(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 }