Files
MusicServer/sdk/wy/playlist.ts
T
snowy-yang 8c02fb5c15 refactor(sdk): 审计 P2 卫生项清理——请求层收敛与死代码清除
- 删除旧 sdk/request.ts,全部 import 收敛到 sdk/common/request.ts(85 文件)
- 注释死代码 630+ 行、裸 console.log 吞错 6 处、未使用符号 26 处清零
- bili 状态校验去重;utils DNS 预解析失败日志改 warn 带上下文
- CODE-AUDIT.md:第四节 P2 清理记录 + 第八节线索逐条复核结论(kw/kg/tx/wy parse 骨架相似实质各异,不抽公共层)
2026-10-03 20:48:54 +08:00

169 lines
6.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.
// 网易云歌单(上游 music-lib/netease/playlist.go 移植)
// 搜索/分类:复用现有 wy/songList(search=cloudsearch type1000、getTag=catalogue、getList=playlist/list)
// 推荐:weapi/personalized/playlist(上游 GetRecommendedPlaylists,匿名可用)
// 详情/曲目:复用 songList.getListDetail(linuxapi forward),按 page/limit 切片
// 链接解析:#/playlist?id= / /playlist?id= / /playlist/<id>/<n>/... / 裸数字 id(上游 parseNeteaseLink)
import { httpFetch } from '../common/request.ts'
import { weapi } from './utils/crypto.ts'
import { formatPlayCount } from '../index.ts'
import { getWyCookie } from '../auth/store.ts'
import songList from './songList.ts'
import { parseNeteaseLink } from './link.ts'
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
// catalogue/list 接口固定页大小(songList.limit_list = 30,运行时读取以避免模块环上的 TDZ)
const apiLimit = () => Number(songList.limit_list) || 30
const playlistLink = (id: string) => `https://music.163.com/#/playlist?id=${id}`
/** 歌单摘要:兼容既有 songList 字段并补 spec 形状(id/name/cover/trackCount/creator/...) */
const toSummary = (item: any, categoryId?: string) => {
const id = String(item?.id ?? '').trim()
if (!id) return null
const trackCount = Number(item?.trackCount ?? item?.total ?? 0) || 0
const playCount = Number(item?.playCount ?? 0) || 0
return {
...item,
id,
name: item?.name ?? '',
cover: item?.cover ?? item?.img ?? '',
trackCount,
playCount,
play_count: item?.play_count ?? formatPlayCount(playCount),
creator: item?.creator ?? item?.author ?? '',
description: item?.description ?? item?.desc ?? '',
link: playlistLink(id),
source: 'wy' as const,
...(categoryId ? { categoryId } : {}),
}
}
/** 歌单搜索(上游 SearchPlaylist;复用 songList.search) */
export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => {
const result = await songList.search(String(keyword ?? ''), page, limit)
const list = (result?.list ?? []).map(item => toSummary(item)).filter(Boolean)
return { list, page, limit, total: result?.total ?? list.length, source: 'wy' as const }
}
/** 歌单详情(上游 fetchPlaylistDetail;复用 songList.getListDetail,按 page/limit 切片) */
export const getPlaylistDetail = async(id: string, page = 1, limit = 50) => {
const detail = await songList.getListDetail(String(id ?? ''), 1)
const start = Math.max(0, (page - 1) * limit)
return {
source: 'wy' as const,
info: detail.info,
list: detail.list.slice(start, start + limit),
page,
limit,
total: detail.total ?? detail.list.length,
}
}
/** 歌单曲目全量(上游 GetPlaylistSongs) */
export const getPlaylistSongs = async(id: string) => (await songList.getListDetail(String(id ?? ''), 1)).list
/** 歌单分类(上游 GetPlaylistCategories;复用 songList.getTag/getHotTag 并摊平) */
export const getCategories = async() => {
const [groups, hotTags] = await Promise.all([
songList.getTag().catch(() => []),
songList.getHotTag().catch(() => []),
])
const hotSet = new Set((hotTags ?? []).map((item: any) => String(item.id)))
const list: any[] = [{ id: '', name: '全部', group: '全部' }]
const seen = new Set<string>([''])
for (const group of groups ?? []) {
for (const item of group?.list ?? []) {
const id = String(item?.id ?? '').trim()
const name = String(item?.name ?? '').trim()
if (!id || !name || seen.has(id)) continue
seen.add(id)
list.push({ id, name, group: group.name ?? '', hot: hotSet.has(id) })
}
}
return { source: 'wy' as const, list }
}
/** 分类歌单(上游 GetCategoryPlaylists;接口每页 30 条,按入参 limit 重新切片) */
export const getCategoryPlaylists = async(categoryId: string, page = 1, limit = 20) => {
const cat = String(categoryId ?? '').trim() || '全部'
const start = Math.max(0, (page - 1) * limit)
const apiPages = Math.max(1, Math.ceil((start + limit) / apiLimit()))
const items: any[] = []
let total = 0
for (let apiPage = 1; apiPage <= apiPages; apiPage++) {
const result = await songList.getList('hot', cat, apiPage)
total = Number(result?.total ?? total) || total
const batch = result?.list ?? []
items.push(...batch)
if (batch.length < apiLimit()) break
}
const list = items.slice(start, start + limit).map(item => toSummary(item, cat)).filter(Boolean)
return { list, page, limit, total: total || items.length, source: 'wy' as const }
}
/** 推荐歌单(上游 GetRecommendedPlaylists:weapi/personalized/playlist) */
export const getRecommended = async(page = 1, limit = 20) => {
const cookie = getWyCookie()
const { statusCode, body } = await httpFetch('https://music.163.com/weapi/personalized/playlist', {
method: 'post',
headers: {
'User-Agent': UA,
Referer: 'https://music.163.com/',
origin: 'https://music.163.com',
...(cookie ? { cookie } : {}),
},
form: weapi({ limit: 30, total: true, n: 1000 }),
timeout: 15000,
}).promise
if (statusCode !== 200 || body?.code != 200) {
throw new Error(`wy 推荐歌单获取失败 (code ${body?.code ?? statusCode})`)
}
const rawList: any[] = body?.result ?? []
const list = rawList.map(item => {
const id = String(item?.id ?? '').trim()
if (!id) return null
const playCount = Number(item?.playCount ?? 0) || 0
return {
id,
name: item?.name ?? '',
cover: item?.picUrl ?? '',
trackCount: Number(item?.trackCount ?? 0) || 0,
playCount,
play_count: formatPlayCount(playCount),
creator: item?.copywriter || '网易云推荐',
description: item?.copywriter ?? '',
link: playlistLink(id),
alg: item?.alg ?? '',
source: 'wy' as const,
}
}).filter(Boolean)
if (!list.length) throw new Error('no recommended playlists found')
const start = Math.max(0, (page - 1) * limit)
return { list: list.slice(start, start + limit), page, limit, total: list.length, source: 'wy' as const }
}
export const getRecommendedPlaylists = getRecommended
/** 歌单链接解析(上游 ParsePlaylist) */
export const parsePlaylistLink = async(link: string) => {
const raw = String(link ?? '').trim()
if (!raw) throw new Error('无效的网易云歌单链接')
if (/^\d+$/.test(raw)) return getPlaylistDetail(raw)
const { kind, id } = parseNeteaseLink(raw)
if (kind !== 'playlist') throw new Error('无效的网易云歌单链接')
return getPlaylistDetail(id)
}
export default {
searchPlaylist,
getPlaylistDetail,
getPlaylistSongs,
getCategories,
getCategoryPlaylists,
getRecommended,
getRecommendedPlaylists,
parsePlaylistLink,
}