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:
1 parent
aa4f2eec67
commit
0f9cff2a08
26 files changed
+2172
-9
No files matched your search
@@ -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
@@ -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
@@ -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: '健康检查、平台信息、文档' },
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user