Files
MusicServer/sdk/apple/playlist.ts
T
snowy-yang e1357dd412 refactor(api)!: 平台 id 回退——joox/5sing/bili/apple 恢复全名,目录对齐 bd/mg
- 撤销 3381e9b/1b972ec 中 jx/5s/bl/ap 四个短码,恢复 joox/5sing/bili/apple;
  qian→bd、migu→mg 保留
- 目录与文件对齐保留的新 id:sdk/qian/→sdk/bd/、sdk/migu/→sdk/mg/、
  sdk/apis/qian.ts→bd.ts、migu.ts→mg.ts、src/http/migu-stream.ts→mg-stream.ts
  (规则改为「目录=对外 id」,聚合入口的模块变量同步更名;music.migu.cn 等域名、
  上游 music-lib 对照注释、miguGet 等功能标识符不动)
- 对外 id 全表:kw kg tx wy qs xm me qt lr mg bd joox 5sing bili apple

**破坏性变更**:joox/5sing/bili/apple 四平台的 URL、/sources 返回与
data/auth.json 凭据键恢复全名,已存短码键的登录态需重新登录。

验证:typecheck ✓、pnpm test 112/112 ✓
2026-10-03 21:04:55 +08:00

166 lines
5.9 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.
// Apple Music 歌单(上游 apple.go 的 SearchPlaylist / ParsePlaylist / GetPlaylistSongs /
// GetPlaylistCategories / GetCategoryPlaylists 与 applePlaylistToPlaylist 映射)
// 详情:amp-api /v1/catalog/<sf>/playlists/<id>?limit[tracks]=300&extend=extendedAssetUrls
// 曲目超过一页时沿 relationships.tracks.next 全量拉取(上游 fetchAllTrackResources,带 20 页上限防死循环)。
// 分类:上游使用写死的 Apple Music genre curator 列表(appleCuratorCategories),
// GetCategoryPlaylists 固定 storefront=cn + l=zh-Hans-CN(curator id 取自 cn 区)。
// 链接:extractAppleID(link, 'playlist'),支持 pl.* 裸 id 与 /us/playlist/<slug>/pl.* 链接。
import {
appleAmpGet,
applePlaylistToPlaylist,
appleSongFromCatalog,
extractAppleID,
fetchAllTrackResources,
getAppleConfig,
} from './util.ts'
export { searchPlaylist } from './musicSearch.ts'
import { searchPlaylist } from './musicSearch.ts'
/** 上游 appleCuratorCategories(从公开搜索落地页提取的 Apple Music genre curators) */
export const appleCuratorCategories: Array<{ id: string, name: string }> = [
{ id: '1526756058', name: '热门' },
{ id: '1479949880', name: 'C-Pop' },
{ id: '1019400042', name: '国语流行' },
{ id: '1019398918', name: '粤语流行' },
{ id: '1019399540', name: '国际流行' },
{ id: '1019399551', name: 'K-Pop' },
{ id: '1019399547', name: 'J-Pop' },
{ id: '989061415', name: '嘻哈/说唱' },
{ id: '1019400044', name: 'R&B' },
{ id: '1019400046', name: '摇滚' },
{ id: '1019397973', name: '另类音乐' },
{ id: '976439535', name: '舞曲' },
{ id: '976439536', name: '电子' },
{ id: '976439541', name: '独立音乐' },
{ id: '1019399549', name: '爵士乐' },
{ id: '1019398924', name: '古典音乐' },
{ id: '976439528', name: '蓝调' },
{ id: '976439534', name: '乡村音乐' },
{ id: '1531542847', name: '拉丁音乐' },
{ id: '988656348', name: '非洲音乐' },
{ id: '976439543', name: '金属乐' },
{ id: '976439550', name: '朋克乐' },
{ id: '1019400049', name: '不插电' },
{ id: '1231181168', name: '影视原声' },
{ id: '1441811365', name: 'DJ 混音精选' },
{ id: '1532467784', name: '瞩目之星' },
{ id: '1554938339', name: '年代之声' },
{ id: '1564180390', name: '空间音频' },
{ id: '989010186', name: '亲子' },
]
/** 上游 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 })),
})
/** 上游 GetCategoryPlaylists:按 curator 分类取歌单(cn storefront,单请求上限 25,循环补齐) */
export const getCategoryPlaylists = async(categoryID: string, page = 1, limit = 20) => {
const categoryId = String(categoryID ?? '').trim()
if (!categoryId) throw new Error('apple 分类 id 不能为空')
const p = Math.max(1, Number(page) || 1)
const l = Math.min(Math.max(1, Number(limit) || 20), 300)
const apiMax = 25
let offset = (p - 1) * l
const all: any[] = []
while (all.length < l) {
const batchSize = Math.min(apiMax, l - all.length)
let body: any
try {
body = await appleAmpGet(`/v1/catalog/cn/apple-curators/${categoryId}/playlists`, {
limit: batchSize,
offset,
l: 'zh-Hans-CN',
})
} catch (err) {
if (all.length > 0) break // 上游:已有数据时返回已取部分
throw err
}
const data = body?.data ?? []
all.push(...data.map((item: any) => applePlaylistToPlaylist(item)))
// 没有更多数据或没有下一页
if (data.length < batchSize || !body?.next) break
offset += data.length
}
return {
source: 'apple' as const,
list: all.slice(0, l),
page: p,
limit: l,
total: all.length,
}
}
/** 歌单详情(全量曲目后按 page/limit 切片) */
export const getPlaylistDetail = async(rawId: string, page = 1, limit = 100) => {
const playlistId = extractAppleID(String(rawId ?? ''), 'playlist')
if (!playlistId) throw new Error(`invalid apple music playlist id/link: ${rawId}`)
const cfg = getAppleConfig()
const body: any = await appleAmpGet(`/v1/catalog/${cfg.storefront}/playlists/${playlistId}`, {
'limit[tracks]': 300,
extend: 'extendedAssetUrls',
})
const item = (body?.data ?? [])[0]
if (!item) throw new Error(`apple playlist not found: ${playlistId}`)
const tracks = await fetchAllTrackResources(item?.relationships?.tracks)
const songs = tracks.map((track: any) => appleSongFromCatalog(track))
const p = Math.max(1, Number(page) || 1)
const l = Math.max(1, Number(limit) || 100)
const summary = applePlaylistToPlaylist(item)
if (!summary.trackCount || songs.length > summary.trackCount) summary.trackCount = songs.length
return {
source: 'apple' as const,
info: {
id: playlistId,
name: summary.name,
img: summary.cover,
desc: summary.description,
author: summary.creator,
link: summary.link,
trackCount: summary.trackCount,
},
list: songs.slice((p - 1) * l, (p - 1) * l + l),
page: p,
limit: l,
total: songs.length,
}
}
/** 上游 GetPlaylistSongs:只返回曲目数组 */
export const getPlaylistSongs = async(id: string): Promise<any[]> => {
const { list } = await getPlaylistDetail(id, 1, 100000)
return list
}
/** 上游 ParsePlaylist:歌单链接 → 详情(曲目全量,不分页) */
export const parsePlaylistLink = async(link: string) => {
const detail = await getPlaylistDetail(link, 1, 100000)
return {
source: 'apple' as const,
id: detail.info.id,
info: detail.info,
list: detail.list,
page: 1,
limit: Math.max(detail.total, 1),
total: detail.total,
}
}
export default {
searchPlaylist,
getPlaylistDetail,
getPlaylistSongs,
getCategories,
getCategoryPlaylists,
parsePlaylistLink,
}