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

+1 -1
View File
@@ -3,7 +3,7 @@
// 完整曲目是 FairPlay DRM 加密流(extendedAssetUrls.enhancedHls),需要 gamdl 等外部工具解密,
// 本 SDK 不提供。因此这里**任何请求档位都返回 type='preview'**,并在注释/响应里明确标注,不假装是无损完整曲。
// 歌曲的 previews[0].url 来自 catalog 歌曲详情(搜索结果缓存里的 previewUrl 也会直接复用,同上游 s.URL 逻辑)。
// 统一接口约定:apis('apple').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// 统一接口约定:apis('ap').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type:'preview', url, ext:'m4a', preview:true, songDuration, isrc }
// (songDuration 是歌曲完整时长,试听片段本身约 30-90 秒)
// type 请求参数不影响结果(只有 preview 一档);supportQualitys 声明为 ['128k'] 以对齐
+1 -1
View File
@@ -298,7 +298,7 @@ const MAX_PROBES = 10
export interface MiguUrlResult {
type: string
url: string
/** Z3D/I3D 加密流:URL 需解密后才能播放(本服务提供 /stream/migu 代理) */
/** Z3D/I3D 加密流:URL 需解密后才能播放(本服务提供 /stream/mg 代理) */
encrypted?: boolean
fileKey?: string
ext?: string
+2 -2
View File
@@ -35,7 +35,7 @@ export const getAlbumDetail = async(rawId: string, page = 1, limit = 50) => {
if (!summary.trackCount || songs.length > summary.trackCount) summary.trackCount = songs.length
return {
source: 'apple' as const,
source: 'ap' as const,
info: {
id: albumId,
name: summary.name,
@@ -64,7 +64,7 @@ export const getAlbumSongs = async(id: string): Promise<any[]> => {
export const parseAlbumLink = async(link: string) => {
const detail = await getAlbumDetail(link, 1, 100000)
return {
source: 'apple' as const,
source: 'ap' as const,
id: detail.info.id,
info: detail.info,
list: detail.list,
+4 -4
View File
@@ -1,8 +1,8 @@
// Apple Music 平台模块(上游 music-lib/apple/apple.go 的 Node/TS 移植,平台 id: apple)
// Apple Music 平台模块(上游 music-lib/apple/apple.go 的 Node/TS 移植,平台 id: ap)
// 能力:搜索 / 歌词(TTML,匿名 developer token 通常拿不到,见 lyric.ts)/ 封面 /
// 专辑(搜索+详情+链接解析)/ 歌单(搜索+详情+分类+分类歌单+链接解析)/
// 链接解析(song/album/playlist)/ 取流(**仅 preview 试听片段**,见 sdk/apis/apple.ts)。
// 凭据:上游 New(cookie) → getCredentials('apple'),cookie 形如
// 凭据:上游 New(cookie) → getCredentials('ap'),cookie 形如
// "media-user-token=xxx;storefront=cn;token=xxx",也支持 storefront/mediaUserToken/token 直填字段。
// 未实现(上游也没有):userPlaylist、榜单、热搜、歌手、评论、扫码登录。
import appleApi from '../apis/apple.ts'
@@ -72,7 +72,7 @@ const apple = {
playlist,
// 契约别名:Apple 的歌单分类即 genre curators,挂到 songList 键供 /song-list/apple 复用
// 契约别名:Apple 的歌单分类即 genre curators,挂到 songList 键供 /song-list/ap 复用
songList: {
sortList: [{ name: '热门', id: '1526756058' }],
getTags() {
@@ -96,7 +96,7 @@ const apple = {
/** 登录状态(media-user-token 存在性判断;无在线校验接口) */
export const status = () => {
const cred = getCredentials('apple') as any
const cred = getCredentials('ap') as any
const cfg = getAppleConfig()
return (cfg.mediaUserToken || cred?.cookie)
? { logged: true, updatedAt: cred?.updatedAt }
+3 -3
View File
@@ -39,7 +39,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => {
allPage: hasNext ? p + 1 : p,
total: (p - 1) * l + list.length,
limit: l,
source: 'apple' as const,
source: 'ap' as const,
}
}
@@ -53,7 +53,7 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 20) => {
total: (p - 1) * l + list.length,
page: p,
limit: l,
source: 'apple' as const,
source: 'ap' as const,
}
}
@@ -67,7 +67,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
total: (p - 1) * l + list.length,
page: p,
limit: l,
source: 'apple' as const,
source: 'ap' as const,
}
}
+5 -5
View File
@@ -53,8 +53,8 @@ export const appleCuratorCategories: Array<{ id: string, name: string }> = [
/** 上游 GetPlaylistCategories:静态返回 Apple Music genre curators */
export const getCategories = async() => ({
source: 'apple' as const,
list: appleCuratorCategories.map(item => ({ id: item.id, name: item.name, group: 'Apple Music 分类', source: 'apple' as const })),
source: 'ap' as const,
list: appleCuratorCategories.map(item => ({ id: item.id, name: item.name, group: 'Apple Music 分类', source: 'ap' as const })),
})
/** 上游 GetCategoryPlaylists:按 curator 分类取歌单(cn storefront,单请求上限 25,循环补齐) */
@@ -88,7 +88,7 @@ export const getCategoryPlaylists = async(categoryID: string, page = 1, limit =
}
return {
source: 'apple' as const,
source: 'ap' as const,
list: all.slice(0, l),
page: p,
limit: l,
@@ -118,7 +118,7 @@ export const getPlaylistDetail = async(rawId: string, page = 1, limit = 100) =>
if (!summary.trackCount || songs.length > summary.trackCount) summary.trackCount = songs.length
return {
source: 'apple' as const,
source: 'ap' as const,
info: {
id: playlistId,
name: summary.name,
@@ -145,7 +145,7 @@ export const getPlaylistSongs = async(id: string): Promise<any[]> => {
export const parsePlaylistLink = async(link: string) => {
const detail = await getPlaylistDetail(link, 1, 100000)
return {
source: 'apple' as const,
source: 'ap' as const,
id: detail.info.id,
info: detail.info,
list: detail.list,
+5 -5
View File
@@ -1,6 +1,6 @@
// Apple Music(music.apple.com / amp-api.music.apple.com)公共工具
// 上游 music-lib/apple/apple.go 的移植基础层:
// - New(cookie) → getCredentials('apple'):cookie 支持 "media-user-token=xxx;storefront=cn;token=xxx"
// - New(cookie) → getCredentials('ap'):cookie 支持 "media-user-token=xxx;storefront=cn;token=xxx"
// - fetchAppleToken():从 music.apple.com 首页 → index-legacy JS 抓 developer token(amp-api 的 Bearer)
// 当前(2026-10)Apple 下发的 JWT 以 eyJ0eXAi({"typ":...})开头而非上游正则写的 eyJh,
// 故这里泛化为任意 JWT(eyJ…)并优先选择 issuer=AMPWebPlay 的 token;带回退与失效重取见 getAppleToken/invalidateAppleToken。
@@ -42,7 +42,7 @@ export const parseCookieParts = (cookie: string): Record<string, string> => {
/** 读取 apple 平台运行时配置(对应上游 New(cookie)) */
export const getAppleConfig = (): AppleRuntimeConfig => {
const cred = (getCredentials('apple') as any) || {}
const cred = (getCredentials('ap') as any) || {}
const parts = parseCookieParts(String(cred.cookie ?? ''))
const storefront = String(cred.storefront || parts.storefront || DEFAULT_STOREFRONT).trim()
return {
@@ -297,7 +297,7 @@ export const appleSongFromCatalog = (res: any, defaultAlbumId = ''): any => {
lrc: null,
types: [],
_types: {},
source: 'apple' as const,
source: 'ap' as const,
link: attr.url || '', // music.apple.com 网页地址
previewUrl: previewURL, // 30-90 秒试听片段直链(Apple 不提供完整曲目明文流)
duration: durationSec,
@@ -316,7 +316,7 @@ export const appleAlbumToPlaylist = (res: any): any => {
creator: attr.artistName || '',
description: attr.editorialNotes?.short || '',
link: attr.url || '',
source: 'apple' as const,
source: 'ap' as const,
extra: {
release_date: attr.releaseDate || '',
record_label: attr.recordLabel || '',
@@ -340,7 +340,7 @@ export const applePlaylistToPlaylist = (res: any): any => {
creator: attr.curatorName || '',
description,
link: attr.url || '',
source: 'apple' as const,
source: 'ap' as const,
}
}
+4 -4
View File
@@ -53,13 +53,13 @@ const SUPPORTED_SOURCES: Array<{ id: string; name: string; module: PlatformModul
{ id: 'me', name: '猫耳FM', module: me, noAggregate: true },
{ id: 'qt', name: '蜻蜓FM', module: qt, noAggregate: true },
{ id: 'lr', name: '懒人听书', module: lr, noAggregate: true },
{ id: 'migu', name: '咪咕音乐', module: migu },
{ id: 'mg', name: '咪咕音乐', module: migu },
{ id: 'bd', name: '千千音乐', module: qian },
{ id: 'jx', name: 'JOOX', module: joox },
{ id: 'bl', name: '哔哩哔哩', module: bili },
{ id: '5s', name: '5sing', module: fivesing },
// Apple Music 仅提供 preview 试听片段(完整曲为 DRM),不参与跨平台精确匹配
{ id: 'apple', name: 'Apple Music', module: apple, noAggregate: true },
{ id: 'ap', name: 'Apple Music', module: apple, noAggregate: true },
]
const SOURCE_MODULES = Object.fromEntries(SUPPORTED_SOURCES.map((s) => [s.id, s.module]))
@@ -81,12 +81,12 @@ export const supportQuality: Record<string, string[]> = {
me: meQualitys,
qt: qtQualitys,
lr: lrQualitys,
migu: miguQualitys,
mg: miguQualitys,
bd: qianQualitys,
jx: jooxQualitys,
bl: biliQualitys,
'5s': fivesingQualitys,
apple: appleQualitys,
ap: appleQualitys,
}
const trimStr = (str: unknown): string => (typeof str === 'string' ? str.trim() : str as string)
+3 -3
View File
@@ -29,10 +29,10 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 10) => {
creator: String(item.singer ?? '').trim(),
description: firstNonEmpty(item.desc, item.publishDate),
link: miguAlbumLink(albumID),
source: 'migu' as const,
source: 'mg' as const,
}
}).filter(Boolean)
return { list, source: 'migu' as const, page, limit, total: list.length }
return { list, source: 'mg' as const, page, limit, total: list.length }
}
/** 专辑曲目(每页 50 全量翻页 + 去重;上游 fetchAlbumSongs) */
@@ -101,7 +101,7 @@ export const getAlbumDetail = async(id: string, page = 1, limit = 50) => {
const start = (page - 1) * limit
return {
source: 'migu' as const,
source: 'mg' as const,
info,
list: songs.slice(start, start + limit),
page,
+1 -1
View File
@@ -37,7 +37,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => {
allPage: Math.max(1, Math.ceil(total / limit)),
total,
limit,
source: 'migu' as const,
source: 'mg' as const,
}
}
+5 -5
View File
@@ -31,7 +31,7 @@ const toPlaylist = (item: MiguPlaylistItem, categoryId?: string) => {
playCount: parseInt(String(item.playNum ?? ''), 10) || 0,
creator: firstNonEmpty(item.userName, item.ownerName),
link: miguPlaylistLink(playlistID),
source: 'migu' as const,
source: 'mg' as const,
...(categoryId ? { categoryId } : {}),
}
}
@@ -41,7 +41,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
const body = await searchAll(keyword, 'songlist', page, limit)
const result: MiguPlaylistItem[] = body?.songListResultData?.result ?? []
const list = result.map(item => toPlaylist(item)).filter(Boolean)
return { list, source: 'migu' as const, page, limit, total: list.length }
return { list, source: 'mg' as const, page, limit, total: list.length }
}
/** 静态分类(上游 miguPlaylistCategoryKeywords) */
@@ -67,7 +67,7 @@ const CATEGORY_KEYWORDS = [
]
export const getCategories = () => ({
source: 'migu' as const,
source: 'mg' as const,
list: [
{ id: '', name: '全部', group: '全部' },
...CATEGORY_KEYWORDS.map(c => ({ id: c.name, name: c.name, group: c.group, hot: Boolean(c.hot) })),
@@ -77,7 +77,7 @@ export const getCategories = () => ({
export const getCategoryPlaylists = async(categoryId: string, page = 1, limit = 20) => {
const keyword = String(categoryId ?? '').trim() || '华语'
const { list } = await searchPlaylist(keyword, page, limit)
return { list: list.map(p => toPlaylist(p as any, keyword)).filter(Boolean), source: 'migu' as const, page, limit }
return { list: list.map(p => toPlaylist(p as any, keyword)).filter(Boolean), source: 'mg' as const, page, limit }
}
/** 歌单曲目(每页 50 全量翻页 + 去重;上游 GetPlaylistSongs) */
@@ -123,7 +123,7 @@ export const getPlaylistDetail = async(id: string, page = 1, limit = 50) => {
const { songs, total } = await fetchPlaylistSongs(playlistID)
const start = (page - 1) * limit
return {
source: 'migu' as const,
source: 'mg' as const,
info: { name: playlistID, img: songs[0]?.img ?? '', desc: '', author: '' },
list: songs.slice(start, start + limit),
page,
+3 -3
View File
@@ -38,7 +38,7 @@ const ENCRYPTED_APP_CODE = Buffer.from([
])
export const getMiguCookie = (): string => {
const cred = getCredentials('migu') as { cookie?: string } | null
const cred = getCredentials('mg') as { cookie?: string } | null
return String(cred?.cookie ?? '').trim()
}
@@ -269,7 +269,7 @@ export interface MiguSong {
payPlay?: boolean
/** 源特有元数据(取流/歌词需要):contentId/resourceType/formatType/copyrightId/songId/albumId */
extra: Record<string, string>
source: 'migu'
source: 'mg'
[key: string]: unknown
}
@@ -369,7 +369,7 @@ export const convertItemToSong = (item: MiguSongItem, allowPaid = true): MiguSon
payPlay: isVIP,
// 路由/取流需要 content_id|resource_type|format_type 三元组与版权号,放在 extra 里
extra: { ...extra, link: miguSongLink(linkID) },
source: 'migu',
source: 'mg',
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
/** 音乐平台标识 */
export type Source =
| 'kw' | 'kg' | 'tx' | 'wy' | 'qs'
| 'migu' | 'bd' | 'jx' | 'bl' | '5s' | 'apple'
| 'mg' | 'bd' | 'jx' | 'bl' | '5s' | 'ap'
| 'xm' | 'me' | 'qt' | 'lr'
/** 音质标识(wav 仅部分接口声明使用) */