refactor(qs)!: 手机端链路彻底清除——PC 签名成为唯一供给
安卓自签 crypto/ 整目录(11 文件)与 device.ts 设备注册删除,audiodecrypt 上移 qs/ 根,getDeviceIds 并入 passport(_qs_passport_device 持久化); musicSearch 去指纹搜索(需 PC 签名,匿名搜索下线);mssdk 签名供给改三级: QS_SIGN_SERVER 显式 → 本机 16689 /healthz 自动探测(qs-signer 的 mssdk-sign) → data/qs-sign.json 固定值兜底。
This commit is contained in:
1 parent
cec63f864e
commit
6b05fde64b
19 files changed
+111
-1271
No files matched your search
+7
-73
@@ -1,8 +1,9 @@
|
||||
// 汽水音乐 URL 获取(直连)
|
||||
// 两条取流通道:
|
||||
// 匿名(未登录): seo_track → url_player_info(VOD GetPlayInfo 免登录)→ 未加密试听直链(30s 132k)
|
||||
// 登录态: 设备注册 + X-Argus/X-Ladon 自签 → /luna/pc/track_v2 → 加密音频(cenc-aes-ctr)
|
||||
// → spade key 解密 → /stream/qs/:id 代理端点回明文流(相对地址,路由层负责补全 origin)
|
||||
// 登录态: 官方 PC 签名 X-Helios/X-Medusa(见 sdk/qs/mssdk.ts)→ /luna/pc/track_v2
|
||||
// → 加密音频(cenc-aes-ctr)→ spade key 解密 → /stream/qs/:id 代理端点回明文流
|
||||
// (相对地址,路由层负责补全 origin)
|
||||
// 实测(2026-10-01): track_v2 匿名=加密试听;完整曲目需登录 sessionid(汽水音乐 APP 扫码 /login/qs/qr)。
|
||||
// 登录请求失败自动降级匿名试听。
|
||||
// 统一接口约定:apis('qs').getMusicUrl(songInfo, type) → { promise, cancelHttp }
|
||||
@@ -11,11 +12,9 @@
|
||||
import { httpFetch } from '../common/request.ts'
|
||||
import { createHash, randomBytes } from 'node:crypto'
|
||||
import musicInfo from '../qs/musicInfo.ts'
|
||||
import { getDevice } from '../qs/device.ts'
|
||||
import { getCookie, getSessionId } from '../qs/passport.ts'
|
||||
import { mssdkAvailable, signQishuiRequest, pcCommonParams, PC_UA } from '../qs/mssdk.ts'
|
||||
import { signRequest } from '../qs/crypto/sign.ts'
|
||||
import { decryptAudioBuffer, resolveAudioKey } from '../qs/crypto/audiodecrypt.ts'
|
||||
import { decryptAudioBuffer, resolveAudioKey } from '../qs/audiodecrypt.ts'
|
||||
import type { HttpResponse } from '../types.ts'
|
||||
|
||||
// 官方直连接口支持的音质:授权曲匿名只有 30 秒试听 128k(登录态取码率最高条目,324k/hi_res 视权益);
|
||||
@@ -76,43 +75,7 @@ const anonymousPreviewUrl = async (trackId: string): Promise<string> => {
|
||||
return url
|
||||
}
|
||||
|
||||
// 登录态通道:注册设备 + argus/ladon 自签 → track_v2 → 解密最高码率音频
|
||||
const buildSignedUrl = (device: Awaited<ReturnType<typeof getDevice>>): string => {
|
||||
const params = new URLSearchParams({
|
||||
device_platform: 'android',
|
||||
os: 'android',
|
||||
ssmix: 'a',
|
||||
cdid: device.cdid,
|
||||
channel: device.channel,
|
||||
aid: '386088',
|
||||
app_name: 'luna',
|
||||
version_code: '100198030',
|
||||
version_name: '19.8.0',
|
||||
manifest_version_code: '100198030',
|
||||
update_version_code: '100198030',
|
||||
resolution: '1080*1920',
|
||||
dpi: '480',
|
||||
device_type: device.device_type,
|
||||
device_brand: device.device_brand,
|
||||
language: 'zh',
|
||||
os_api: '35',
|
||||
os_version: device.os_version,
|
||||
ac: 'wifi',
|
||||
device_model: device.device_model,
|
||||
device_id: device.device_id,
|
||||
iid: device.install_id,
|
||||
tz_name: 'Asia/Shanghai',
|
||||
tz_offset: '28800',
|
||||
sim_region: 'cn',
|
||||
package: 'com.luna.music',
|
||||
luna_apk_type: 'normal_apk',
|
||||
output_device_type: 'Phone',
|
||||
openudid: device.openudid,
|
||||
_rticket: String(Date.now()),
|
||||
})
|
||||
return `${TRACK_V2_URL}?${params.toString()}`
|
||||
}
|
||||
|
||||
// 登录态通道:官方 PC 客户端签名 X-Helios/X-Medusa(签名服务器或固定值,见 sdk/qs/mssdk.ts)
|
||||
const pickBestEntry = (playInfoList: any) => {
|
||||
const entries = (playInfoList || []).filter(item => item?.MainPlayUrl)
|
||||
if (!entries.length) return null
|
||||
@@ -138,28 +101,9 @@ const fetchTrackPlayer = async (url: string, headers: Record<string, string>, re
|
||||
const TRACK_V2_BODY = (trackId: string) =>
|
||||
JSON.stringify({ track_id: trackId, media_type: 'track', queue_type: 'search_one_track', scene_name: 'search' })
|
||||
|
||||
// 通道 A(默认):安卓 App 参数 + X-Argus/X-Ladon 自签
|
||||
const fetchTrackPlayerByApp = async (trackId: string) => {
|
||||
const device = await getDevice()
|
||||
const sessionId = getSessionId()
|
||||
const url = buildSignedUrl(device)
|
||||
const requestBody = TRACK_V2_BODY(trackId)
|
||||
const headers = await signRequest(url, requestBody)
|
||||
return fetchTrackPlayer(url, {
|
||||
...headers,
|
||||
'User-Agent': 'Luna/19.8.0 (com.luna.music/100198030);Android 15;ABR-AL80',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-Luna-Background-Type': 'foreground',
|
||||
'X-Luna-Is-Background-Req': '0',
|
||||
'X-Luna-Is-Local-User': '1',
|
||||
...(sessionId ? { Cookie: getCookie() || `sessionid=${sessionId}` } : {}),
|
||||
}, requestBody)
|
||||
}
|
||||
|
||||
// 通道 B(降级):官方 PC 客户端签名 X-Helios/X-Medusa(签名服务器或固定值,见 sdk/qs/mssdk.ts)
|
||||
const fetchTrackPlayerByPc = async (trackId: string) => {
|
||||
const sessionId = getSessionId()
|
||||
if (!mssdkAvailable()) throw new Error('PC 签名不可用')
|
||||
if (!mssdkAvailable()) throw new Error('PC 签名不可用(16689 未探测到签名服务,可配置 QS_SIGN_SERVER 或 data/qs-sign.json,见 qs-signer 仓库)')
|
||||
const url = `${TRACK_V2_URL}?${new URLSearchParams(pcCommonParams()).toString()}`
|
||||
const requestBody = TRACK_V2_BODY(trackId)
|
||||
const headers: Record<string, string> = {
|
||||
@@ -177,18 +121,8 @@ const fetchTrackPlayerByPc = async (trackId: string) => {
|
||||
return fetchTrackPlayer(url, { ...headers, ...signed }, requestBody)
|
||||
}
|
||||
|
||||
const resolveTrackPlayer = async (trackId: string) => {
|
||||
try {
|
||||
return await fetchTrackPlayerByApp(trackId)
|
||||
} catch (err: any) {
|
||||
if (!mssdkAvailable()) throw err
|
||||
console.log(`[qs/track_v2] 安卓自签通道失败(${err?.message ?? err}),降级官方 PC 签名`)
|
||||
return fetchTrackPlayerByPc(trackId)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveSignedStream = async (trackId: string): Promise<StreamEntry> => {
|
||||
const trackPlayer = await resolveTrackPlayer(trackId)
|
||||
const trackPlayer = await fetchTrackPlayerByPc(trackId)
|
||||
|
||||
const playInfoRes = await httpFetch(trackPlayer.url_player_info, { method: 'get', timeout: 15000 }).promise
|
||||
const entry = pickBestEntry((playInfoRes.body as any)?.Result?.Data?.PlayInfoList)
|
||||
|
||||
Reference in new issue
Block a user