Files
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

79 lines
2.6 KiB
TypeScript
Raw Permalink 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/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 }