mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-28 10:57:23 +08:00
feat: 优化解析功能,添加缓存
This commit is contained in:
+6
-184
@@ -1,17 +1,10 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
|
||||||
|
|
||||||
import { musicDB } from '@/hooks/MusicHook';
|
import { musicDB } from '@/hooks/MusicHook';
|
||||||
import { useSettingsStore, useUserStore } from '@/store';
|
import { useSettingsStore, useUserStore } from '@/store';
|
||||||
import type { ILyric } from '@/types/lyric';
|
import type { ILyric } from '@/types/lyric';
|
||||||
import type { SongResult } from '@/types/music';
|
import type { SongResult } from '@/types/music';
|
||||||
import { isElectron } from '@/utils';
|
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import requestMusic from '@/utils/request_music';
|
|
||||||
|
|
||||||
import { searchAndGetBilibiliAudioUrl } from './bilibili';
|
import { MusicParser, type MusicParseResult } from './musicParser';
|
||||||
import type { ParsedMusicResult } from './gdmusic';
|
|
||||||
import { parseFromGDMusic } from './gdmusic';
|
|
||||||
import { parseFromCustomApi } from './parseFromCustomApi';
|
|
||||||
|
|
||||||
const { addData, getData, deleteData } = musicDB;
|
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
|
* 获取解析后的音乐URL
|
||||||
* @param id 歌曲ID
|
* @param id 歌曲ID
|
||||||
* @param data 歌曲数据
|
* @param data 歌曲数据
|
||||||
* @returns 解析结果
|
* @returns 解析结果
|
||||||
*/
|
*/
|
||||||
export const getParsingMusicUrl = async (id: number, data: SongResult) => {
|
export const getParsingMusicUrl = async (
|
||||||
try {
|
id: number,
|
||||||
if (isElectron) {
|
data: SongResult
|
||||||
let musicSources: any[] = [];
|
): Promise<MusicParseResult> => {
|
||||||
let quality: string = 'higher';
|
return await MusicParser.parseMusic(id, data);
|
||||||
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 } });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 收藏歌曲
|
// 收藏歌曲
|
||||||
|
|||||||
@@ -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 playMusic: ComputedRef<SongResult>;
|
||||||
export let artistList: ComputedRef<Artist[]>;
|
export let artistList: ComputedRef<Artist[]>;
|
||||||
|
|
||||||
export const musicDB = await useIndexedDB('musicDB', [
|
export const musicDB = await useIndexedDB(
|
||||||
{ name: 'music', keyPath: 'id' },
|
'musicDB',
|
||||||
{ name: 'music_lyric', keyPath: 'id' },
|
[
|
||||||
{ name: 'api_cache', keyPath: 'id' }
|
{ 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 = () => {
|
const setupKeyboardListeners = () => {
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ export const getSongUrl = async (id: any, songData: any, isDownloaded: boolean =
|
|||||||
|
|
||||||
const { data } = await getMusicUrl(id, !unlimitedDownload);
|
const { data } = await getMusicUrl(id, !unlimitedDownload);
|
||||||
let url = '';
|
let url = '';
|
||||||
let songDetail = null;
|
let songDetail: any = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (data.data[0].freeTrialInfo || !data.data[0].url) {
|
if (data.data[0].freeTrialInfo || !data.data[0].url) {
|
||||||
const res = await getParsingMusicUrl(id, cloneDeep(songData));
|
const res = await getParsingMusicUrl(id, cloneDeep(songData));
|
||||||
url = res.data.data.url;
|
if (res.data.data?.url) {
|
||||||
songDetail = res.data.data;
|
url = res.data.data.url;
|
||||||
|
songDetail = res.data.data;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
songDetail = data.data[0] as any;
|
songDetail = data.data[0] as any;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user