四个主流平台从「搜索 + 取流」补到能力对齐,新增 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 规范化, 与新增平台的注册表改动分开,出问题时便于单独回滚。
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
// 网易云链接解析(上游 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 }
|