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:
snowy-yang committed 2026-10-03 11:07:21 +08:00
1 parent 3381e9b906
commit 1b972ec4dd
20 files changed
+106 -106

No files matched your search

+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'))