feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+213
View File
@@ -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
}
}