refactor: 架构整固——分层单向化、全量 TypeScript、直接运行

- 移除 bd/mg 两平台及磁盘残留产物;清理 api-test 死脚本、musicSdk/message+options 死模块、infSign vendor
- musicSdk/ 改名 sdk/;src/common、src/auth、src/apis 整体移入 sdk/,依赖方向定为 src → sdk 严格单向
- src/ 手写 JS 全量转 .ts,删除 JS 类型垫片 modules.d.ts;源码 import 统一 .ts 后缀
- Node 原生类型剥离直接运行 .ts(engines >=22.18),取消 tsc 发射产物,tsc 仅类型检查(pnpm typecheck)
- 删除 kwdes 备用实现与 src/index.ts 转发垫片;测试移至 test/smoke-test.ts
- common/request.ts 补显式导出类型;修复 set-cookie 单值形态、ApiError 类字段等隐患

验证:tsc 零错误,smoke-test 41/41
This commit is contained in:
snowy-yang committed 2026-09-25 23:06:23 +08:00
1 parent c8c939aa32
commit 354df2e739
126 files changed
+413 -2942

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
import { httpFetch } from '../request.ts'
import { weapi } from './utils/crypto.ts'
import { dateFormat2 } from '../index.ts'
import type { HttpRequestObject } from '../types.ts'
const emojis = [
['大笑', '😃'],
['可爱', '😊'],
['憨笑', '☺️'],
['色', '😍'],
['亲亲', '😙'],
['惊恐', '😱'],
['流泪', '😭'],
['亲', '😚'],
['呆', '😳'],
['哀伤', '😔'],
['呲牙', '😁'],
['吐舌', '😝'],
['撇嘴', '😒'],
['怒', '😡'],
['奸笑', '😏'],
['汗', '😓'],
['痛苦', '😖'],
['惶恐', '😰'],
['生病', '😨'],
['口罩', '😷'],
['大哭', '😂'],
['晕', '😵'],
['发怒', '👿'],
['开心', '😄'],
['鬼脸', '😜'],
['皱眉', '😞'],
['流感', '😢'],
['爱心', '❤️'],
['心碎', '💔'],
['钟情', '💘'],
['星星', '⭐️'],
['生气', '💢'],
['便便', '💩'],
['强', '👍'],
['弱', '👎'],
['拜', '🙏'],
['牵手', '👫'],
['跳舞', '👯‍♀️'],
['禁止', '🙅‍♀️'],
['这边', '💁‍♀️'],
['爱意', '💏'],
['示爱', '👩‍❤️‍👨'],
['嘴唇', '👄'],
['狗', '🐶'],
['猫', '🐱'],
['猪', '🐷'],
['兔子', '🐰'],
['小鸡', '🐤'],
['公鸡', '🐔'],
['幽灵', '👻'],
['圣诞', '🎅'],
['外星', '👽'],
['钻石', '💎'],
['礼物', '🎁'],
['男孩', '👦'],
['女孩', '👧'],
['蛋糕', '🎂'],
['18', '🔞'],
['圈', '⭕'],
['叉', '❌'],
]
const applyEmoji = (text: string): string => {
for (const e of emojis) text = text.replaceAll(`[${e[0]}]`, e[1])
return text
}
let cursorTools = {
cache: {} as Record<string, {
page?: number
prevCursor?: number
cursor?: number
orderType?: number
offset?: number
}>,
getCursor(id, page, limit) {
let cacheData = this.cache[id]
if (!cacheData) cacheData = this.cache[id] = {}
let orderType
let cursor
let offset
if (page == 1) {
cacheData.page = 1
cursor = cacheData.cursor = cacheData.prevCursor = Date.now()
orderType = 1
offset = 0
} else if (cacheData.page) {
cursor = cacheData.cursor
if (page > cacheData.page) {
orderType = 1
offset = (page - cacheData.page - 1) * limit
} else if (page < cacheData.page) {
orderType = 0
offset = (cacheData.page - page - 1) * limit
} else {
cursor = cacheData.cursor = cacheData.prevCursor
offset = cacheData.offset
orderType = cacheData.orderType
}
}
return {
orderType,
cursor,
offset,
}
},
setCursor(id, cursor, orderType, offset, page) {
let cacheData = this.cache[id]
if (!cacheData) cacheData = this.cache[id] = {}
cacheData.prevCursor = cacheData.cursor
cacheData.cursor = cursor
cacheData.orderType = orderType
cacheData.offset = offset
cacheData.page = page
},
}
export default {
_requestObj: null as HttpRequestObject | null,
_requestObj2: null as HttpRequestObject | null,
async getComment({ songmid }, page = 1, limit = 20) {
if (this._requestObj) this._requestObj.cancelHttp()
const id = 'R_SO_4_' + songmid
const cursorInfo = cursorTools.getCursor(songmid, page, limit)
const _requestObj = httpFetch('https://music.163.com/weapi/comment/resource/comments/get', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
origin: 'https://music.163.com',
Refere: 'http://music.163.com/',
},
form: weapi({
cursor: cursorInfo.cursor,
offset: cursorInfo.offset,
orderType: cursorInfo.orderType,
pageNo: page,
pageSize: limit,
rid: id,
threadId: id,
}),
})
const { body, statusCode } = await _requestObj.promise
// console.log(body)
if (statusCode != 200 || body.code !== 200) throw new Error('获取评论失败')
cursorTools.setCursor(songmid, body.data.cursor, cursorInfo.orderType, cursorInfo.offset, page)
return { source: 'wy', comments: this.filterComment(body.data.comments), total: body.data.totalCount, page, limit, maxPage: Math.ceil(body.data.totalCount / limit) || 1 }
},
async getHotComment({ songmid }, page = 1, limit = 100) {
if (this._requestObj2) this._requestObj2.cancelHttp()
const id = 'R_SO_4_' + songmid
page = page - 1
const _requestObj2 = httpFetch(`https://music.163.com/weapi/v1/resource/hotcomments/${id}`, {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
origin: 'https://music.163.com',
Refere: 'http://music.163.com/',
},
form: weapi({
rid: id,
limit,
offset: limit * page,
beforeTime: Date.now().toString(),
}),
})
const { body, statusCode } = await _requestObj2.promise
if (statusCode != 200 || body.code !== 200) throw new Error('获取热门评论失败')
const total = body.total ?? 0
return { source: 'wy', comments: this.filterComment(body.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }
},
filterComment(rawList) {
return rawList.map(item => {
let data = {
id: item.commentId,
text: item.content ? applyEmoji(item.content) : '',
time: item.time ? item.time : '',
timeStr: item.time ? dateFormat2(item.time) : '',
location: item.ipLocation?.location,
userName: item.user.nickname,
avatar: item.user.avatarUrl,
userId: item.user.userId,
likedCount: item.likedCount,
reply: [],
}
let replyData = item.beReplied && item.beReplied[0]
return replyData
? {
id: item.commentId,
rootId: replyData.beRepliedCommentId,
text: replyData.content ? applyEmoji(replyData.content) : '',
time: item.time,
timeStr: null,
location: replyData.ipLocation?.location,
userName: replyData.user.nickname,
avatar: replyData.user.avatarUrl,
userId: replyData.user.userId,
likedCount: null,
reply: [data],
}
: data
})
},
}
+21
View File
@@ -0,0 +1,21 @@
import { eapiRequest } from './utils/index.ts'
import type { HttpRequestObject } from '../types.ts'
export default {
_requestObj: null as HttpRequestObject | null,
async getList(retryNum = 0) {
if (this._requestObj) this._requestObj.cancelHttp()
if (retryNum > 2) return Promise.reject(new Error('try max num'))
const _requestObj = eapiRequest('/api/search/chart/detail', {
id: 'HOT_SEARCH_SONG#@#',
})
const { body, statusCode } = await _requestObj.promise
if (statusCode != 200 || body.code !== 200) throw new Error('获取热搜词失败')
return { source: 'wy', list: this.filterList(body.data.itemList) }
},
filterList(rawList) {
return rawList.map(item => item.searchWord)
},
}
+35
View File
@@ -0,0 +1,35 @@
import leaderboard from './leaderboard.ts'
import { apis } from '../api-source.ts'
import getLyric from './lyric.ts'
import getMusicInfo from './musicInfo.ts'
import musicSearch from './musicSearch.ts'
import songList from './songList.ts'
import hotSearch from './hotSearch.ts'
import comment from './comment.ts'
import singer from './singer.ts'
// import tipSearch from './tipSearch.ts'
const wy = {
// tipSearch,
leaderboard,
musicSearch,
songList,
hotSearch,
comment,
singer,
getMusicUrl(songInfo, type) {
return apis('wy').getMusicUrl!(songInfo, type)
},
getLyric(songInfo) {
return getLyric(songInfo.songmid)
},
getPic(songInfo) {
const requestObj = getMusicInfo(songInfo.songmid)
return requestObj.promise.then(info => info.al.picUrl)
},
getMusicDetailPageUrl(songInfo) {
return `https://music.163.com/#/song?id=${songInfo.songmid}`
},
}
export default wy
+209
View File
@@ -0,0 +1,209 @@
import { weapi } from './utils/crypto.ts'
import { httpFetch } from '../request.ts'
import musicDetailApi from './musicDetail.ts'
import type { HttpRequestObject } from '../types.ts'
const topList = [{ id: 'wy__19723756', name: '飙升榜', bangid: '19723756' },
{ id: 'wy__3779629', name: '新歌榜', bangid: '3779629' },
{ id: 'wy__2884035', name: '原创榜', bangid: '2884035' },
{ id: 'wy__3778678', name: '热歌榜', bangid: '3778678' },
{ id: 'wy__991319590', name: '说唱榜', bangid: '991319590' },
{ id: 'wy__71384707', name: '古典榜', bangid: '71384707' },
{ id: 'wy__1978921795', name: '电音榜', bangid: '1978921795' },
{ id: 'wy__5453912201', name: '黑胶VIP爱听榜', bangid: '5453912201' },
{ id: 'wy__71385702', name: 'ACG榜', bangid: '71385702' },
{ id: 'wy__745956260', name: '韩语榜', bangid: '745956260' },
{ id: 'wy__10520166', name: '国电榜', bangid: '10520166' },
{ id: 'wy__180106', name: 'UK排行榜周榜', bangid: '180106' },
{ id: 'wy__60198', name: '美国Billboard榜', bangid: '60198' },
{ id: 'wy__3812895', name: 'Beatport全球电子舞曲榜', bangid: '3812895' },
{ id: 'wy__21845217', name: 'KTV唛榜', bangid: '21845217' },
{ id: 'wy__60131', name: '日本Oricon榜', bangid: '60131' },
{ id: 'wy__2809513713', name: '欧美热歌榜', bangid: '2809513713' },
{ id: 'wy__2809577409', name: '欧美新歌榜', bangid: '2809577409' },
{ id: 'wy__27135204', name: '法国 NRJ Vos Hits 周榜', bangid: '27135204' },
{ id: 'wy__3001835560', name: 'ACG动画榜', bangid: '3001835560' },
{ id: 'wy__3001795926', name: 'ACG游戏榜', bangid: '3001795926' },
{ id: 'wy__3001890046', name: 'ACG VOCALOID榜', bangid: '3001890046' },
{ id: 'wy__3112516681', name: '中国新乡村音乐排行榜', bangid: '3112516681' },
{ id: 'wy__5059644681', name: '日语榜', bangid: '5059644681' },
{ id: 'wy__5059633707', name: '摇滚榜', bangid: '5059633707' },
{ id: 'wy__5059642708', name: '国风榜', bangid: '5059642708' },
{ id: 'wy__5338990334', name: '潜力爆款榜', bangid: '5338990334' },
{ id: 'wy__5059661515', name: '民谣榜', bangid: '5059661515' },
{ id: 'wy__6688069460', name: '听歌识曲榜', bangid: '6688069460' },
{ id: 'wy__6723173524', name: '网络热歌榜', bangid: '6723173524' },
{ id: 'wy__6732051320', name: '俄语榜', bangid: '6732051320' },
{ id: 'wy__6732014811', name: '越南语榜', bangid: '6732014811' },
{ id: 'wy__6886768100', name: '中文DJ榜', bangid: '6886768100' },
{ id: 'wy__6939992364', name: '俄罗斯top hit流行音乐榜', bangid: '6939992364' },
{ id: 'wy__7095271308', name: '泰语榜', bangid: '7095271308' },
{ id: 'wy__7356827205', name: 'BEAT排行榜', bangid: '7356827205' },
{ id: 'wy__7325478166', name: '编辑推荐榜VOL.44 天才女子摇滚乐队boygenius剖白卑微心迹', bangid: '7325478166' },
{ id: 'wy__7603212484', name: 'LOOK直播歌曲榜', bangid: '7603212484' },
{ id: 'wy__7775163417', name: '赏音榜', bangid: '7775163417' },
{ id: 'wy__7785123708', name: '黑胶VIP新歌榜', bangid: '7785123708' },
{ id: 'wy__7785066739', name: '黑胶VIP热歌榜', bangid: '7785066739' },
{ id: 'wy__7785091694', name: '黑胶VIP爱搜榜', bangid: '7785091694' },
]
export default {
limit: 100000,
list: [
{
id: 'wybsb',
name: '飙升榜',
bangid: '19723756',
},
{
id: 'wyrgb',
name: '热歌榜',
bangid: '3778678',
},
{
id: 'wyxgb',
name: '新歌榜',
bangid: '3779629',
},
{
id: 'wyycb',
name: '原创榜',
bangid: '2884035',
},
{
id: 'wygdb',
name: '古典榜',
bangid: '71384707',
},
{
id: 'wydouyb',
name: '抖音榜',
bangid: '2250011882',
},
{
id: 'wyhyb',
name: '韩语榜',
bangid: '745956260',
},
{
id: 'wydianyb',
name: '电音榜',
bangid: '1978921795',
},
{
id: 'wydjb',
name: '电竞榜',
bangid: '2006508653',
},
{
id: 'wyktvbb',
name: 'KTV唛榜',
bangid: '21845217',
},
],
getUrl(id) {
return `https://music.163.com/discover/toplist?id=${id}`
},
regExps: {
list: /<textarea id="song-list-pre-data" style="display:none;">(.+?)<\/textarea>/,
},
_requestBoardsObj: null as HttpRequestObject | null,
getBoardsData() {
if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp()
this._requestBoardsObj = httpFetch('https://music.163.com/weapi/toplist', {
method: 'post',
form: weapi({}),
})
return this._requestBoardsObj!.promise
},
getData(id) {
const requestBoardsDetailObj = httpFetch('https://music.163.com/weapi/v3/playlist/detail', {
method: 'post',
form: weapi({
id,
n: 100000,
p: 1,
}),
})
return requestBoardsDetailObj.promise
},
filterBoardsData(rawList) {
// console.log(rawList)
let list: any[] = []
for (const board of rawList) {
// 排除 MV榜
// if (board.id == 201) continue
list.push({
id: 'wy__' + board.id,
name: board.name,
bangid: String(board.id),
})
}
return list
},
async getBoards(retryNum = 0) {
// if (++retryNum > 3) return Promise.reject(new Error('try max num'))
// let response
// try {
// response = await this.getBoardsData()
// } catch (error) {
// return this.getBoards(retryNum)
// }
// console.log(response.body)
// if (response.statusCode !== 200 || response.body.code !== 200) return this.getBoards(retryNum)
// const list = this.filterBoardsData(response.body.list)
// console.log(list)
// console.log(JSON.stringify(list))
// this.list = list
// return {
// list,
// source: 'wy',
// }
this.list = topList
return {
list: topList,
source: 'wy',
}
},
async getList(bangid, page, retryNum = 0) {
if (++retryNum > 6) return Promise.reject(new Error('try max num'))
// console.log(bangid)
let resp
try {
resp = await this.getData(bangid)
} catch (err: any) {
if (err.message == 'try max num') {
throw err
} else {
return this.getList(bangid, page, retryNum)
}
}
if (resp.statusCode !== 200 || resp.body.code !== 200) return this.getList(bangid, page, retryNum)
// console.log(resp.body)
let musicDetail
try {
musicDetail = await (musicDetailApi as any).getList(resp.body.playlist.trackIds.map(trackId => trackId.id))
} catch (err: any) {
console.log(err)
if (err.message == 'try max num') {
throw err
} else {
return this.getList(bangid, page, retryNum)
}
}
// console.log(musicDetail)
return {
total: musicDetail.list.length,
list: musicDetail.list,
limit: this.limit,
page,
source: 'wy',
}
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('wy__', '')
return `https://music.163.com/#/discover/toplist?id=${id}`
},
}
+289
View File
@@ -0,0 +1,289 @@
import { httpFetch } from '../request.ts'
import { eapi } from './utils/crypto.ts'
import type { LyricInfo, HttpRequestObject } from '../types.ts'
// import { decodeName } from '../...ts'
// const parseLyric = (str, lrc) => {
// if (!str) return ''
// str = str.replace(/\r/g, '')
// let yrc = str.replace(/\[((\d+),\d+)\].*/g, str => {
// let result = str.match(/\[((\d+),\d+)\].*/)
// let time = parseInt(result[2])
// let ms = time % 1000
// time /= 1000
// let m = parseInt(time / 60).toString().padStart(2, '0')
// time %= 60
// let s = parseInt(time).toString().padStart(2, '0')
// time = `${m}:${s}.${ms}`
// str = str.replace(result[1], time)
// let startTime = 0
// str = str.replace(/\(0,1\) /g, ' ').replace(/\(\d+,\d+\)/g, time => {
// const [start, end] = time.replace(/^\((\d+,\d+)\)$/, '$1').split(',')
// time = `<${parseInt(startTime + parseInt(start))},${end}>`
// startTime = parseInt(startTime + parseInt(end))
// return time
// })
// return str
// })
// yrc = decodeName(yrc)
// return yrc.trim()
// }
const eapiRequest = (url: string, data: object) => {
return httpFetch('https://interface3.music.163.com/eapi/song/lyric/v1', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
origin: 'https://music.163.com',
// cookie: 'os=pc; deviceId=A9C064BB4584D038B1565B58CB05F95290998EE8B025AA2D07AE; osver=Microsoft-Windows-10-Home-China-build-19043-64bit; appver=2.5.2.197409; channel=netease; MUSIC_A=37a11f2eb9de9930cad479b2ad495b0e4c982367fb6f909d9a3f18f876c6b49faddb3081250c4980dd7e19d4bd9bf384e004602712cf2b2b8efaafaab164268a00b47359f85f22705cc95cb6180f3aee40f5be1ebf3148d888aa2d90636647d0c3061cd18d77b7a0; __csrf=05b50d54082694f945d7de75c210ef94; mode=Z7M-KP5(7)GZ; NMTID=00OZLp2VVgq9QdwokUgq3XNfOddQyIAAAF_6i8eJg; ntes_kaola_ad=1',
},
form: eapi(url, data),
})
// requestObj.promise = requestObj.promise.then(({ body }) => {
// // console.log(raw)
// console.log(body)
// // console.log(eapiDecrypt(raw))
// // return eapiDecrypt(raw)
// return body
// })
// return requestObj
}
const parseTools = {
rxps: {
info: /^{"/,
lineTime: /^\[(\d+),\d+\]/,
wordTime: /\(\d+,\d+,\d+\)/,
wordTimeAll: /(\(\d+,\d+,\d+\))/g,
},
msFormat(timeMs) {
if (Number.isNaN(timeMs)) return ''
let ms = timeMs % 1000
timeMs /= 1000
let m = parseInt((timeMs / 60) as any).toString().padStart(2, '0')
timeMs %= 60
let s = parseInt(timeMs as any).toString().padStart(2, '0')
return `[${m}:${s}.${ms}]`
},
parseLyric(lines) {
const yrcLines: string[] = []
const lrcLines: string[] = []
for (let line of lines) {
line = line.trim()
let result = this.rxps.lineTime.exec(line)
if (!result) {
if (line.startsWith('[offset')) {
yrcLines.push(line)
lrcLines.push(line)
}
continue
}
const startMsTime = parseInt(result[1])
const startTimeStr = this.msFormat(startMsTime)
if (!startTimeStr) continue
let words = line.replace(this.rxps.lineTime, '')
lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`)
let times = words.match(this.rxps.wordTimeAll)
if (!times) continue
times = times.map(time => {
const result = /\((\d+),(\d+),\d+\)/.exec(time)
return `<${Math.max(parseInt(result![1]) - startMsTime, 0)},${result![2]}>`
})
const wordArr = words.split(this.rxps.wordTime)
wordArr.shift()
const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')
yrcLines.push(`${startTimeStr}${newWords}`)
}
return {
lyric: lrcLines.join('\n'),
yrc: yrcLines.join('\n'),
}
},
parseHeaderInfo(str) {
str = str.trim()
str = str.replace(/\r/g, '')
if (!str) return null
const lines = str.split('\n')
return lines.map(line => {
if (!this.rxps.info.test(line)) return line
try {
const info = JSON.parse(line)
const timeTag = this.msFormat(info.t)
return timeTag ? `${timeTag}${info.c.map(t => t.tx).join('')}` : ''
} catch {
return ''
}
})
},
getIntv(interval) {
if (!interval) return 0
if (!interval.includes('.')) interval += '.0'
let arr = interval.split(/:|\./)
while (arr.length < 3) arr.unshift('0')
const [m, s, ms] = arr
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
},
fixTimeTag(lrc, targetlrc) {
let lrcLines = lrc.split('\n')
const targetlrcLines = targetlrc.split('\n')
const timeRxp = /^\[([\d:.]+)\]/
let temp: string[] = []
let newLrc: string[] = []
targetlrcLines.forEach((line) => {
const result = timeRxp.exec(line)
if (!result) return
const words = line.replace(timeRxp, '')
if (!words.trim()) return
const t1 = this.getIntv(result[1])
while (lrcLines.length) {
const lrcLine = lrcLines.shift()!
const lrcLineResult = timeRxp.exec(lrcLine)
if (!lrcLineResult) continue
const t2 = this.getIntv(lrcLineResult[1])
if (Math.abs(t1 - t2) < 100) {
const lrc = line.replace(timeRxp, lrcLineResult[0]).trim()
if (!lrc) continue
newLrc.push(lrc)
break
}
temp.push(lrcLine)
}
lrcLines = [...temp, ...lrcLines]
temp = []
})
return newLrc.join('\n')
},
parse(ylrc, ytlrc, yrlrc, lrc, tlrc, rlrc) {
const info = {
lyric: '',
tlyric: '',
rlyric: '',
yrc: '',
}
if (ylrc) {
let lines = this.parseHeaderInfo(ylrc)
if (lines) {
const result = this.parseLyric(lines)
if (ytlrc) {
const lines = this.parseHeaderInfo(ytlrc)
if (lines) {
// if (lines.length == result.lyricLines.length) {
info.tlyric = this.fixTimeTag(result.lyric, lines.join('\n'))
// } else info.tlyric = lines.join('\n')
}
}
if (yrlrc) {
const lines = this.parseHeaderInfo(yrlrc)
if (lines) {
// if (lines.length == result.lyricLines.length) {
info.rlyric = this.fixTimeTag(result.lyric, lines.join('\n'))
// } else info.rlyric = lines.join('\n')
}
}
const timeRxp = /^\[[\d:.]+\]/
const headers = lines.filter(l => timeRxp.test(l)).join('\n')
info.lyric = `${headers}\n${result.lyric}`
info.yrc = result.yrc
return info
}
}
if (lrc) {
const lines = this.parseHeaderInfo(lrc)
if (lines) info.lyric = lines.join('\n')
}
if (tlrc) {
const lines = this.parseHeaderInfo(tlrc)
if (lines) info.tlyric = lines.join('\n')
}
if (rlrc) {
const lines = this.parseHeaderInfo(rlrc)
if (lines) info.rlyric = lines.join('\n')
}
return info
},
}
// https://github.com/Binaryify/NeteaseCloudMusicApi/pull/1523/files
// export default songmid => {
// const requestObj = httpFetch('https://music.163.com/api/linux/forward', {
// method: 'post',
// 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
// form: linuxapi({
// method: 'POST',
// url: 'https://music.163.com/api/song/lyric?_nmclfl=1',
// params: {
// id: songmid,
// tv: -1,
// lv: -1,
// rv: -1,
// kv: -1,
// },
// }),
// })
// requestObj.promise = requestObj.promise.then(({ body }) => {
// if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))
// // console.log(body)
// return {
// lyric: body.lrc.lyric,
// tlyric: body.tlyric?.lyric ?? '',
// rlyric: body.romalrc?.lyric ?? '',
// // yrc: parseLyric(body.klyric.lyric),
// }
// })
// return requestObj
// }
// 参考社区通用的歌词时间标签修正思路
const fixTimeLabel = (lrc: string, tlrc?: string, romalrc?: string) => {
if (lrc) {
let newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]')
let newTlrc = tlrc?.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') ?? tlrc
if (newLrc != lrc || newTlrc != tlrc) {
lrc = newLrc
tlrc = newTlrc
if (romalrc) romalrc = romalrc.replace(/\[(\d{2}:\d{2}):(\d{2,3})]/g, '[$1.$2]').replace(/\[(\d{2}:\d{2}\.\d{2})0]/g, '[$1]')
}
}
return { lrc, tlrc, romalrc }
}
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/lyric_new.js
export default (songmid: string): HttpRequestObject => {
const requestObj = eapiRequest('/api/song/lyric/v1', {
id: songmid,
cp: false,
tv: 0,
lv: 0,
rv: 0,
kv: 0,
yv: 0,
ytv: 0,
yrv: 0,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
// console.log(body)
if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))
const fixTimeLabelLrc = fixTimeLabel(body.lrc.lyric, body.tlyric?.lyric, body.romalrc?.lyric)
const info = parseTools.parse(body.yrc?.lyric, body.ytlrc?.lyric, body.yromalrc?.lyric, fixTimeLabelLrc.lrc, fixTimeLabelLrc.tlrc, fixTimeLabelLrc.romalrc)
// console.log(info)
if (!info.lyric) return Promise.reject(new Error('Get lyric failed'))
return info
}) as any
return requestObj
}
+112
View File
@@ -0,0 +1,112 @@
import { httpFetch } from '../request.ts'
import { weapi } from './utils/crypto.ts'
import { formatPlayTime, sizeFormate } from '../index.ts'
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js
export default {
getSinger(singers) {
let arr: any[] = []
singers?.forEach(singer => {
arr.push(singer.name)
})
return arr.join('、')
},
filterList({ songs, privileges }) {
// console.log(songs, privileges)
const list: any[] = []
songs.forEach((item, index) => {
const types: Array<{ type: string, size: string | null }> = []
const _types: Record<string, { size: string | null }> = {}
let size
let privilege = privileges[index]
if (privilege.id !== item.id) privilege = privileges.find(p => p.id === item.id)
if (!privilege) return
if (privilege.maxBrLevel == 'hires') {
size = item.hr ? sizeFormate(item.hr.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
switch (privilege.maxbr) {
case 999000:
size = item.sq ? sizeFormate(item.sq.size) : null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
}
types.reverse()
if (item.pc) {
list.push({
singer: item.pc.ar ?? '',
name: item.pc.sn ?? '',
albumName: item.pc.alb ?? '',
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl ?? '',
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
} else {
list.push({
singer: this.getSinger(item.ar),
name: item.name ?? '',
albumName: item.al?.name,
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl,
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
}
})
// console.log(list)
return list
},
async getList(ids = [], retryNum = 0) {
if (retryNum > 2) return Promise.reject(new Error('try max num'))
const requestObj = httpFetch('https://music.163.com/weapi/v3/song/detail', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
origin: 'https://music.163.com',
},
form: weapi({
c: '[' + ids.map(id => ('{"id":' + id + '}')).join(',') + ']',
ids: '[' + ids.join(',') + ']',
}),
})
const { body, statusCode } = await requestObj.promise
if (statusCode != 200 || body.code !== 200) throw new Error('获取歌曲详情失败')
// console.log(body)
return { source: 'wy', list: this.filterList(body) }
},
}
+25
View File
@@ -0,0 +1,25 @@
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js
import { httpFetch } from '../request.ts'
import { weapi } from './utils/crypto.ts'
import type { RequestObj } from '../types.ts'
export default (songmid: string): RequestObj<any> => {
const requestObj = httpFetch('https://music.163.com/weapi/v3/song/detail', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
Referer: 'https://music.163.com/song?id=' + songmid,
origin: 'https://music.163.com',
},
form: weapi({
c: `[{"id":${songmid}}]`,
ids: `[${songmid}]`,
}),
})
requestObj.promise = requestObj.promise.then(({ body }) => {
// console.log(body)
if (body.code !== 200 || !body.songs.length) return Promise.reject(new Error('获取歌曲信息失败'))
return body.songs[0]
})
return requestObj
}
+119
View File
@@ -0,0 +1,119 @@
// import { httpFetch } from '../request.ts'
// import { weapi } from './utils/crypto.ts'
import { sizeFormate, formatPlayTime } from '../index.ts'
// import musicDetailApi from './musicDetail.ts'
import { eapiRequest } from './utils/index.ts'
export default {
limit: 30,
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page, limit) {
// const searchRequest = eapiRequest('/api/cloudsearch/pc', {
// s: str,
// type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
// limit,
// total: page == 1,
// offset: limit * (page - 1),
// })
const searchRequest = eapiRequest('/api/search/song/list/page', {
keyword: str,
needCorrect: '1',
channel: 'typing',
offset: limit * (page - 1),
scene: 'normal',
total: page == 1,
limit,
})
return searchRequest.promise.then(({ body }) => body)
},
getSinger(singers) {
let arr: any[] = []
singers.forEach(singer => {
arr.push(singer.name)
})
return arr.join('、')
},
handleResult(rawList) {
// console.log(rawList)
if (!rawList) return []
return rawList.map(item => {
item = item.baseInfo.simpleSongData
const types: Array<{ type: string, size: string | null }> = []
const _types: Record<string, { size: string | null }> = {}
let size
if (item.privilege.maxBrLevel == 'hires') {
size = item.hr ? sizeFormate(item.hr.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
switch (item.privilege.maxbr) {
case 999000:
size = item.sq ? sizeFormate(item.sq.size) : null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
}
types.reverse()
return {
singer: this.getSinger(item.ar),
name: item.name,
albumName: item.al.name,
albumId: item.al.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al.picUrl,
lrc: null,
types,
_types,
typeUrl: {},
}
})
},
search(str, page = 1, limit, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
if (limit == null) limit = this.limit
return this.musicSearch(str, page, limit).then(result => {
// console.log(result)
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
let list = this.handleResult(result.data.resources || [])
// console.log(list)
if (list == null) return this.search(str, page, limit, retryNum)
this.total = result.data.totalCount || 0
this.page = page
this.allPage = Math.ceil(this.total / this.limit)
return {
list,
allPage: this.allPage,
limit: this.limit,
total: this.total,
source: 'wy',
}
// return result.data
})
},
}
+155
View File
@@ -0,0 +1,155 @@
import { eapiRequest } from './utils/index.ts'
import { formatPlayTime, sizeFormate } from '../index.ts'
import { formatSingerName } from '../utils.ts'
export default {
/**
* 获取歌手信息
* @param {*} id
*/
getInfo(id) {
return eapiRequest('/api/artist/head/info/get', { id }).promise.then(({ body }) => {
if (!body || body.code != 200) throw new Error('get singer info faild.')
// 响应结构为 { code, data: { artist: { ... } } },无 body.user
const artist = body.data?.artist
if (!artist) throw new Error('get singer info faild.')
return {
source: 'wy',
id: artist.id,
info: {
name: artist.name,
desc: artist.briefDesc,
avatar: artist.avatar ?? artist.cover ?? null,
gender: null,
},
count: {
music: artist.musicSize,
album: artist.albumSize,
},
}
})
},
/**
* 获取歌手歌曲列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
getSongList(id, page = 1, limit = 100) {
return eapiRequest('/api/v2/artist/songs', {
id,
limit,
offset: (page - 1) * limit,
}).promise.then(({ body }) => {
if (!body.songs || body.code != 200) throw new Error('get singer song list faild.')
const list = this.filterSongList(body.songs)
return {
list,
limit,
page,
total: body.total,
source: 'wy',
}
})
},
/**
* 获取歌手专辑列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
getAlbumList(id, page = 1, limit = 10) {
return eapiRequest(`/api/artist/albums/${id}`, {
limit,
offset: (page - 1) * limit,
}).promise.then(({ body }) => {
if (!body.hotAlbums || body.code != 200) throw new Error('get singer album list faild.')
const list = this.filterAlbumList(body.hotAlbums)
return {
source: 'wy',
list,
limit,
page,
total: body.artist.albumSize,
}
})
},
filterAlbumList(raw) {
const list: any[] = []
raw.forEach(item => {
if (!item.id) return
list.push({
id: item.id,
count: item.size,
info: {
name: item.name,
author: formatSingerName(item.artists),
img: item.picUrl,
desc: null,
},
})
})
return list
},
filterSongList(raw) {
const list: any[] = []
raw.forEach(item => {
if (!item.id) return
const types: Array<{ type: string, size: string | null }> = []
const _types: Record<string, { size: string | null }> = {}
let size
item.privilege.chargeInfoList.forEach(i => {
switch (i.rate) {
case 128000:
size = item.lMusic ? sizeFormate(item.lMusic.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
break
case 320000:
size = item.hMusic ? sizeFormate(item.hMusic.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
break
case 999000:
size = item.sqMusic ? sizeFormate(item.sqMusic.size) : null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
break
case 1999000:
size = item.hrMusic ? sizeFormate(item.hrMusic.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
break
}
})
list.push({
singer: formatSingerName(item.artists),
name: item.name,
albumName: item.album.name,
albumId: item.album.id,
songmid: item.id,
source: 'wy',
interval: formatPlayTime((item.dt ?? item.duration) / 1000),
img: item.album?.picUrl ?? null,
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
},
}
+340
View File
@@ -0,0 +1,340 @@
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_catlist.js
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_hot.js
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/top_playlist.js
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_detail.js
import { weapi, linuxapi } from './utils/crypto.ts'
import { httpFetch } from '../request.ts'
import { formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index.ts'
import musicDetailApi from './musicDetail.ts'
import { eapiRequest } from './utils/index.ts'
import { formatSingerName } from '../utils.ts'
import type { HttpRequestObject } from '../types.ts'
export default {
_requestObj_tags: null as HttpRequestObject | null,
_requestObj_hotTags: null as HttpRequestObject | null,
_requestObj_list: null as HttpRequestObject | null,
limit_list: 30,
limit_song: 100000,
successCode: 200,
cookie: 'MUSIC_U=',
sortList: [
{
name: '最热',
id: 'hot',
},
// {
// name: '最新',
// id: 'new',
// },
],
regExps: {
listDetailLink: /^.+(?:\?|&)id=(\d+)(?:&.*$|#.*$|$)/,
listDetailLink2: /^.+\/playlist\/(\d+)\/\d+\/.+$/,
},
async handleParseId(link, retryNum = 0) {
if (retryNum > 2) throw new Error('link try max num')
const requestObj_listDetailLink = httpFetch(link)
const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise
// console.log(headers)
if (statusCode > 400) return this.handleParseId(link, ++retryNum)
const url = location == null ? link : location
return this.regExps.listDetailLink.test(url)
? url.replace(this.regExps.listDetailLink, '$1')
: url.replace(this.regExps.listDetailLink2, '$1')
},
async getListId(id) {
let cookie
if (/###/.test(id)) {
const [url, token] = id.split('###')
id = url
cookie = `MUSIC_U=${token}`
}
if ((/[?&:/]/.test(id))) {
if (this.regExps.listDetailLink.test(id)) {
id = id.replace(this.regExps.listDetailLink, '$1')
} else if (this.regExps.listDetailLink2.test(id)) {
id = id.replace(this.regExps.listDetailLink2, '$1')
} else {
id = await this.handleParseId(id)
}
// console.log(id)
}
return { id, cookie }
},
async getListDetail(rawId, page, tryNum = 0) { // 获取歌曲列表内的音乐
if (tryNum > 2) return Promise.reject(new Error('try max num'))
const { id, cookie } = await this.getListId(rawId)
if (cookie) this.cookie = cookie
const requestObj_listDetail = httpFetch('https://music.163.com/api/linux/forward', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
Cookie: this.cookie,
},
form: linuxapi({
method: 'POST',
url: 'https://music.163.com/api/v3/playlist/detail',
params: {
id,
n: this.limit_song,
s: 8,
},
}),
})
const { statusCode, body } = await requestObj_listDetail.promise
if (statusCode !== 200 || body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
let limit = 1000
let rangeStart = (page - 1) * limit
// console.log(body)
let list
if (body.playlist.trackIds.length == body.privileges.length) {
list = this.filterListDetail(body)
} else {
try {
list = (await (musicDetailApi as any).getList(body.playlist.trackIds.slice(rangeStart, limit * page).map(trackId => trackId.id))).list
} catch (err: any) {
console.log(err)
if (err.message == 'try max num') {
throw err
} else {
return this.getListDetail(id, page, ++tryNum)
}
}
}
// console.log(list)
return {
list,
page,
limit,
total: body.playlist.trackIds.length,
source: 'wy',
info: {
play_count: formatPlayCount(body.playlist.playCount),
name: body.playlist.name,
img: body.playlist.coverImgUrl,
desc: body.playlist.description,
author: body.playlist.creator.nickname,
},
}
},
filterListDetail({ playlist: { tracks }, privileges }) {
// console.log(tracks, privileges)
const list: any[] = []
tracks.forEach((item, index) => {
const types: Array<{ type: string, size: string | null }> = []
const _types: Record<string, { size: string | null }> = {}
let size
let privilege = privileges[index]
if (privilege.id !== item.id) privilege = privileges.find(p => p.id === item.id)
if (!privilege) return
if (privilege.maxBrLevel == 'hires') {
size = item.hr ? sizeFormate(item.hr.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
switch (privilege.maxbr) {
case 999000:
size = null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
}
types.reverse()
if (item.pc) {
list.push({
singer: item.pc.ar ?? '',
name: item.pc.sn ?? '',
albumName: item.pc.alb ?? '',
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl ?? '',
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
} else {
list.push({
singer: formatSingerName(item.ar, 'name'),
name: item.name ?? '',
albumName: item.al?.name,
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl,
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
}
})
return list
},
// 获取列表数据
getList(sortId, tagId, page, tryNum = 0) {
if (tryNum > 2) return Promise.reject(new Error('try max num'))
if (this._requestObj_list) this._requestObj_list.cancelHttp()
this._requestObj_list = httpFetch('https://music.163.com/weapi/playlist/list', {
method: 'post',
form: weapi({
cat: tagId || '全部', // 全部,华语,欧美,日语,韩语,粤语,小语种,流行,摇滚,民谣,电子,舞曲,说唱,轻音乐,爵士,乡村,R&B/Soul,古典,民族,英伦,金属,朋克,蓝调,雷鬼,世界音乐,拉丁,另类/独立,New Age,古风,后摇,Bossa Nova,清晨,夜晚,学习,工作,午休,下午茶,地铁,驾车,运动,旅行,散步,酒吧,怀旧,清新,浪漫,性感,伤感,治愈,放松,孤独,感动,兴奋,快乐,安静,思念,影视原声,ACG,儿童,校园,游戏,70后,80后,90后,网络歌曲,KTV,经典,翻唱,吉他,钢琴,器乐,榜单,00后
order: sortId, // hot,new
limit: this.limit_list,
offset: this.limit_list * (page - 1),
total: true,
}),
})
return this._requestObj_list!.promise.then(({ body }) => {
// console.log(body)
if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
return {
list: this.filterList(body.playlists),
total: parseInt(body.total),
page,
limit: this.limit_list,
source: 'wy',
}
})
},
filterList(rawData) {
// console.log(rawData)
return rawData.map(item => ({
play_count: formatPlayCount(item.playCount),
id: String(item.id),
author: item.creator.nickname,
name: item.name,
time: item.createTime ? dateFormat(item.createTime, 'Y-M-D') : '',
img: item.coverImgUrl,
grade: item.grade,
total: item.trackCount,
desc: item.description,
source: 'wy',
}))
},
// 获取标签
getTag(tryNum = 0) {
if (this._requestObj_tags) this._requestObj_tags.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_tags = httpFetch('https://music.163.com/weapi/playlist/catalogue', {
method: 'post',
form: weapi({}),
})
return this._requestObj_tags!.promise.then(({ body }) => {
// console.log(JSON.stringify(body))
if (body.code !== this.successCode) return this.getTag(++tryNum)
return this.filterTagInfo(body)
})
},
filterTagInfo({ sub, categories }) {
const subList = {}
for (const item of sub) {
if (!subList[item.category]) subList[item.category] = []
subList[item.category].push({
parent_id: categories[item.category],
parent_name: categories[item.category],
id: item.name,
name: item.name,
source: 'wy',
})
}
const list: any[] = []
for (const key of Object.keys(categories)) {
list.push({
name: categories[key],
list: subList[key],
source: 'wy',
})
}
return list
},
// 获取热门标签
getHotTag(tryNum = 0) {
if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_hotTags = httpFetch('https://music.163.com/weapi/playlist/hottags', {
method: 'post',
form: weapi({}),
})
return this._requestObj_hotTags!.promise.then(({ body }) => {
// console.log(JSON.stringify(body))
if (body.code !== this.successCode) return this.getTag(++tryNum)
return this.filterHotTagInfo(body.tags)
})
},
filterHotTagInfo(rawList) {
return rawList.map(item => ({
id: item.playlistTag.name,
name: item.playlistTag.name,
source: 'wy',
}))
},
getTags() {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'wy' }))
},
async getDetailPageUrl(rawId) {
const { id } = await this.getListId(rawId)
return `https://music.163.com/#/playlist?id=${id}`
},
search(text, page, limit = 20) {
return eapiRequest('/api/cloudsearch/pc', {
s: text,
type: 1000, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
limit,
total: page == 1,
offset: limit * (page - 1),
})
.promise.then(({ body }) => {
if (body.code != this.successCode) throw new Error('filed')
// console.log(body)
return {
list: this.filterList(body.result.playlists),
limit,
total: body.result.playlistCount,
source: 'wy',
}
})
},
}
// getList
// getTags
// getListDetail
+34
View File
@@ -0,0 +1,34 @@
import { httpFetch } from '../request.ts'
import { weapi } from './utils/crypto.ts'
import { formatSingerName } from '../utils.ts'
import type { HttpRequestObject } from '../types.ts'
export default {
requestObj: null as HttpRequestObject | null,
cancelTipSearch() {
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
},
tipSearchBySong(str) {
this.cancelTipSearch()
this.requestObj = httpFetch('https://music.163.com/weapi/search/suggest/web', {
method: 'POST',
headers: {
referer: 'https://music.163.com/',
origin: 'https://music.163.com/',
},
form: weapi({
s: str,
}),
})
return this.requestObj!.promise.then(({ statusCode, body }) => {
if (statusCode != 200 || body.code != 200) return Promise.reject(new Error('请求失败'))
return body.result.songs
})
},
handleResult(rawData) {
return rawData.map(info => `${info.name} - ${formatSingerName(info.artists, 'name')}`)
},
async search(str) {
return this.tipSearchBySong(str).then(result => this.handleResult(result))
},
}
+54
View File
@@ -0,0 +1,54 @@
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js
import { createCipheriv, createDecipheriv, publicEncrypt, randomBytes, createHash, constants } from 'crypto'
const iv = Buffer.from('0102030405060708')
const presetKey = Buffer.from('0CoJUm6Qyw8W8jud')
const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q')
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
const publicKey = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----'
const eapiKey = 'e82ckenh8dichen8'
const aesEncrypt = (buffer: Buffer, mode: string, key: Buffer | string, iv: Buffer | string): Buffer => {
const cipher = createCipheriv(mode, key, iv)
return Buffer.concat([cipher.update(buffer), cipher.final()])
}
const aesDecrypt = function(cipherBuffer: Buffer, mode: string, key: Buffer | string, iv: Buffer | string): Buffer {
let decipher = createDecipheriv(mode, key, iv)
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()])
}
const rsaEncrypt = (buffer: Buffer, key: string): Buffer => {
buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])
return publicEncrypt({ key, padding: constants.RSA_NO_PADDING }, buffer)
}
export const weapi = (object: object): { params: string, encSecKey: string } => {
const text = JSON.stringify(object)
const secretKey = randomBytes(16).map(n => ((base62.charAt(n % 62) as any).charCodeAt())) as Buffer
return {
params: aesEncrypt(Buffer.from(aesEncrypt(Buffer.from(text), 'aes-128-cbc', presetKey, iv).toString('base64')), 'aes-128-cbc', secretKey, iv).toString('base64'),
encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),
}
}
export const linuxapi = (object: object): { eparams: string } => {
const text = JSON.stringify(object)
return {
eparams: aesEncrypt(Buffer.from(text), 'aes-128-ecb', linuxapiKey, '').toString('hex').toUpperCase(),
}
}
export const eapi = (url: string, object: object | string): { params: string } => {
const text = typeof object === 'object' ? JSON.stringify(object) : object
const message = `nobody${url}use${text}md5forencrypt`
const digest = createHash('md5').update(message).digest('hex')
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`
return {
params: aesEncrypt(Buffer.from(data), 'aes-128-ecb', eapiKey, '').toString('hex').toUpperCase(),
}
}
export const eapiDecrypt = (cipherBuffer: Buffer): string => {
return aesDecrypt(cipherBuffer, 'aes-128-ecb', eapiKey, '').toString()
}
+26
View File
@@ -0,0 +1,26 @@
import { httpFetch } from '../../request.ts'
import { eapi } from './crypto.ts'
import { getWyCookie } from '../../auth/store.ts'
import type { HttpRequestObject } from '../../types.ts'
export const eapiRequest = (url: string, data: object): HttpRequestObject => {
// 登录后携带 MUSIC_U(见 /login/wy),未登录时不附加
const cookie = getWyCookie()
return httpFetch('http://interface.music.163.com/eapi/batch', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
origin: 'https://music.163.com',
...(cookie ? { cookie } : {}),
},
form: eapi(url, data),
})
// requestObj.promise = requestObj.promise.then(({ body }) => {
// // console.log(raw)
// console.log(body)
// // console.log(eapiDecrypt(raw))
// // return eapiDecrypt(raw)
// return body
// })
// return requestObj
}