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:
1 parent
33b96f92c4
commit
68e071b166
20 files changed
+2218
-33
No files matched your search
@@ -0,0 +1,80 @@
|
||||
// 网易云链接解析(上游 music-lib/netease/song.go 的 Parse 移植)
|
||||
// 歌曲链接形态:
|
||||
// https://music.163.com/#/song?id=29732995
|
||||
// https://music.163.com/song?id=29732995
|
||||
// https://music.163.com/song/29732995
|
||||
// 裸数字 id 也接受(补充便利;上游仅接受链接)
|
||||
// 直链 url/type 为尽力附带,失败不影响元数据返回(与上游一致);
|
||||
// 歌单/专辑链接抛清晰错误,提示使用对应 parsePlaylist/parseAlbum。
|
||||
import { httpFetch } from '../request.ts'
|
||||
import { weapi } from './utils/crypto.ts'
|
||||
import { getWyCookie } from '../auth/store.ts'
|
||||
import wyApi from '../apis/wy.ts'
|
||||
import { convertSong, parseAlbumLink } from './album.ts'
|
||||
import { parsePlaylistLink } from './playlist.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'
|
||||
|
||||
/** 上游 fetchSongsBatch 的单曲版本(weapi/v3/song/detail) */
|
||||
const fetchSongById = async(songId: string) => {
|
||||
const cookie = getWyCookie()
|
||||
const { statusCode, body } = await httpFetch('https://music.163.com/weapi/v3/song/detail', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'User-Agent': UA,
|
||||
Referer: 'https://music.163.com/',
|
||||
origin: 'https://music.163.com',
|
||||
...(cookie ? { cookie } : {}),
|
||||
},
|
||||
form: weapi({
|
||||
c: `[{"id":${songId}}]`,
|
||||
ids: `[${songId}]`,
|
||||
}),
|
||||
timeout: 15000,
|
||||
}).promise
|
||||
if (statusCode !== 200 || body?.code != 200) {
|
||||
throw new Error(`wy 歌曲详情获取失败 (code ${body?.code ?? statusCode})`)
|
||||
}
|
||||
const item = body?.songs?.[0]
|
||||
if (!item?.id) throw new Error('netease song not found')
|
||||
return convertSong(item)
|
||||
}
|
||||
|
||||
/** 上游 Parse:歌曲链接 → Song(尽力附带直链 url/type) */
|
||||
export const parseSong = async(link: string) => {
|
||||
const raw = String(link ?? '').trim()
|
||||
if (!raw) throw new Error('无效的网易云歌曲链接')
|
||||
|
||||
let songId = ''
|
||||
if (/^\d+$/.test(raw)) {
|
||||
songId = raw
|
||||
} else {
|
||||
const parsed = (() => {
|
||||
try {
|
||||
return parseNeteaseLink(raw)
|
||||
} catch {
|
||||
throw new Error('无效的网易云歌曲链接')
|
||||
}
|
||||
})()
|
||||
if (parsed.kind === 'playlist') throw new Error('检测到网易云歌单链接,请使用 parsePlaylist')
|
||||
if (parsed.kind === 'album') throw new Error('检测到网易云专辑链接,请使用 parseAlbum')
|
||||
if (parsed.kind !== 'song') throw new Error('无效的网易云歌曲链接')
|
||||
songId = parsed.id
|
||||
}
|
||||
|
||||
const song: any = await fetchSongById(songId)
|
||||
try {
|
||||
const hit = await wyApi.getMusicUrl(song, '320k').promise
|
||||
song.url = hit.url
|
||||
song.type = hit.type
|
||||
} catch {
|
||||
// 与上游一致:直链获取失败忽略(VIP/下架曲目仍返回元数据)
|
||||
}
|
||||
return song
|
||||
}
|
||||
|
||||
export const parseAlbum = async(link: string) => parseAlbumLink(link)
|
||||
export const parsePlaylist = async(link: string) => parsePlaylistLink(link)
|
||||
|
||||
export default { parseSong, parseAlbum, parsePlaylist }
|
||||
Reference in new issue
Block a user