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:
snowy-yang committed 2026-10-01 22:43:49 +08:00
1 parent aa4f2eec67
commit 0f9cff2a08
26 files changed
+2172 -9

No files matched your search

+41
View File
@@ -4,6 +4,7 @@
import * as wyAuth from '../sdk/auth/wy.ts'
import * as kgAuth from '../sdk/auth/kg.ts'
import * as txAuth from '../sdk/auth/tx.ts'
import * as qsPassport from '../sdk/qs/passport.ts'
import { clearCredentials, getCredentials, getTxAuth, setCredentials } from '../sdk/auth/store.ts'
import { refreshAll } from './refresh.ts'
@@ -226,6 +227,45 @@ export const loginRoutes = async(fastify) => {
return { logged: false }
})
// ---------- 汽水音乐 ----------
fastify.post('/login/qs/qr', {
schema: {
summary: '获取汽水音乐扫码登录二维码',
description: '用「抖音 APP」扫码(汽水 App 扫可能不完成确认)后轮询 /login/qs/qr/check 完成登录。' +
'返回 qrimg(data URL)可直接展示,token 需原样回传。登录后 qs 取流走签名 track_v2,解锁完整曲目。',
tags: ['登录'],
},
}, async(_request, reply) => {
try {
return await qsPassport.createQr()
} catch (err) {
return fail(reply, err)
}
})
fastify.post('/login/qs/qr/check', {
schema: {
summary: '轮询汽水音乐扫码状态',
description: 'code:800 二维码过期 / 801 等待扫码 / 802 已扫码待确认 / 803 登录成功(自动保存凭据)。',
tags: ['登录'],
body: { type: 'object', properties: { token: { type: 'string' } }, required: ['token'] },
},
}, async(request, reply) => {
try {
return await qsPassport.checkQr(request.body.token)
} catch (err) {
return fail(reply, err)
}
})
fastify.delete('/login/qs', {
schema: { summary: '退出汽水音乐登录', tags: ['登录'] },
}, async() => {
clearCredentials('qs')
return { logged: false }
})
// ---------- 通用 ----------
fastify.post('/login/refresh', {
@@ -252,6 +292,7 @@ export const loginRoutes = async(fastify) => {
wy: wy ? { logged: true, ...wy } : { logged: false },
kg: kg ? { logged: true, ...kg, updatedAt: kgCred?.updatedAt } : { logged: false },
tx: tx ? { logged: true, uin: tx.uin, vipType: txCred?.vipType ?? null, updatedAt: txCred?.updatedAt } : { logged: false },
qs: qsPassport.status(),
}
})
}
+28 -1
View File
@@ -141,7 +141,11 @@ export const musicRoutes = async(fastify: any) => {
}
return result.then(res => {
if (!res?.url) throw upstream('获取播放地址失败')
return { type: quality, url: res.url }
// qs 解密流等代理地址返回相对路径,按请求的协议与 host 补全(兼容本机/局域网播放器)
const url = String(res.url).startsWith('/')
? `${request.protocol}://${request.headers.host}${res.url}`
: res.url
return { type: quality, url }
}).catch(err => {
throw err.status ? err : upstream(err.message)
})
@@ -161,6 +165,29 @@ export const musicRoutes = async(fastify: any) => {
},
}, urlHandler)
// qs 解密流代理:登录态取流返回 /stream/qs/:id 相对地址,本端点完成"签名 track_v2 →
// 密音频下载 → spade key + AES-CTR 解密 → 回明文流"。raw 豁免 { code, data } 信封包装
fastify.get('/stream/qs/:id', {
config: { raw: true },
schema: {
summary: '汽水音乐解密流代理',
description: '登录态取流地址(/url/qs 返回的相对地址)。服务端完成 track_v2 签名请求与 cenc-aes-ctr 本地解密,返回明文音频。',
tags: ['歌曲资源'],
params: params({ id: pId }),
},
}, async(request, reply) => {
const { id } = request.params
try {
const entry = await fastify.musicSdk.qs.getStreamBuffer(id)
reply.type(entry.contentType || 'audio/mp4')
reply.header('Accept-Ranges', 'none')
reply.header('Content-Length', entry.buffer.length)
return reply.send(entry.buffer)
} catch (err: any) {
throw upstream(err.message)
}
})
const lyricHandler = async(request) => {
const { source, id } = request.params
const { name, interval } = request.query
+2 -2
View File
@@ -41,7 +41,7 @@ const PORT = process.env.PORT || 16688
const DOCS_PREFIX = '/docs'
const API_TITLE = 'MusicServer 聚合音乐 API'
const API_DESCRIPTION = '多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单。' +
'支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)平台。'
'支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)/ qs(汽水音乐)平台。'
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8'))
@@ -154,7 +154,7 @@ fastify.register(swagger, {
{ name: '榜单', description: '排行榜' },
{ name: '歌单', description: '歌单与热搜' },
{ name: '歌手专辑', description: '歌手信息、歌曲与专辑详情' },
{ name: '登录', description: '账号登录(wy 扫码/手机号、kg token)与状态' },
{ name: '登录', description: '账号登录(wy 扫码/手机号、kg 扫码/token、tx 扫码/cookie、qs 抖音扫码)与状态' },
{ name: '系统', description: '健康检查、平台信息、文档' },
],
},