feat: 优化解析功能,添加缓存

This commit is contained in:
alger
2025-09-14 01:03:29 +08:00
parent 8f0728d9db
commit 659c9f9a4c
4 changed files with 654 additions and 192 deletions

View File

@@ -1,17 +1,10 @@
import { cloneDeep } from 'lodash';
import { musicDB } from '@/hooks/MusicHook';
import { useSettingsStore, useUserStore } from '@/store';
import type { ILyric } from '@/types/lyric';
import type { SongResult } from '@/types/music';
import { isElectron } from '@/utils';
import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
import { searchAndGetBilibiliAudioUrl } from './bilibili';
import type { ParsedMusicResult } from './gdmusic';
import { parseFromGDMusic } from './gdmusic';
import { parseFromCustomApi } from './parseFromCustomApi';
import { MusicParser, type MusicParseResult } from './musicParser';
const { addData, getData, deleteData } = musicDB;
@@ -89,188 +82,17 @@ export const getMusicLrc = async (id: number) => {
}
};
/**
* 从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));
};
/**
* 获取解析后的音乐URL
* @param id 歌曲ID
* @param data 歌曲数据
* @returns 解析结果
*/
export const getParsingMusicUrl = async (id: number, data: SongResult) => {
try {
if (isElectron) {
let musicSources: any[] = [];
let quality: string = 'higher';
try {
const settingStore = useSettingsStore();
const enableMusicUnblock = settingStore?.setData?.enableMusicUnblock;
// 如果禁用了音乐解析功能,则直接返回空结果
if (!enableMusicUnblock) {
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
}
// 1. 确定使用的音源列表(自定义或全局)
const songId = String(id);
const savedSourceStr = (() => {
try {
return localStorage.getItem(`song_source_${songId}`);
} catch (e) {
console.warn('读取本地存储失败,忽略自定义音源', e);
return null;
}
})();
if (savedSourceStr) {
try {
musicSources = JSON.parse(savedSourceStr);
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
} catch (e) {
console.error('解析音源设置失败,回退到默认全局设置', e);
musicSources = settingStore?.setData?.enabledMusicSources || [];
}
} else {
// 使用全局音源设置
musicSources = settingStore?.setData?.enabledMusicSources || [];
console.log(`使用全局音源设置:`, musicSources);
}
quality = settingStore?.setData?.musicQuality || 'higher';
} catch (e) {
console.error('读取设置失败,使用默认配置', e);
musicSources = [];
quality = 'higher';
}
// 优先级 1: 自定义 API
try {
const hasCustom = Array.isArray(musicSources) && musicSources.includes('custom');
const customEnabled = (() => {
try {
const st = useSettingsStore();
return Boolean(st?.setData?.customApiPlugin);
} catch {
return false;
}
})();
if (hasCustom && customEnabled) {
console.log('尝试使用 自定义API 解析...');
const customResult = await parseFromCustomApi(id, data, quality);
if (customResult) {
return customResult; // 成功则直接返回
}
console.log('自定义API解析失败继续尝试其他音源...');
}
} catch (e) {
console.error('自定义API解析发生异常继续尝试其他音源', e);
}
// 优先级 2: Bilibili
try {
if (Array.isArray(musicSources) && musicSources.includes('bilibili')) {
console.log('尝试使用 Bilibili 解析...');
const bilibiliResult = await getBilibiliAudio(data);
if (bilibiliResult?.data?.data?.url) {
return bilibiliResult;
}
console.log('Bilibili解析失败继续尝试其他音源...');
}
} catch (e) {
console.error('Bilibili解析发生异常继续尝试其他音源', e);
}
// 优先级 3: GD 音乐台
try {
if (Array.isArray(musicSources) && musicSources.includes('gdmusic')) {
console.log('尝试使用 GD音乐台 解析...');
const gdResult = await getGDMusicAudio(id, data);
if (gdResult) {
return gdResult;
}
console.log('GD音乐台解析失败继续尝试其他音源...');
}
} catch (e) {
console.error('GD音乐台解析发生异常继续尝试其他音源', e);
}
// 优先级 4: UnblockMusic (migu, kugou, pyncmd)
try {
const unblockSources = (Array.isArray(musicSources) ? musicSources : []).filter(
(source) => !['custom', 'bilibili', 'gdmusic'].includes(source)
);
if (unblockSources.length > 0) {
console.log('尝试使用 UnblockMusic 解析:', unblockSources);
// 捕获内部可能的异常
return await getUnblockMusicAudio(id, data, unblockSources);
} else {
console.warn('UnblockMusic API 不可用,跳过此解析方式');
}
} catch (e) {
console.error('UnblockMusic 解析发生异常,继续后备方案', e);
}
}
} catch (e) {
console.error('getParsingMusicUrl 执行异常,将使用后备方案:', e);
}
// 后备方案使用API请求
console.log('无可用音源或不在Electron环境中使用API请求');
return requestMusic.get<any>('/music', { params: { id } });
export const getParsingMusicUrl = async (
id: number,
data: SongResult
): Promise<MusicParseResult> => {
return await MusicParser.parseMusic(id, data);
};
// 收藏歌曲

View File

@@ -0,0 +1,632 @@
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
};
/**
* 缓存管理器
*/
class CacheManager {
/**
* 获取缓存的音乐URL
*/
static async getCachedMusicUrl(id: number): 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
) {
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): Promise<void> {
try {
await saveData('music_url_cache', {
id,
data: result,
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);
}
}
}
/**
* 重试工具
*/
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 } });
}
// 检查缓存
console.log(`检查歌曲 ${id} 的缓存...`);
const cachedResult = await CacheManager.getCachedMusicUrl(id);
if (cachedResult) {
const endTime = performance.now();
console.log(`✅ 命中缓存,歌曲 ${id},耗时: ${(endTime - startTime).toFixed(2)}ms`);
return cachedResult;
}
console.log(`❌ 未命中缓存,歌曲 ${id},开始解析...`);
// 获取设置存储
let settingsStore: any;
try {
settingsStore = useSettingsStore();
} catch (error) {
console.error('无法获取设置存储,使用后备方案:', error);
return await requestMusic.get<any>('/music', { params: { id } });
}
// 检查音乐解析功能是否启用
if (!settingsStore?.setData?.enableMusicUnblock) {
console.log('音乐解析功能已禁用');
return {
data: {
code: 404,
message: '音乐解析功能已禁用',
data: undefined
}
};
}
// 获取音源配置
const { musicSources, quality } = getMusicConfig(id, settingsStore);
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);
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
}
};
}
}
}

View File

@@ -55,11 +55,17 @@ export const textColors = ref<any>(getTextColors());
export let playMusic: ComputedRef<SongResult>;
export let artistList: ComputedRef<Artist[]>;
export const musicDB = await useIndexedDB('musicDB', [
{ name: 'music', keyPath: 'id' },
{ name: 'music_lyric', keyPath: 'id' },
{ name: 'api_cache', keyPath: 'id' }
]);
export const musicDB = await useIndexedDB(
'musicDB',
[
{ name: 'music', keyPath: 'id' },
{ name: 'music_lyric', keyPath: 'id' },
{ name: 'api_cache', keyPath: 'id' },
{ name: 'music_url_cache', keyPath: 'id' },
{ name: 'music_failed_cache', keyPath: 'id' }
],
2
);
// 键盘事件处理器,在初始化后设置
const setupKeyboardListeners = () => {

View File

@@ -19,13 +19,15 @@ export const getSongUrl = async (id: any, songData: any, isDownloaded: boolean =
const { data } = await getMusicUrl(id, !unlimitedDownload);
let url = '';
let songDetail = null;
let songDetail: any = null;
try {
if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id, cloneDeep(songData));
url = res.data.data.url;
songDetail = res.data.data;
if (res.data.data?.url) {
url = res.data.data.url;
songDetail = res.data.data;
}
} else {
songDetail = data.data[0] as any;
}