Files
AlgerMusicPlayer/src/renderer/api/musicParser.ts
T
alger 9153d85992 fix(api): 修复失败缓存清不掉、自定义音源解析及请求逻辑问题
- musicParser: clearMusicCache 改清内存失败缓存(覆盖含 lxMusic 全部策略);unblock 解析加 15s 整体超时兜底,超时降级不触发重复重试
- parseFromCustomApi: GET 请求按 apiUrl 是否已带查询串选择 ?/& 分隔符;br 由字符串码率映射,修复恒为 NaN
- request: 301 登录态失效直接拒绝(原 retryCount=3 与日志矛盾),并对 config.params 加可选链防护
2026-07-05 19:53:29 +08:00

609 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cloneDeep } from 'lodash';
import { musicDB } from '@/hooks/MusicHook';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import { useSettingsStore } from '@/store';
import type { SongResult } from '@/types/music';
import { isElectron } from '@/utils';
import type { ParsedMusicResult } from './gdmusic';
import { parseFromGDMusic } from './gdmusic';
import { LxMusicStrategy } from './lxMusicStrategy';
import { parseFromCustomApi } from './parseFromCustomApi';
const { saveData, getData, deleteData } = musicDB;
/**
* 音乐解析结果接口
*/
export interface MusicParseResult {
data: {
code: number;
message: string;
data?: {
url: string;
[key: string]: any;
};
};
}
/**
* 缓存配置
*/
const CACHE_CONFIG = {
// 音乐URL缓存时间:30分钟
MUSIC_URL_CACHE_TIME: 30 * 60 * 1000,
// 失败缓存时间:1分钟(减少到 1 分钟以便更快恢复)
FAILED_CACHE_TIME: 1 * 60 * 1000,
// 重试配置
MAX_RETRY_COUNT: 2,
RETRY_DELAY: 1000
};
/**
* 内存失败缓存(替代 IndexedDB,更轻量且应用重启后自动失效)
*/
const failedCacheMap = new Map<string, number>();
/**
* 缓存管理器
*/
export class CacheManager {
/**
* 获取缓存的音乐URL
*/
static async getCachedMusicUrl(
id: number,
musicSources?: string[]
): Promise<MusicParseResult | null> {
try {
const cached = await getData('music_url_cache', id);
if (
cached?.createTime &&
Date.now() - cached.createTime < CACHE_CONFIG.MUSIC_URL_CACHE_TIME
) {
// 检查缓存的音源配置是否与当前配置一致
const cachedSources = cached.musicSources || [];
const currentSources = musicSources || [];
// 如果音源配置不一致,清除缓存
if (JSON.stringify(cachedSources.sort()) !== JSON.stringify(currentSources.sort())) {
console.log(`音源配置已变更,清除歌曲 ${id} 的缓存`);
await deleteData('music_url_cache', id);
return null;
}
console.log(`使用缓存的音乐URL: ${id}`);
return cached.data;
}
// 清理过期缓存
if (cached) {
await deleteData('music_url_cache', id);
}
} catch (error) {
console.warn('获取缓存失败:', error);
}
return null;
}
/**
* 缓存音乐URL
*/
static async setCachedMusicUrl(
id: number,
result: MusicParseResult,
musicSources?: string[]
): Promise<void> {
try {
// 深度克隆数据,确保可以被 IndexedDB 存储
await saveData('music_url_cache', {
id,
data: cloneDeep(result),
musicSources: cloneDeep(musicSources || []),
createTime: Date.now()
});
console.log(`缓存音乐URL成功: ${id}`);
} catch (error) {
console.error('缓存音乐URL失败:', error);
}
}
/**
* 检查是否在失败缓存期内(使用内存缓存)
*/
static isInFailedCache(id: number, strategyName: string): boolean {
const cacheKey = `${id}_${strategyName}`;
const cachedTime = failedCacheMap.get(cacheKey);
if (cachedTime && Date.now() - cachedTime < CACHE_CONFIG.FAILED_CACHE_TIME) {
console.log(`策略 ${strategyName} 在失败缓存期内,跳过`);
return true;
}
// 清理过期缓存
if (cachedTime) {
failedCacheMap.delete(cacheKey);
}
return false;
}
/**
* 添加失败缓存(使用内存缓存)
*/
static addFailedCache(id: number, strategyName: string): void {
const cacheKey = `${id}_${strategyName}`;
failedCacheMap.set(cacheKey, Date.now());
console.log(
`添加失败缓存成功: ${strategyName} (缓存时间: ${CACHE_CONFIG.FAILED_CACHE_TIME / 1000}秒)`
);
}
/**
* 清除指定歌曲的失败缓存
*/
static clearFailedCache(id: number): void {
const keysToDelete: string[] = [];
failedCacheMap.forEach((_, key) => {
if (key.startsWith(`${id}_`)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach((key) => failedCacheMap.delete(key));
if (keysToDelete.length > 0) {
console.log(`清除歌曲 ${id} 的失败缓存: ${keysToDelete.length} 项`);
}
}
/**
* 清除指定歌曲的所有缓存
*/
static async clearMusicCache(id: number): Promise<void> {
try {
// 清除URL缓存
await deleteData('music_url_cache', id);
console.log(`清除歌曲 ${id} 的URL缓存`);
} catch (error) {
console.error('清除URL缓存失败:', error);
}
// 清除内存失败缓存(覆盖所有策略,含 lxMusic)
CacheManager.clearFailedCache(id);
}
}
/**
* 重试工具
*/
class RetryHelper {
/**
* 带重试的异步执行
*/
static async withRetry<T>(
fn: () => Promise<T>,
maxRetries = CACHE_CONFIG.MAX_RETRY_COUNT,
delay = CACHE_CONFIG.RETRY_DELAY
): Promise<T> {
let lastError: Error;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (i < maxRetries) {
console.log(`重试第 ${i + 1} 次,延迟 ${delay}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= 2; // 指数退避
}
}
}
throw lastError!;
}
}
/**
* 从GD音乐台获取音频URL
* @param id 歌曲ID
* @param data 歌曲数据
* @returns 解析结果,失败时返回null
*/
const getGDMusicAudio = async (id: number, data: SongResult): Promise<ParsedMusicResult | null> => {
try {
const gdResult = await parseFromGDMusic(id, data, '999');
if (gdResult) {
return gdResult;
}
} catch (error) {
console.error('GD音乐台解析失败:', error);
}
return null;
};
/**
* 使用unblockMusic解析音频URL
* @param id 歌曲ID
* @param data 歌曲数据
* @param sources 音源列表
* @returns 解析结果
*/
const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => {
const filteredSources = sources.filter((source) => source !== 'gdmusic');
console.log(`使用unblockMusic解析,音源:`, filteredSources);
// 整体超时兜底:unblock 全链路(IPC → match())本身无超时,底层请求挂起时会
// 导致渲染进程无限等待、起播/切歌长时间无响应。超时解析为 null 以走降级,
// 且不抛异常(避免触发 RetryHelper 的多次重试放大等待时间)。
const UNBLOCK_TIMEOUT = 15000;
return Promise.race([
window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources)),
new Promise((resolve) => {
setTimeout(() => {
console.warn(`unblockMusic 解析超时(${UNBLOCK_TIMEOUT}ms),放弃等待`);
resolve(null);
}, UNBLOCK_TIMEOUT);
})
]);
};
/**
* 统一的解析结果适配器
*/
const adaptParseResult = (result: any): MusicParseResult | null => {
if (!result) return null;
// 如果已经是标准格式
if (result.data?.code !== undefined && result.data?.message !== undefined) {
return result;
}
// 适配GD音乐台的返回格式
if (result.data?.data?.url) {
return {
data: {
code: 200,
message: 'success',
data: {
url: result.data.data.url,
...result.data.data
}
}
};
}
// 适配其他格式
if (result.url) {
return {
data: {
code: 200,
message: 'success',
data: {
url: result.url,
...result
}
}
};
}
return null;
};
/**
* 音源解析策略接口
*/
interface MusicSourceStrategy {
name: string;
priority: number;
canHandle: (sources: string[], settingsStore?: any) => boolean;
parse: (
id: number,
data: SongResult,
quality?: string,
sources?: string[]
) => Promise<MusicParseResult | null>;
}
/**
* 自定义API解析策略
*/
class CustomApiStrategy implements MusicSourceStrategy {
name = 'custom';
priority = 1;
canHandle(sources: string[], settingsStore?: any): boolean {
return sources.includes('custom') && Boolean(settingsStore?.setData?.customApiPlugin);
}
async parse(id: number, data: SongResult, quality = 'higher'): Promise<MusicParseResult | null> {
// 检查失败缓存
if (CacheManager.isInFailedCache(id, this.name)) {
return null;
}
try {
console.log('尝试使用自定义API解析...');
const result = await RetryHelper.withRetry(async () => {
return await parseFromCustomApi(id, data, quality);
});
const adaptedResult = adaptParseResult(result);
if (adaptedResult?.data?.data?.url) {
console.log('自定义API解析成功');
return adaptedResult;
}
// 解析失败,添加失败缓存
CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('自定义API解析失败:', error);
CacheManager.addFailedCache(id, this.name);
return null;
}
}
}
/**
* GD音乐台解析策略
*/
class GDMusicStrategy implements MusicSourceStrategy {
name = 'gdmusic';
priority = 3;
canHandle(sources: string[]): boolean {
return sources.includes('gdmusic');
}
async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {
// 检查失败缓存
if (CacheManager.isInFailedCache(id, this.name)) {
return null;
}
try {
console.log('尝试使用GD音乐台解析...');
const result = await RetryHelper.withRetry(async () => {
return await getGDMusicAudio(id, data);
});
const adaptedResult = adaptParseResult(result);
if (adaptedResult?.data?.data?.url) {
console.log('GD音乐台解析成功');
return adaptedResult;
}
// 解析失败,添加失败缓存
CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('GD音乐台解析失败:', error);
CacheManager.addFailedCache(id, this.name);
return null;
}
}
}
/**
* UnblockMusic解析策略
*/
class UnblockMusicStrategy implements MusicSourceStrategy {
name = 'unblockMusic';
priority = 4;
canHandle(sources: string[]): boolean {
const unblockSources = sources.filter((source) => !['custom', 'gdmusic'].includes(source));
return unblockSources.length > 0;
}
async parse(
id: number,
data: SongResult,
_quality?: string,
sources?: string[]
): Promise<MusicParseResult | null> {
// 检查失败缓存
if (CacheManager.isInFailedCache(id, this.name)) {
return null;
}
try {
const unblockSources = (sources || []).filter(
(source) => !['custom', 'gdmusic'].includes(source)
);
console.log('尝试使用UnblockMusic解析:', unblockSources);
const result = await RetryHelper.withRetry(async () => {
return await getUnblockMusicAudio(id, data, unblockSources);
});
const adaptedResult = adaptParseResult(result);
if (adaptedResult?.data?.data?.url) {
console.log('UnblockMusic解析成功');
return adaptedResult;
}
// 解析失败,添加失败缓存
CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('UnblockMusic解析失败:', error);
CacheManager.addFailedCache(id, this.name);
return null;
}
}
}
/**
* 音源策略工厂
*/
class MusicSourceStrategyFactory {
private static strategies: MusicSourceStrategy[] = [
new LxMusicStrategy(),
new CustomApiStrategy(),
new GDMusicStrategy(),
new UnblockMusicStrategy()
];
/**
* 获取可用的解析策略
* @param sources 音源列表
* @param settingsStore 设置存储
* @returns 排序后的可用策略列表
*/
static getAvailableStrategies(sources: string[], settingsStore?: any): MusicSourceStrategy[] {
return this.strategies
.filter((strategy) => strategy.canHandle(sources, settingsStore))
.sort((a, b) => a.priority - b.priority);
}
}
/**
* 获取音源配置
* @param id 歌曲ID
* @param settingsStore 设置存储
* @returns 音源列表和音质设置
*/
const getMusicConfig = (id: number, settingsStore?: any) => {
let musicSources: string[] = [];
let quality = 'higher';
try {
// 尝试获取歌曲自定义音源(使用 SongSourceConfigManager
const songConfig = SongSourceConfigManager.getConfig(id);
if (songConfig && songConfig.sources.length > 0) {
musicSources = songConfig.sources;
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
}
// 如果没有自定义音源,使用全局设置
if (musicSources.length === 0) {
musicSources = settingsStore?.setData?.enabledMusicSources || [];
console.log('使用全局音源设置:', musicSources);
}
quality = settingsStore?.setData?.musicQuality || 'higher';
} catch (error) {
console.error('读取音源配置失败,使用默认配置:', error);
musicSources = [];
quality = 'higher';
}
return { musicSources, quality };
};
/**
* 构造解析失败结果
* (原远端后备接口 music_proxy 已停止服务,解析失败时不再回退到远端请求)
*/
const buildFailedResult = (message: string, code = 404): MusicParseResult => ({
data: {
code,
message,
data: undefined
}
});
/**
* 音乐解析器主类
*/
export class MusicParser {
/**
* 解析音乐URL
* @param id 歌曲ID
* @param data 歌曲数据
* @returns 解析结果
*/
static async parseMusic(id: number, data: SongResult): Promise<MusicParseResult> {
const startTime = performance.now();
try {
// 非Electron环境不支持本地解析
if (!isElectron) {
console.log('非Electron环境,不支持音乐解析');
return buildFailedResult('当前环境不支持音乐解析');
}
// 获取设置存储
let settingsStore: any;
try {
settingsStore = useSettingsStore();
} catch (error) {
console.error('无法获取设置存储:', error);
return buildFailedResult('无法读取设置,音乐解析不可用');
}
// 获取音源配置
const { musicSources, quality } = getMusicConfig(id, settingsStore);
// 检查缓存(传入音源配置用于验证缓存有效性)
console.log(`检查歌曲 ${id} 的缓存...`);
const cachedResult = await CacheManager.getCachedMusicUrl(id, musicSources);
if (cachedResult) {
const endTime = performance.now();
console.log(`✅ 命中缓存,歌曲 ${id},耗时: ${(endTime - startTime).toFixed(2)}ms`);
return cachedResult;
}
console.log(`❌ 未命中缓存,歌曲 ${id},开始解析...`);
// 检查音乐解析功能是否启用
if (!settingsStore?.setData?.enableMusicUnblock) {
console.log('音乐解析功能已禁用');
return {
data: {
code: 404,
message: '音乐解析功能已禁用',
data: undefined
}
};
}
if (musicSources.length === 0) {
console.warn('没有配置可用的音源');
return buildFailedResult('没有配置可用的音源');
}
// 获取可用的解析策略
const availableStrategies = MusicSourceStrategyFactory.getAvailableStrategies(
musicSources,
settingsStore
);
if (availableStrategies.length === 0) {
console.warn('没有可用的解析策略');
return buildFailedResult('没有可用的解析策略');
}
console.log(
`开始解析歌曲 ${id},可用策略:`,
availableStrategies.map((s) => s.name)
);
// 按优先级依次尝试解析策略
for (const strategy of availableStrategies) {
try {
const result = await strategy.parse(id, data, quality, musicSources);
if (result?.data?.data?.url) {
const endTime = performance.now();
console.log(
`解析成功,使用策略: ${strategy.name},耗时: ${(endTime - startTime).toFixed(2)}ms`
);
// 缓存成功结果(包含音源配置)
await CacheManager.setCachedMusicUrl(id, result, musicSources);
return result;
}
console.log(`策略 ${strategy.name} 解析失败,继续尝试下一个策略`);
} catch (error) {
console.error(`策略 ${strategy.name} 解析异常:`, error);
// 继续尝试下一个策略
}
}
console.warn('所有解析策略都失败了');
} catch (error) {
console.error('MusicParser.parseMusic 执行异常:', error);
}
const endTime = performance.now();
console.log(`总耗时: ${(endTime - startTime).toFixed(2)}ms`);
return buildFailedResult('所有解析方式都失败了', 500);
}
}