Files
MusicServer/sdk/wy/userPlaylist.ts
T
snowy-yang 68e071b166 feat(sdk): kg/kw/tx/wy 补齐专辑、歌单与 parse 能力
四个主流平台从「搜索 + 取流」补到能力对齐,新增 playlist/parse
(tx、wy 另加 userPlaylist),顺带修掉酷我直链的一个真实缺陷。

- kw:酷我 CDN 回吐的 query 用 `$` 顶替 `=`(`?bitrate$320&format$mp3…`),
  `$` 非 URL 标准分隔符,播放器与解析器普遍不认,导致拿到直链也放不出声;
  统一还原成 `=` 再对外返回
- wy:补齐 album/playlist/parse/userPlaylist,与 kg/tx 能力对齐
- kg/kw/tx:接入 parse(跨平台精确匹配的输入源)与歌单

为什么单独一次提交:这批改动不动架构,只做能力补齐与 URL 规范化,
与新增平台的注册表改动分开,出问题时便于单独回滚。
2026-10-03 00:26:51 +08:00

96 lines
3.5 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/user_playlist.go 移植)
// 需登录 cookie MUSIC_U(sdk/auth/store.ts 的 getCredentials('wy').cookie / musicU):
// 未登录抛出清晰错误,不返回空列表
// 1) weapi/nuser/account/get → 当前用户 userId
// 2) weapi/user/playlist → 用户歌单列表(分页 limit/offset)
import { httpFetch } from '../request.ts'
import { weapi } from './utils/crypto.ts'
import { formatPlayCount } from '../index.ts'
import { getCredentials, getWyCookie } from '../auth/store.ts'
import type { WyCredentials } from '../auth/store.ts'
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
const playlistLink = (id: string) => `https://music.163.com/#/playlist?id=${id}`
const readCookie = (): string => {
const cred = getCredentials('wy') as WyCredentials | null
const musicU = String(cred?.musicU ?? '').trim()
if (musicU) return `MUSIC_U=${musicU}`
return String(cred?.cookie ?? getWyCookie()).trim()
}
const weapiPost = async(url: string, data: object, cookie: string) => {
const { statusCode, body } = await httpFetch(url, {
method: 'post',
headers: {
'User-Agent': UA,
Referer: 'https://music.163.com/',
origin: 'https://music.163.com',
'Content-Type': 'application/x-www-form-urlencoded',
Cookie: cookie,
},
form: weapi(data),
timeout: 15000,
}).promise
if (statusCode !== 200 || !body || typeof body !== 'object') throw new Error('wy 个人歌单接口请求失败')
return body
}
/**
* 个人歌单列表(上游 GetUserPlaylists)
* 未登录(无 cookie/MUSIC_U)时抛出清晰错误,不返回空列表
*/
export const getList = async(page = 1, limit = 30) => {
const cookie = readCookie()
if (!cookie) throw new Error('wy 个人歌单需要登录 cookie(MUSIC_U),请先登录网易云')
const safePage = Math.max(1, Number(page) || 1)
let safeLimit = Number(limit) || 30
if (safeLimit <= 0) safeLimit = 30
if (safeLimit > 100) safeLimit = 100
const account = await weapiPost('https://music.163.com/weapi/nuser/account/get', { csrf_token: '' }, cookie)
if (account.code !== 200 || !account?.profile?.userId) {
throw new Error(`wy 登录态无效或已过期 (code ${account?.code ?? 'unknown'})`)
}
const uid = account.profile.userId
const nickname = account.profile.nickname ?? ''
const body = await weapiPost('https://music.163.com/weapi/user/playlist', {
uid,
limit: safeLimit,
offset: (safePage - 1) * safeLimit,
includeVideo: true,
csrf_token: '',
}, cookie)
if (body.code !== 200) throw new Error(`wy 个人歌单获取失败 (code ${body.code})`)
const list = (body.playlist ?? []).map(item => {
const id = String(item?.id ?? '').trim()
if (!id) return null
const trackCount = Number(item?.trackCount ?? 0) || 0
const playCount = Number(item?.playCount ?? 0) || 0
const creator = String(item?.creator?.nickname ?? '').trim() || nickname
return {
id,
name: item?.name ?? '',
cover: item?.coverImgUrl ?? '',
trackCount,
playCount,
play_count: formatPlayCount(playCount),
creator,
description: item?.description ?? '',
subscribed: Boolean(item?.subscribed),
link: playlistLink(id),
source: 'wy' as const,
}
}).filter(Boolean)
return { list, page: safePage, limit: safeLimit, total: list.length, source: 'wy' as const }
}
export const getUserPlaylists = getList
export default { getList, getUserPlaylists }