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
-1
@@ -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
@@ -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
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 仅部分接口声明使用) */
|
||||
|
||||
Reference in new issue
Block a user