feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// ChKSz 音源(api.chksz.com):用户自配 apikey,前端直连(接口返回 Access-Control-Allow-Origin: *)
|
||||
// 仅支持 netease / tencent 平台,接口免费配额有限(约 400 次 + 每分钟 20 次),需缓存与限流
|
||||
const CHKSZ_BASE_URL = 'https://api.chksz.com'
|
||||
const STORAGE_KEY = 'chksz_api_key'
|
||||
|
||||
// 播放链接缓存(5 分钟,chksz 返回的直链有时效)
|
||||
const CHKSZ_CACHE_TTL = 5 * 60 * 1000
|
||||
// 滑动窗口限流:60 秒内最多 18 次,留余量规避接口 20 次/分钟限制
|
||||
const CHKSZ_RATE_LIMIT_MAX = 18
|
||||
const CHKSZ_RATE_LIMIT_WINDOW = 60 * 1000
|
||||
|
||||
// 项目音质值 → chksz 接口参数
|
||||
// 网易 level:standard/exhigh/lossless/hires/jymaster
|
||||
const NETEASE_LEVEL_MAP: Record<number, string> = {
|
||||
2: 'standard',
|
||||
4: 'exhigh',
|
||||
5: 'lossless',
|
||||
6: 'hires',
|
||||
9: 'jymaster'
|
||||
}
|
||||
// QQ size:128k/320k/flac/hires/master
|
||||
const TENCENT_SIZE_MAP: Record<number, string> = {
|
||||
4: '128k',
|
||||
8: '320k',
|
||||
10: 'flac',
|
||||
11: 'hires',
|
||||
14: 'master'
|
||||
}
|
||||
|
||||
const chkszUrlCache = new Map<string, { url: string; expireAt: number }>()
|
||||
// 同 key 请求合并,避免并发重复请求
|
||||
const chkszInflight = new Map<string, Promise<string | null>>()
|
||||
// 最近请求时间戳(滑动窗口限流)
|
||||
const chkszRequestTimes: number[] = []
|
||||
|
||||
const readStoredKey = (): string => {
|
||||
if (typeof window === 'undefined') return ''
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
let globalChkszKey: any = null
|
||||
|
||||
export function useChkszSource() {
|
||||
if (!globalChkszKey) {
|
||||
globalChkszKey = ref(readStoredKey())
|
||||
|
||||
if (import.meta.client) {
|
||||
watch(
|
||||
globalChkszKey,
|
||||
(newValue) => {
|
||||
try {
|
||||
if (newValue && newValue.trim()) {
|
||||
localStorage.setItem(STORAGE_KEY, newValue.trim())
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[chksz] Failed to save apikey:', error)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const chkszApiKey = globalChkszKey
|
||||
|
||||
const hasChkszKey = () => {
|
||||
const key = chkszApiKey.value
|
||||
return !!(key && key.trim())
|
||||
}
|
||||
|
||||
const clearChkszKey = () => {
|
||||
chkszApiKey.value = ''
|
||||
}
|
||||
|
||||
const checkRateLimit = (): boolean => {
|
||||
const now = Date.now()
|
||||
const windowStart = now - CHKSZ_RATE_LIMIT_WINDOW
|
||||
while (chkszRequestTimes.length > 0 && chkszRequestTimes[0] < windowStart) {
|
||||
chkszRequestTimes.shift()
|
||||
}
|
||||
if (chkszRequestTimes.length >= CHKSZ_RATE_LIMIT_MAX) {
|
||||
return false
|
||||
}
|
||||
chkszRequestTimes.push(now)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 ChKSz 获取播放链接
|
||||
* @returns 播放 URL;未配 key / 不支持的平台音质 / 接口失败均返回 null(走降级链)
|
||||
*/
|
||||
const getChkszMusicUrl = async (
|
||||
platform: string,
|
||||
musicId: string | number,
|
||||
quality: number | string
|
||||
): Promise<string | null> => {
|
||||
if (!hasChkszKey()) return null
|
||||
|
||||
const apiKey = chkszApiKey.value.trim()
|
||||
const normalizedQuality = Number(quality)
|
||||
let endpoint = ''
|
||||
let qualityParam = ''
|
||||
|
||||
if (platform === 'netease') {
|
||||
const level = NETEASE_LEVEL_MAP[normalizedQuality]
|
||||
if (!level) return null
|
||||
endpoint = '163_music'
|
||||
qualityParam = `level=${level}`
|
||||
} else if (platform === 'tencent') {
|
||||
const size = TENCENT_SIZE_MAP[normalizedQuality]
|
||||
if (!size) return null
|
||||
endpoint = 'qq_music'
|
||||
qualityParam = `size=${size}&type=json`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
const cacheKey = `${platform}:${musicId}:${qualityParam}`
|
||||
const cached = chkszUrlCache.get(cacheKey)
|
||||
if (cached && cached.expireAt > Date.now()) {
|
||||
return cached.url
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
for (const [key, entry] of chkszUrlCache) {
|
||||
if (entry.expireAt <= now) {
|
||||
chkszUrlCache.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
const inflight = chkszInflight.get(cacheKey)
|
||||
if (inflight) {
|
||||
return inflight
|
||||
}
|
||||
|
||||
if (!checkRateLimit()) {
|
||||
console.warn('[chksz] 请求过于频繁,跳过本次请求')
|
||||
return null
|
||||
}
|
||||
|
||||
const promise = (async (): Promise<string | null> => {
|
||||
try {
|
||||
const apiUrl =
|
||||
`${CHKSZ_BASE_URL}/api/${endpoint}?` +
|
||||
`${platform === 'netease' ? 'id' : 'mid'}=${encodeURIComponent(String(musicId))}&` +
|
||||
`${qualityParam}&apikey=${encodeURIComponent(apiKey)}`
|
||||
const response = await fetch(apiUrl, { signal: AbortSignal.timeout(8000) })
|
||||
if (!response.ok) return null
|
||||
const data: any = await response.json()
|
||||
const url = platform === 'netease' ? data?.data?.url : data?.url
|
||||
if (data?.code === 200 && url && /^https?:\/\//i.test(String(url))) {
|
||||
const finalUrl = String(url)
|
||||
chkszUrlCache.set(cacheKey, { url: finalUrl, expireAt: Date.now() + CHKSZ_CACHE_TTL })
|
||||
return finalUrl
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[chksz] 获取播放链接失败:', error)
|
||||
}
|
||||
return null
|
||||
})()
|
||||
|
||||
chkszInflight.set(cacheKey, promise)
|
||||
promise.finally(() => {
|
||||
chkszInflight.delete(cacheKey)
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 apikey 有效性(消耗 1 次接口配额)
|
||||
*/
|
||||
const verifyChkszKey = async (
|
||||
key?: string
|
||||
): Promise<{ valid: boolean; message: string; detail?: string }> => {
|
||||
const apiKey = (key !== undefined ? key : chkszApiKey.value).trim()
|
||||
if (!apiKey) {
|
||||
return { valid: false, message: 'empty' }
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${CHKSZ_BASE_URL}/api/163_music?id=347230&level=standard&apikey=${encodeURIComponent(apiKey)}`,
|
||||
{ signal: AbortSignal.timeout(8000) }
|
||||
)
|
||||
if (!response.ok) {
|
||||
return { valid: false, message: `http_${response.status}` }
|
||||
}
|
||||
const data: any = await response.json()
|
||||
if (data?.code === 200 && data?.data?.url) {
|
||||
return { valid: true, message: 'ok' }
|
||||
}
|
||||
// key 无效(401/403 语义)或配额耗尽时接口返回业务错误码
|
||||
return { valid: false, message: 'api_error', detail: data?.msg }
|
||||
} catch (error: any) {
|
||||
return { valid: false, message: error?.name === 'TimeoutError' ? 'timeout' : 'network' }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chkszApiKey,
|
||||
hasChkszKey,
|
||||
clearChkszKey,
|
||||
getChkszMusicUrl,
|
||||
verifyChkszKey
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user