补上最后两个多字母 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 零残留。
131 lines
4.7 KiB
TypeScript
131 lines
4.7 KiB
TypeScript
// 咪咕专辑 —— 搜索(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 }
|