Files
MusicServer/sdk/migu/album.ts
T
snowy-yang 1b972ec4dd 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 零残留。
2026-10-03 11:07:21 +08:00

131 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 咪咕专辑 —— 搜索(search_all.do album 开关)+ 曲目(queryAlbumSong,分页聚合去重)+ 详情(resourceinfo 2003)
// 上游对应:music-lib/migu/album.go、migu.go 的 fetchAlbumDetail / fetchAlbumSongs
import {
buildUrl, convertItemToSong, firstNonEmpty, miguGet, normalizeMiguImageURL,
pickMiguImage, type MiguImageItem, type MiguSong, type MiguSongItem, miguAlbumLink,
} from './util.ts'
import { searchAll } from './musicSearch.ts'
interface MiguAlbumSearchItem {
id?: string
resourceType?: string
name?: string
singer?: string
publishDate?: string
desc?: string
imgItems?: MiguImageItem[]
}
export const searchAlbum = async(keyword: string, page = 1, limit = 10) => {
const body = await searchAll(keyword, 'album', page, limit)
const result: MiguAlbumSearchItem[] = body?.albumResultData?.result ?? []
const list = result.map(item => {
const albumID = String(item.id ?? '').trim()
if (!albumID) return null
return {
id: albumID,
name: String(item.name ?? '').trim(),
cover: pickMiguImage(item.imgItems),
creator: String(item.singer ?? '').trim(),
description: firstNonEmpty(item.desc, item.publishDate),
link: miguAlbumLink(albumID),
source: 'mg' as const,
}
}).filter(Boolean)
return { list, source: 'mg' as const, page, limit, total: list.length }
}
/** 专辑曲目(每页 50 全量翻页 + 去重;上游 fetchAlbumSongs) */
export const fetchAlbumSongs = async(albumId: string): Promise<{ songs: MiguSong[], total: number }> => {
const albumID = String(albumId ?? '').trim()
if (!albumID) throw new Error('migu 专辑 id 为空')
const pageSize = 50
const seen = new Set<string>()
const songs: MiguSong[] = []
let totalCount = 0
for (let pageNo = 1; ; pageNo++) {
const body = await miguGet(buildUrl('https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/queryAlbumSong', {
albumId: albumID, pageNo, pageSize,
}))
if (body?.code && body.code !== '000000') throw new Error(`migu api error: ${body.info} (code ${body.code})`)
const songList: MiguSongItem[] = body?.data?.songList ?? []
if (!totalCount) totalCount = Number(body?.data?.totalCount ?? 0) || 0
if (!songList.length) break
const before = songs.length
for (const item of songList) {
const song = convertItemToSong(item, true)
if (!song) continue
const key = firstNonEmpty(song.extra.content_id, item.contentId, song.songmid, item.copyrightId)
|| `${song.name}|${song.singer}`
if (seen.has(key)) continue
seen.add(key)
songs.push(song)
}
if (songList.length < pageSize) break
if (totalCount > 0 && songs.length >= totalCount) break
if (songs.length === before) break
}
if (!songs.length) throw new Error('migu 专辑没有可播放歌曲')
return { songs, total: totalCount || songs.length }
}
/** 专辑详情(曲目 + resourceinfo 2003 的元数据),对齐既有 kg/kw 的 getAlbumDetail 返回形状 */
export const getAlbumDetail = async(id: string, page = 1, limit = 50) => {
const albumID = String(id ?? '').trim()
if (!albumID) throw new Error('migu 专辑 id 为空')
const { songs, total } = await fetchAlbumSongs(albumID)
let info: { name: string, img: string, desc: string, author: string } = { name: albumID, img: '', desc: '', author: '' }
try {
const body = await miguGet(buildUrl('https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do', {
needSimple: '00', resourceType: '2003', resourceId: albumID,
}))
if (body?.code && body.code !== '000000') throw new Error(body.info)
const res = body?.resource?.[0]
if (res) {
info = {
name: String(res.title ?? '').trim() || albumID,
img: pickMiguImage(res.imgItems),
desc: String(res.summary ?? '').trim(),
author: String(res.singer ?? '').trim(),
}
if (!info.img && songs[0]?.img) info.img = songs[0].img!
}
} catch {
if (songs[0]?.img) info.img = songs[0].img!
}
const start = (page - 1) * limit
return {
source: 'mg' as const,
info,
list: songs.slice(start, start + limit),
page,
limit,
total: total || songs.length,
}
}
export const getAlbumSongs = async(id: string) => (await fetchAlbumSongs(id)).songs
const ALBUM_PATTERNS = [
/music\.migu\.cn\/(?:v3|v5)\/music\/album\/(\d+)/,
/albumId=(\d+)/,
/resourceId=(\d+)/,
]
export const parseAlbumLink = async(link: string) => {
for (const pattern of ALBUM_PATTERNS) {
const m = String(link ?? '').match(pattern)
if (m) return getAlbumDetail(m[1]!)
}
throw new Error('无效的咪咕专辑链接')
}
export { normalizeMiguImageURL }
export default { searchAlbum, getAlbumDetail, getAlbumSongs, parseAlbumLink }