来一个lx音乐的TuneHUB音源
后端开发
/**
* @name TuneHub
* @description 基于 TuneHub V3 API 的 LX 自定义音源
* @version 1.0.0
* @author leee
* @homepage https://tunehub.sayqz.com
*/
const { EVENT_NAMES, request, on, send } = globalThis.lx
const CONFIG = {
API_BASE_URL: 'https://tunehub.sayqz.com/api',
API_KEY: '', // 在这里填写你的 TuneHub API Key
}
const PLATFORM_BY_SOURCE = {
wy: 'netease',
tx: 'qq',
kw: 'kuwo',
}
const SUPPORTED_QUALITIES = ['128k', '320k', 'flac', 'flac24bit']
const httpRequest = (url, options) =>
new Promise((resolve, reject) => {
request(url, options, (err, resp, body) => {
if (err) return reject(err)
const normalizedBody =
typeof body !== 'undefined'
? body
: resp && typeof resp.body !== 'undefined'
? resp.body
: resp
resolve({
resp,
body: normalizedBody,
})
})
})
const toJSON = body => {
if (body && typeof body === 'object') return body
if (typeof body !== 'string') {
if (body === null || typeof body === 'undefined') {
throw new Error('响应体为空')
}
body = String(body)
}
try {
return JSON.parse(body)
} catch (err) {
throw new Error(`JSON 解析失败: ${err.message}`)
}
}
const normalizeQuality = quality =>
SUPPORTED_QUALITIES.includes(quality) ? quality : '320k'
const pickSongId = musicInfo => {
if (!musicInfo || typeof musicInfo !== 'object') {
throw new Error('musicInfo 缺失或格式错误')
}
const candidates = [
musicInfo.songmid,
musicInfo.mid,
musicInfo.id,
musicInfo.musicId,
musicInfo.songId,
musicInfo.rid,
musicInfo.hash,
]
const id = candidates.find(v => v !== undefined && v !== null && v !== '')
if (!id) throw new Error('无法从 musicInfo 提取歌曲 ID')
return String(id)
}
const pickPlayableUrl = data => {
const queue = [data]
const visited = new Set()
const maxNodes = 80
let scanned = 0
while (queue.length && scanned < maxNodes) {
const current = queue.shift()
scanned += 1
if (!current || typeof current !== 'object') continue
if (visited.has(current)) continue
visited.add(current)
const directUrl =
current.url ||
current.playUrl ||
current.play_url ||
current.musicUrl ||
current.src
if (typeof directUrl === 'string' && /^https?:\/\//i.test(directUrl)) {
return directUrl
}
if (Array.isArray(current)) {
for (const item of current) queue.push(item)
continue
}
for (const val of Object.values(current)) {
if (val && typeof val === 'object') queue.push(val)
}
}
return null
}
const parseMusicUrl = async ({ source, musicInfo, quality }) => {
const apiKey = (CONFIG.API_KEY || '').trim()
if (!apiKey) throw new Error('请先在脚本顶部 CONFIG.API_KEY 中填写令牌')
const platform = PLATFORM_BY_SOURCE[source]
if (!platform) throw new Error(`不支持的 source: ${source}`)
const body = JSON.stringify({
platform,
ids: pickSongId(musicInfo),
quality: normalizeQuality(quality),
})
const { resp, body: rawBody } = await httpRequest(`${CONFIG.API_BASE_URL}/v1/parse`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
},
body,
timeout: 15000,
})
const statusCode = Number(resp && resp.statusCode)
if (statusCode && (statusCode < 200 || statusCode >= 300)) {
throw new Error(`HTTP ${statusCode}: ${String(rawBody).slice(0, 200)}`)
}
const json = toJSON(rawBody)
if (Number(json.code) !== 0) {
throw new Error(
`接口错误: code=${json.code}, message=${json.message || json.msg || 'unknown'}`
)
}
const url = pickPlayableUrl(json.data)
if (!url || typeof url !== 'string') throw new Error('未获取到可播放链接')
return url
}
on(EVENT_NAMES.request, ({ source, action, info }) => {
if (action !== 'musicUrl') {
return Promise.reject(new Error(`不支持的 action: ${action}`))
}
return parseMusicUrl({
source,
musicInfo: info && info.musicInfo,
quality: info && info.type,
}).catch(err => Promise.reject(new Error(`[TuneHub:${source}] ${err.message}`)))
})
send(EVENT_NAMES.inited, {
openDevTools: false,
sources: {
wy: {
name: '网易云音乐(TuneHub)',
type: 'music',
actions: ['musicUrl'],
qualitys: SUPPORTED_QUALITIES,
},
tx: {
name: 'QQ音乐(TuneHub)',
type: 'music',
actions: ['musicUrl'],
qualitys: SUPPORTED_QUALITIES,
},
kw: {
name: '酷我音乐(TuneHub)',
type: 'music',
actions: ['musicUrl'],
qualitys: SUPPORTED_QUALITIES,
},
},
})
![[衡天云]爆款云服务器 低至12元/月](/hty.png)