mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-15 07:20:49 +08:00
feat: 优化解析功能,添加缓存
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
// 收藏歌曲
|
||||
|
||||
632
src/renderer/api/musicParser.ts
Normal file
632
src/renderer/api/musicParser.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user