refactor(api)!: 平台 id 收尾统一——migu→mg、apple→ap
补上最后两个多字母 id,至此 15 个平台对外 id 全部为两字母: kw kg tx wy qs xm me qt lr mg bd jx bl 5s ap **破坏性变更**:id 出现在所有 URL、/sources 返回以及 data/auth.json 的 凭据键里,咪咕与 Apple Music 已存的登录态会失效,需重新登录。 - sdk/index.ts:id 与 supportQuality 映射键(展示名 `咪咕音乐` / `Apple Music` 保持不动) - sdk/types.ts:Source 联合类型 - src/routes.ts:/stream 的 enum ['qs','migu'] → ['qs','mg'],以及 /url/mg、/stream/mg 等路径与 schema 描述 - src/http/migu-stream.ts、src/routes-login.ts、src/server.ts - test/smoke-test.ts、README.md、docs/API.md 同步 有意未改:域名与 API 路径(music.migu.cn、music.apple.com、 amp-api.music.apple.com、/v1/catalog/cn/apple-curators/…)、 标识符(registerMiguStream、appleAmpGet、APPLE_AMP_API_URL 等)、 sdk/migu 与 sdk/apple 的目录与文件名、错误文案里的平台名。 验证:typecheck ✓、pnpm test 112/112 ✓;另起临时实例实证 /sources (15/15 平台 id 与展示名全对)与 /login/status,旧 id 零残留。
This commit is contained in:
1 parent
3381e9b906
commit
1b972ec4dd
20 files changed
+106
-106
No files matched your search
@@ -1,5 +1,5 @@
|
||||
// 咪咕加密流票据 —— /url/migu 命中 Z3D/I3D 加密候选时登记,/stream/migu/:id 凭票解密
|
||||
// (统一入口为 /stream/:source/:id,source=migu 时 id 即此处返回的票据)。
|
||||
// 咪咕加密流票据 —— /url/mg 命中 Z3D/I3D 加密候选时登记,/stream/mg/:id 凭票解密
|
||||
// (统一入口为 /stream/:source/:id,source=mg 时 id 即此处返回的票据)。
|
||||
// 不把上游 URL 塞进播放地址(Fastify path 参数限长,且 URL 含时效签名与中文路径),
|
||||
// 票据短时有效(10 分钟,够播放器起播),进程内存储、不落盘。
|
||||
import { randomUUID } from 'node:crypto'
|
||||
@@ -29,7 +29,7 @@ const prune = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 登记一条加密流,返回可直接放入 /stream/migu/:id 的短票据 id */
|
||||
/** 登记一条加密流,返回可直接放入 /stream/mg/:id 的短票据 id */
|
||||
export const registerMiguStream = (url: string, fileKey: string): string => {
|
||||
prune()
|
||||
const id = randomUUID().replace(/-/g, '')
|
||||
|
||||
+2
-2
@@ -477,7 +477,7 @@ export const loginRoutes = async(fastify) => {
|
||||
schema: {
|
||||
summary: '查看登录状态',
|
||||
description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg/tx/bl 为已存凭据(含 VIP 等级与最近更新时间),' +
|
||||
'qs/xm/me/lr/bd/jx/apple/5s 为已存凭据。',
|
||||
'qs/xm/me/lr/bd/jx/ap/5s 为已存凭据。',
|
||||
tags: ['登录'],
|
||||
},
|
||||
}, async() => {
|
||||
@@ -496,7 +496,7 @@ export const loginRoutes = async(fastify) => {
|
||||
bl: biliAuth.status(),
|
||||
bd: qianStatus(),
|
||||
jx: jooxStatus(),
|
||||
apple: appleStatus(),
|
||||
ap: appleStatus(),
|
||||
'5s': fivesingStatus(),
|
||||
}
|
||||
})
|
||||
|
||||
+16
-16
@@ -178,10 +178,10 @@ export const musicRoutes = async(fastify: any) => {
|
||||
// 取流链:外部音源脚本(SOURCE_ENABLED 启用时)→ SDK 官方直连 → 酷我兜底(跨平台匹配)
|
||||
const finish = (res: any, via: string) => {
|
||||
if (!res?.url) throw new Error('获取播放地址失败')
|
||||
// 加密流(migu 的 Z3D/I3D):客户端无法自行解密,改指向服务端解密代理 /stream/migu/<票据>
|
||||
// 加密流(migu 的 Z3D/I3D):客户端无法自行解密,改指向服务端解密代理 /stream/mg/<票据>
|
||||
let rawUrl = String(res.url)
|
||||
if (res.encrypted && res.fileKey) {
|
||||
rawUrl = `/stream/migu/${registerMiguStream(rawUrl, String(res.fileKey))}`
|
||||
rawUrl = `/stream/mg/${registerMiguStream(rawUrl, String(res.fileKey))}`
|
||||
}
|
||||
// qs 解密流等代理地址返回相对路径,按请求的协议与 host 补全(兼容本机/局域网播放器);
|
||||
// 启用 AUTH_KEY 时附带短时签名(播放器拉流无法携带自定义头,签名不暴露秘钥本身)
|
||||
@@ -260,23 +260,23 @@ export const musicRoutes = async(fastify: any) => {
|
||||
},
|
||||
}, urlHandler)
|
||||
|
||||
// 加密流解密代理(qs 汽水解密流 / migu 咪咕 Z3D 加密流):统一 /stream/:source/:id,用 path 段 source 区分平台。
|
||||
// 加密流解密代理(qs 汽水解密流 / mg 咪咕 Z3D 加密流):统一 /stream/:source/:id,用 path 段 source 区分平台。
|
||||
// - qs:/url/qs 登录态取流返回相对地址 /stream/qs/<trackId>,本端点完成"签名 track_v2 →
|
||||
// 密音频下载 → spade key + AES-CTR 解密 → 回明文流"
|
||||
// - migu:/url/migu 命中 Z3D/I3D 加密候选时登记票据并返回 /stream/migu/<票据>,本端点凭票下载密流逐字节解密
|
||||
// - mg:/url/mg 命中 Z3D/I3D 加密候选时登记票据并返回 /stream/mg/<票据>,本端点凭票下载密流逐字节解密
|
||||
// 两者均由 /url 产出相对地址,路由层负责补全 origin;raw 豁免 { code, data } 信封包装
|
||||
fastify.get('/stream/:source/:id', {
|
||||
config: { raw: true },
|
||||
schema: {
|
||||
summary: '加密流解密代理(qs / migu)',
|
||||
description: '取流响应中返回的相对地址(/url/qs 的流地址、/url/migu 命中加密流时的地址),' +
|
||||
summary: '加密流解密代理(qs / mg)',
|
||||
description: '取流响应中返回的相对地址(/url/qs 的流地址、/url/mg 命中加密流时的地址),' +
|
||||
'服务端下载上游密流并在本地解密后返回明文音频;不接受客户端自行构造。' +
|
||||
'source=qs 时 id 为汽水 track id(track_v2 签名 + cenc-aes-ctr 解密);' +
|
||||
'source=migu 时 id 为取流响应 url 中的票据段(票据 10 分钟内有效,过期请重新 /url/migu 取流)。',
|
||||
'source=mg 时 id 为取流响应 url 中的票据段(票据 10 分钟内有效,过期请重新 /url/mg 取流)。',
|
||||
tags: ['歌曲资源'],
|
||||
params: params({
|
||||
source: { type: 'string', enum: ['qs', 'migu'], description: '平台 id:qs=汽水音乐解密流 / migu=咪咕加密流', example: 'qs' },
|
||||
id: { type: 'string', description: 'qs: 汽水 track id;migu: 取流响应 url 中的票据段(无需自行构造)', example: '7123456789' },
|
||||
source: { type: 'string', enum: ['qs', 'mg'], description: '平台 id:qs=汽水音乐解密流 / mg=咪咕加密流', example: 'qs' },
|
||||
id: { type: 'string', description: 'qs: 汽水 track id;mg: 取流响应 url 中的票据段(无需自行构造)', example: '7123456789' },
|
||||
}),
|
||||
},
|
||||
}, async(request, reply) => {
|
||||
@@ -297,7 +297,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
|
||||
// 咪咕:票据 → 逐块解密流
|
||||
const ticket = takeMiguStream(String(id))
|
||||
if (!ticket) throw notFound('解密票据不存在或已过期(请重新调用 /url/migu 取流)')
|
||||
if (!ticket) throw notFound('解密票据不存在或已过期(请重新调用 /url/mg 取流)')
|
||||
// 流式下载 + 逐块解密:咪咕 Z3D 密钥流按 32 字节循环,块内保持全局偏移即可,
|
||||
// 避免把上百 MB 的 wav/flac 整段读进内存(httpFetch 有 64MB 缓冲上限)
|
||||
const { miguZ3DStreamKey, isMiguAudioMagic } = await import('../sdk/migu/util.ts')
|
||||
@@ -513,7 +513,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
|
||||
const albumSchema = (withPage: boolean) => ({
|
||||
summary: '获取专辑详情与曲目列表',
|
||||
description: '支持 kg/kw/tx/wy/xm/me/qt/lr/migu/bd/jx/apple(见 /sources 的 album 能力位)。' +
|
||||
description: '支持 kg/kw/tx/wy/xm/me/qt/lr/mg/bd/jx/ap(见 /sources 的 album 能力位)。' +
|
||||
'kw 单页固定 1000 条、xm/me 固定 30 条、limit 不生效。',
|
||||
tags: ['歌手专辑'],
|
||||
params: params({
|
||||
@@ -538,7 +538,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
fastify.get('/album/:source/search/:keyword', {
|
||||
schema: {
|
||||
summary: '搜索专辑',
|
||||
description: '支持 kg/kw/tx/wy/migu/bd/jx/apple(见 /sources 的 albumSearch 能力位)。',
|
||||
description: '支持 kg/kw/tx/wy/mg/bd/jx/ap(见 /sources 的 albumSearch 能力位)。',
|
||||
tags: ['歌手专辑'],
|
||||
params: params({ source: pSource('album.searchAlbum'), keyword: { type: 'string', description: '专辑名关键词', example: '魔杰座' } }),
|
||||
querystring: qs({ page: pPage, limit: pLimit }),
|
||||
@@ -563,7 +563,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
fastify.get('/playlist/:source/search/:keyword', {
|
||||
schema: {
|
||||
summary: '搜索歌单',
|
||||
description: '支持 kg/kw/tx/wy/migu/bd/jx/bl/5s/apple(见 /sources 的 playlistSearch 能力位)。' +
|
||||
description: '支持 kg/kw/tx/wy/mg/bd/jx/bl/5s/ap(见 /sources 的 playlistSearch 能力位)。' +
|
||||
'部分平台该能力受限(如 bd 歌单搜索接口已下线、bl 需登录),失败时返回 502。',
|
||||
tags: ['歌单'],
|
||||
params: params({ source: pSource('playlist.searchPlaylist'), keyword: { type: 'string', example: '华语' } }),
|
||||
@@ -586,7 +586,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
fastify.get('/playlist/:source/categories', {
|
||||
schema: {
|
||||
summary: '获取歌单分类',
|
||||
description: '支持 kw/kg/tx/wy/migu/bd/jx/bl/5s/apple(见 /sources 的 playlistCategories 能力位)。',
|
||||
description: '支持 kw/kg/tx/wy/mg/bd/jx/bl/5s/ap(见 /sources 的 playlistCategories 能力位)。',
|
||||
tags: ['歌单'],
|
||||
params: params({ source: pSource('playlist.getCategories') }),
|
||||
response: responses('PlaylistCategoriesResult'),
|
||||
@@ -631,7 +631,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
const playlistSchema = (withPage: boolean) => ({
|
||||
summary: '获取歌单详情与曲目列表',
|
||||
description: 'id 为歌单 id 或链接(部分平台支持链接/裸 id,如 bl 的 season:<id>:<mid>:<bvid>、' +
|
||||
'jx/bd/migu 的网页链接)。列表按 page/limit 切片(聚合接口自身分页的平台由平台决定)。',
|
||||
'jx/bd/mg 的网页链接)。列表按 page/limit 切片(聚合接口自身分页的平台由平台决定)。',
|
||||
tags: ['歌单'],
|
||||
params: params({
|
||||
source: pSource('playlist'),
|
||||
@@ -701,7 +701,7 @@ export const musicRoutes = async(fastify: any) => {
|
||||
schema: {
|
||||
summary: `解析${kind === 'song' ? '歌曲' : kind === 'album' ? '专辑' : '歌单'}链接`,
|
||||
description: '把平台链接(或裸 id)解析成结构化歌曲/曲目数据。支持平台见 /sources 的 parse 能力位;' +
|
||||
'5s 歌曲/歌单、apple/bl/jx/bd/migu 三类、kw/kg/tx/wy 部分支持。',
|
||||
'5s 歌曲/歌单、apple/bl/jx/bd/mg 三类、kw/kg/tx/wy 部分支持。',
|
||||
tags: ['链接解析'],
|
||||
params: params({ source: pSource('parse.parseSong') }),
|
||||
body: parseBodySchema,
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@
|
||||
// GET /playlist/:source/categories[/:categoryId] 歌单分类
|
||||
// POST /parse/{song|album|playlist}/:source 链接解析
|
||||
// GET /user-playlist/:source 个人歌单
|
||||
// GET /stream/:source/:id 解密流代理(source: qs 汽水 / migu 咪咕)
|
||||
// GET /stream/:source/:id 解密流代理(source: qs 汽水 / mg 咪咕)
|
||||
// GET /sources /health GET /docs /redoc /openapi.json
|
||||
//
|
||||
// 状态码语义:200 成功;422 参数校验失败;404 路由或平台不存在;405 方法不允许;502 上游平台失败;500 内部错误
|
||||
@@ -48,8 +48,8 @@ const PORT = process.env.PORT || 16688
|
||||
const DOCS_PREFIX = '/docs'
|
||||
const API_TITLE = 'MusicServer 聚合音乐 API'
|
||||
const API_DESCRIPTION = '多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单、专辑、歌手、链接解析。' +
|
||||
'支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)/ qs(汽水音乐)/ migu(咪咕)/ bd(千千)/ ' +
|
||||
'jx / bl(哔哩哔哩)/ 5s / apple(Apple Music,仅试听)与有声平台 xm(喜马拉雅)/ me(猫耳FM)/ qt(蜻蜓FM)/ lr(懒人听书)。'
|
||||
'支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)/ qs(汽水音乐)/ mg(咪咕)/ bd(千千)/ ' +
|
||||
'jx / bl(哔哩哔哩)/ 5s / ap(Apple Music,仅试听)与有声平台 xm(喜马拉雅)/ me(猫耳FM)/ qt(蜻蜓FM)/ lr(懒人听书)。'
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8'))
|
||||
|
||||
|
||||
Reference in new issue
Block a user