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 规范化,
与新增平台的注册表改动分开,出问题时便于单独回滚。
This commit is contained in:
snowy-yang committed 2026-10-03 00:26:51 +08:00
1 parent 33b96f92c4
commit 68e071b166
20 files changed
+2218 -33

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
// 网易云链接解析(上游 music-lib/netease/netease.go 的 parseNeteaseLink 1:1 移植)
// 支持:
// 歌曲 https://music.163.com/#/song?id=29732995 /song?id= / /song/29732995
// 专辑 https://music.163.com/#/album?id=32311 /album?id= / /album/32311
// 歌单 https://music.163.com/#/playlist?id=7507256008 /playlist?id= / /playlist/<id>/<n>/...
// 裸数字 id 由各模块自行兜底(上游 Parse 不接受裸 id)
export type NeteaseLinkKind = 'song' | 'album' | 'playlist'
const isDigits = (value: string): boolean => value !== '' && /^\d+$/.test(value)
const BASE = 'https://music.163.com'
/** 上游 parseNeteaseLinkCandidate(JS 需给相对候选补 base;Go 的 url.Parse 直接接受相对路径) */
const parseCandidate = (candidate: string): { kind: NeteaseLinkKind | '', id: string } => {
let parsed: URL
try {
parsed = new URL(candidate, BASE)
} catch {
return { kind: '', id: '' }
}
let kind: NeteaseLinkKind | '' = ''
const segments = parsed.pathname.toLowerCase().split('/').filter(Boolean)
for (const segment of segments) {
if (segment === 'song' || segment === 'album' || segment === 'playlist') kind = segment
}
let id = parsed.searchParams.get('id') ?? ''
if (!isDigits(id)) id = ''
if (!kind && segments.length >= 2) {
const last = segments[segments.length - 1]!
const prev = segments[segments.length - 2]!
if (isDigits(last) && (prev === 'song' || prev === 'album' || prev === 'playlist')) {
kind = prev as NeteaseLinkKind
id = last
}
}
if (!kind || !id) return { kind: '', id: '' }
return { kind, id }
}
/** 上游 parseNeteaseLink:依次尝试原链接、path?query、hash 片段 */
export const parseNeteaseLink = (link: string): { kind: NeteaseLinkKind, id: string } => {
const raw = String(link ?? '').trim()
const candidates: string[] = [raw]
const parsed = (() => {
try {
return new URL(raw, BASE)
} catch {
return null
}
})()
if (parsed) {
if (parsed.pathname && parsed.pathname !== '/') {
candidates.push(parsed.pathname + parsed.search)
}
const fragment = parsed.hash.replace(/^#!?/, '').trim()
if (fragment) candidates.push(fragment)
}
for (const candidate of candidates) {
const { kind, id } = parseCandidate(candidate)
if (kind && id) return { kind, id }
}
throw new Error('无效的网易云链接')
}
/** 裸 id 兜底:非 URL 且全数字时视为对应类型的 id */
export const bareId = (link: string): string => {
const raw = String(link ?? '').trim()
return /^\d+$/.test(raw) ? raw : ''
}
export default { parseNeteaseLink, bareId }