Files
AlgerMusicPlayer/src/renderer/api/musicParser.ts
2025-10-11 20:23:54 +08:00

678 lines
18 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 { useSettingsStore } from '@/store';
import type { SongResult } from '@/types/music';
import { isElectron } from '@/utils';
import requestMusic from '@/utils/request_music';
import { searchAndGetBilibiliAudioUrl } from './bilibili';
import type { ParsedMusicResult } from './gdmusic';
import { parseFromGDMusic } from './gdmusic';
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,
// 失败缓存时间5分钟
FAILED_CACHE_TIME: 5 * 60 * 1000,
// 重试配置
MAX_RETRY_COUNT: 2,
RETRY_DELAY: 1000
};
/**
* 缓存管理器
*/
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 async isInFailedCache(id: number, strategyName: string): Promise<boolean> {
try {
const cacheKey = `${id}_${strategyName}`;
const cached = await getData('music_failed_cache', cacheKey);
if (cached?.createTime && Date.now() - cached.createTime < CACHE_CONFIG.FAILED_CACHE_TIME) {
console.log(`策略 ${strategyName} 在失败缓存期内,跳过`);
return true;
}
// 清理过期缓存
if (cached) {
await deleteData('music_failed_cache', cacheKey);
}
} catch (error) {
console.warn('检查失败缓存失败:', error);
}
return false;
}
/**
* 添加失败缓存
*/
static async addFailedCache(id: number, strategyName: string): Promise<void> {
try {
const cacheKey = `${id}_${strategyName}`;
await saveData('music_failed_cache', {
id: cacheKey,
createTime: Date.now()
});
console.log(`添加失败缓存成功: ${strategyName}`);
} catch (error) {
console.error('添加失败缓存失败:', error);
}
}
/**
* 清除指定歌曲的所有缓存
*/
static async clearMusicCache(id: number): Promise<void> {
try {
// 清除URL缓存
await deleteData('music_url_cache', id);
console.log(`清除歌曲 ${id} 的URL缓存`);
// 清除失败缓存 - 需要遍历所有策略
const strategies = ['custom', 'bilibili', 'gdmusic', 'unblockMusic'];
for (const strategy of strategies) {
const cacheKey = `${id}_${strategy}`;
try {
await deleteData('music_failed_cache', cacheKey);
} catch {
// 忽略删除不存在缓存的错误
}
}
console.log(`清除歌曲 ${id} 的失败缓存`);
} catch (error) {
console.error('清除缓存失败:', error);
}
}
}
/**
* 重试工具
*/
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!;
}
}
/**
* 从Bilibili获取音频URL
* @param data 歌曲数据
* @returns 解析结果
*/
const getBilibiliAudio = async (data: SongResult) => {
const songName = data?.name || '';
const artistName =
Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : '';
const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : '';
const searchQuery = [songName, artistName, albumName].filter(Boolean).join(' ').trim();
console.log('开始搜索bilibili音频:', searchQuery);
const url = await searchAndGetBilibiliAudioUrl(searchQuery);
return {
data: {
code: 200,
message: 'success',
data: { url }
}
};
};
/**
* 从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);
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));
};
/**
* 统一的解析结果适配器
*/
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 (await 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;
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('自定义API解析失败:', error);
await CacheManager.addFailedCache(id, this.name);
return null;
}
}
}
/**
* Bilibili解析策略
*/
class BilibiliStrategy implements MusicSourceStrategy {
name = 'bilibili';
priority = 2;
canHandle(sources: string[]): boolean {
return sources.includes('bilibili');
}
async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {
// 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) {
return null;
}
try {
console.log('尝试使用Bilibili解析...');
const result = await RetryHelper.withRetry(async () => {
return await getBilibiliAudio(data);
});
const adaptedResult = adaptParseResult(result);
if (adaptedResult?.data?.data?.url) {
console.log('Bilibili解析成功');
return adaptedResult;
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('Bilibili解析失败:', error);
await 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 (await 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;
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('GD音乐台解析失败:', error);
await 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', 'bilibili', 'gdmusic'].includes(source)
);
return unblockSources.length > 0;
}
async parse(
id: number,
data: SongResult,
_quality?: string,
sources?: string[]
): Promise<MusicParseResult | null> {
// 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) {
return null;
}
try {
const unblockSources = (sources || []).filter(
(source) => !['custom', 'bilibili', '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;
}
// 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name);
return null;
} catch (error) {
console.error('UnblockMusic解析失败:', error);
await CacheManager.addFailedCache(id, this.name);
return null;
}
}
}
/**
* 音源策略工厂
*/
class MusicSourceStrategyFactory {
private static strategies: MusicSourceStrategy[] = [
new CustomApiStrategy(),
new BilibiliStrategy(),
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) => {
const songId = String(id);
let musicSources: string[] = [];
let quality = 'higher';
try {
// 尝试获取歌曲自定义音源
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
if (savedSourceStr) {
try {
const customSources = JSON.parse(savedSourceStr);
if (Array.isArray(customSources)) {
musicSources = customSources;
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
}
} catch (error) {
console.error('解析自定义音源设置失败:', error);
}
}
// 如果没有自定义音源,使用全局设置
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 };
};
/**
* 音乐解析器主类
*/
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环境直接使用API请求
if (!isElectron) {
console.log('非Electron环境使用API请求');
return await requestMusic.get<any>('/music', { params: { id } });
}
// 获取设置存储
let settingsStore: any;
try {
settingsStore = useSettingsStore();
} catch (error) {
console.error('无法获取设置存储,使用后备方案:', error);
return await requestMusic.get<any>('/music', { params: { id } });
}
// 获取音源配置
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 await requestMusic.get<any>('/music', { params: { id } });
}
// 获取可用的解析策略
const availableStrategies = MusicSourceStrategyFactory.getAvailableStrategies(
musicSources,
settingsStore
);
if (availableStrategies.length === 0) {
console.warn('没有可用的解析策略,使用后备方案');
return await requestMusic.get<any>('/music', { params: { id } });
}
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);
}
// 后备方案使用API请求
try {
console.log('使用后备方案API请求');
const result = await requestMusic.get<any>('/music', { params: { id } });
// 如果后备方案成功,也进行缓存
if (result?.data?.data?.url) {
console.log('后备方案成功,缓存结果');
await CacheManager.setCachedMusicUrl(id, result, []);
}
return result;
} catch (apiError) {
console.error('API请求也失败了:', apiError);
const endTime = performance.now();
console.log(`总耗时: ${(endTime - startTime).toFixed(2)}ms`);
return {
data: {
code: 500,
message: '所有解析方式都失败了',
data: undefined
}
};
}
}
}