feat: 平台注册与路由收尾——9 平台(含 xm/me/qt/lr 有声)、复合 id 约定、64k 档位、测试与文档
This commit is contained in:
1 parent
6fb866e343
commit
1fec828547
7 files changed
+307
-14
No files matched your search
+120
-1
@@ -5,6 +5,9 @@ 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 { status as xmStatus } from '../sdk/xm/index.ts'
|
||||
import { status as meStatus } from '../sdk/me/index.ts'
|
||||
import * as lrAuth from '../sdk/lr/auth.ts'
|
||||
import { clearCredentials, getCredentials, getTxAuth, setCredentials } from '../sdk/auth/store.ts'
|
||||
import { refreshAll } from './refresh.ts'
|
||||
|
||||
@@ -159,6 +162,59 @@ export const loginRoutes = async(fastify) => {
|
||||
}
|
||||
})
|
||||
|
||||
fastify.post('/login/kg/password', {
|
||||
schema: {
|
||||
summary: '酷狗账号密码登录',
|
||||
description: '触发 20028 风控时返回 needVerify 挑战(含 eventid、验证类型与 sid/edt 指纹):' +
|
||||
'滑块类需浏览器完成图形验证(返回的 url/txappid 供前端加载腾讯验证码)后调 /login/kg/verify,' +
|
||||
'再重试本接口;短信类(v_type=32)将收到的短信码先提交 /login/kg/verify,随后带 code 重试本接口完成登录。' +
|
||||
'风控较严,频繁失败建议改用扫码或短信登录。',
|
||||
tags: ['登录'],
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: { type: 'string' },
|
||||
password: { type: 'string' },
|
||||
code: { type: 'string', description: '短信验证码(20028 短信验证通过后重试登录时携带)' },
|
||||
},
|
||||
required: ['username', 'password'],
|
||||
},
|
||||
},
|
||||
}, async(request, reply) => {
|
||||
try {
|
||||
return await kgAuth.passwordLogin(request.body)
|
||||
} catch (err) {
|
||||
return fail(reply, err)
|
||||
}
|
||||
})
|
||||
|
||||
fastify.post('/login/kg/verify', {
|
||||
schema: {
|
||||
summary: '提交酷狗风控验证结果',
|
||||
description: '配合 /login/kg/password 的 needVerify 挑战使用:verifycode 为图形验证码凭证' +
|
||||
'(格式 KGCodeXX|{ticket,randstr,txappid})或短信验证码。sid/edt 省略时服务端自动生成行为指纹。' +
|
||||
'验证通过后按挑战类型重试登录(短信码带 code 重试 /login/kg/password)。',
|
||||
tags: ['登录'],
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventid: { type: 'string' },
|
||||
v_type: { type: 'integer', description: '验证类型(needVerify 返回的 v_type)' },
|
||||
verifycode: { type: 'string' },
|
||||
sid: { type: 'string' },
|
||||
edt: { type: 'string' },
|
||||
},
|
||||
required: ['eventid', 'v_type', 'verifycode'],
|
||||
},
|
||||
},
|
||||
}, async(request, reply) => {
|
||||
try {
|
||||
return await kgAuth.verifyUserInfo(request.body)
|
||||
} catch (err) {
|
||||
return fail(reply, err)
|
||||
}
|
||||
})
|
||||
|
||||
fastify.delete('/login/kg', {
|
||||
schema: { summary: '退出酷狗登录', tags: ['登录'] },
|
||||
}, async() => {
|
||||
@@ -266,6 +322,66 @@ export const loginRoutes = async(fastify) => {
|
||||
return { logged: false }
|
||||
})
|
||||
|
||||
// ---------- 喜马拉雅 ----------
|
||||
|
||||
fastify.post('/login/xm', {
|
||||
schema: {
|
||||
summary: '绑定喜马拉雅 cookie',
|
||||
description: 'cookie 从已登录的 www.ximalaya.com 浏览器请求头复制(须含 1&_token 字段),' +
|
||||
'用于解锁 xm 付费/已购内容取流。无法自动续期,失效后需重新复制绑定。',
|
||||
tags: ['登录'],
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: { cookie: { type: 'string' } },
|
||||
required: ['cookie'],
|
||||
},
|
||||
},
|
||||
}, async(request, reply) => {
|
||||
const cookie = String(request.body.cookie || '')
|
||||
if (!/1&_token=\S+/.test(cookie)) {
|
||||
return reply.code(422).send({ code: 422, msg: 'cookie 需包含 1&_token 字段' })
|
||||
}
|
||||
setCredentials('xm', { cookie })
|
||||
return { logged: true }
|
||||
})
|
||||
|
||||
fastify.delete('/login/xm', {
|
||||
schema: { summary: '退出喜马拉雅登录', tags: ['登录'] },
|
||||
}, async() => {
|
||||
clearCredentials('xm')
|
||||
return { logged: false }
|
||||
})
|
||||
|
||||
// ---------- 猫耳FM ----------
|
||||
|
||||
fastify.post('/login/me', {
|
||||
schema: {
|
||||
summary: '绑定猫耳FM token',
|
||||
description: 'cookie 从已登录的 m.missevan.com 浏览器请求头或猫耳 App 抓包复制(须含 token= 字段),' +
|
||||
'用于解锁 me 付费声音取流(App API 签名通道会自动携带)。无法自动续期,失效后需重新绑定。',
|
||||
tags: ['登录'],
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: { cookie: { type: 'string' } },
|
||||
required: ['cookie'],
|
||||
},
|
||||
},
|
||||
}, async(request, reply) => {
|
||||
const cookie = String(request.body.cookie || '')
|
||||
if (!/(?:^|;\s*)token=\S+/.test(cookie)) {
|
||||
return reply.code(422).send({ code: 422, msg: 'cookie 需包含 token 字段' })
|
||||
}
|
||||
setCredentials('me', { cookie })
|
||||
return { logged: true }
|
||||
})
|
||||
|
||||
fastify.delete('/login/me', {
|
||||
schema: { summary: '退出猫耳FM登录', tags: ['登录'] },
|
||||
}, async() => {
|
||||
clearCredentials('me')
|
||||
return { logged: false }
|
||||
})
|
||||
|
||||
// ---------- 通用 ----------
|
||||
|
||||
fastify.post('/login/refresh', {
|
||||
@@ -280,7 +396,7 @@ export const loginRoutes = async(fastify) => {
|
||||
fastify.get('/login/status', {
|
||||
schema: {
|
||||
summary: '查看登录状态',
|
||||
description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg/tx 为已存凭据(含 VIP 等级与最近更新时间)。',
|
||||
description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg/tx 为已存凭据(含 VIP 等级与最近更新时间),qs/xm/me/lr 为已存凭据。',
|
||||
tags: ['登录'],
|
||||
},
|
||||
}, async() => {
|
||||
@@ -293,6 +409,9 @@ export const loginRoutes = async(fastify) => {
|
||||
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(),
|
||||
xm: xmStatus(),
|
||||
me: meStatus(),
|
||||
lr: lrAuth.status(),
|
||||
}
|
||||
})
|
||||
}
|
||||
+5
-5
@@ -6,7 +6,7 @@
|
||||
import { notFound, upstream } from './http/api-error.ts'
|
||||
import { getMusicInfo } from '../sdk/kg/musicInfo.ts'
|
||||
|
||||
const QUALITIES = ['128k', '320k', 'flac', 'flac24bit']
|
||||
const QUALITIES = ['64k', '128k', '320k', 'flac', 'flac24bit']
|
||||
|
||||
/** 统一解包 SDK 返回值:{ promise, cancelHttp } 结构或裸 Promise 兼容 */
|
||||
const unwrap = (result: any): any => (
|
||||
@@ -65,7 +65,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
fastify.get('/search', {
|
||||
schema: {
|
||||
summary: '搜索歌曲',
|
||||
description: 'source 省略时聚合全部平台(排除 xm)搜索,失败的平台会被跳过。',
|
||||
description: 'source 省略时聚合全部平台(排除 xm/me/qt/lr 有声平台)搜索,失败的平台会被跳过。',
|
||||
tags: ['搜索'],
|
||||
querystring: qs({
|
||||
name: { type: 'string', description: '歌名', example: '稻香' },
|
||||
@@ -94,7 +94,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
|
||||
const lists = await Promise.all(
|
||||
musicSdk.sources
|
||||
.filter(s => musicSdk[s.id]?.musicSearch)
|
||||
.filter(s => musicSdk[s.id]?.musicSearch && !(s as any).noAggregate)
|
||||
.map(s => doSearch(s.id).catch(() => [])),
|
||||
)
|
||||
return { source: 'all', list: lists.flat(), page, limit }
|
||||
@@ -188,7 +188,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
fastify.get('/url/:source/:id', {
|
||||
schema: {
|
||||
summary: '获取播放地址',
|
||||
description: 'id 为平台歌曲 id:kw/tx/wy 用 songmid,kg 用 hash,qs 用 track id。' +
|
||||
description: 'id 为平台歌曲 id:kw/tx/wy 用 songmid,kg 用 hash,qs/xm/me 用声音 id,qt 为 <channelId>:<programId>(见 /album/qt),lr 为 <bookId>:<section>(见 /album/lr)。' +
|
||||
'取流按「外部音源脚本(SOURCE_ENABLED 启用时)→ SDK 官方直连 → 酷我兜底」顺序尝试,' +
|
||||
'酷我兜底按歌名/歌手/时长匹配同一首歌(匿名可用无损),kg 可由 hash 反查歌曲信息,' +
|
||||
'其它平台需要兜底时传 name/singer/interval。响应中的 via 字段标记实际命中通道(仅兜底时出现)。',
|
||||
@@ -408,7 +408,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
fastify.get('/album/:source/:id', {
|
||||
schema: {
|
||||
summary: '获取专辑详情与曲目列表',
|
||||
description: '支持 kg/kw/mg(见 /sources 的 album 能力位)。kw 单页固定 1000 条、limit 不生效。',
|
||||
description: '支持 kg/kw/tx/xm/me(见 /sources 的 album 能力位)。kw 单页固定 1000 条、xm/me 固定 30 条、limit 不生效。',
|
||||
tags: ['歌手专辑'],
|
||||
params: params({ source: pSource('album'), id: { type: 'string', description: '平台专辑 id(搜索结果中的 albumId)' } }),
|
||||
querystring: qs({ page: pPage, limit: pLimit }),
|
||||
|
||||
Reference in new issue
Block a user