fix: 修复音源解析致命性错误

This commit is contained in:
alger
2025-12-13 14:46:15 +08:00
parent 1a0e449e13
commit b9287e1c36
8 changed files with 331 additions and 437 deletions
+1
View File
@@ -31,6 +31,7 @@ android/app/release
.cursor .cursor
.windsurf .windsurf
.agent
.auto-imports.d.ts .auto-imports.d.ts
+45 -45
View File
@@ -33,81 +33,81 @@
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"@unblockneteasemusic/server": "^0.27.8-patch.1", "@unblockneteasemusic/server": "^0.27.10",
"cors": "^2.8.5", "cors": "^2.8.5",
"electron-store": "^8.1.0", "electron-store": "^8.2.0",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"express": "^4.18.2", "express": "^4.22.1",
"file-type": "^21.0.0", "file-type": "^21.1.1",
"flac-tagger": "^1.0.7", "flac-tagger": "^1.0.7",
"font-list": "^1.5.1", "font-list": "^1.6.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"music-metadata": "^11.2.3", "music-metadata": "^11.10.3",
"netease-cloud-music-api-alger": "^4.26.1", "netease-cloud-music-api-alger": "^4.26.1",
"node-id3": "^0.2.9", "node-id3": "^0.2.9",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
"pinia-plugin-persistedstate": "^4.5.0", "pinia-plugin-persistedstate": "^4.7.1",
"sharp": "^0.34.3", "sharp": "^0.34.5",
"vue-i18n": "^11.1.3" "vue-i18n": "^11.2.2"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/eslint-config": "^2.1.0", "@electron-toolkit/eslint-config": "^2.1.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@eslint/js": "^9.31.0", "@eslint/js": "^9.39.2",
"@rushstack/eslint-patch": "^1.10.3", "@rushstack/eslint-patch": "^1.15.0",
"@types/howler": "^2.2.12", "@types/howler": "^2.2.12",
"@types/node": "^20.14.8", "@types/node": "^20.19.26",
"@types/tinycolor2": "^1.4.6", "@types/tinycolor2": "^1.4.6",
"@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.30.1", "@typescript-eslint/parser": "^8.49.0",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.0", "@vue/compiler-sfc": "^3.5.25",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0", "@vue/eslint-config-typescript": "^14.6.0",
"@vue/runtime-core": "^3.5.0", "@vue/runtime-core": "^3.5.25",
"@vueuse/core": "^11.3.0", "@vueuse/core": "^11.3.0",
"@vueuse/electron": "^13.8.0", "@vueuse/electron": "^13.9.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.22",
"axios": "^1.7.7", "axios": "^1.13.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"electron": "^38.1.2", "electron": "^39.2.7",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electron-vite": "^4.0.0", "electron-vite": "^4.0.1",
"eslint": "^9.34.0", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.3", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-vue": "^10.3.0", "eslint-plugin-vue": "^10.6.2",
"eslint-plugin-vue-scoped-css": "^2.11.0", "eslint-plugin-vue-scoped-css": "^2.12.0",
"globals": "^16.3.0", "globals": "^16.5.0",
"howler": "^2.2.4", "howler": "^2.2.4",
"lint-staged": "^15.2.10", "lint-staged": "^15.5.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^15.0.4", "marked": "^15.0.12",
"naive-ui": "^2.41.0", "naive-ui": "^2.43.2",
"pinia": "^3.0.1", "pinia": "^3.0.4",
"pinyin-match": "^1.2.6", "pinyin-match": "^1.2.10",
"postcss": "^8.4.47", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.7.4",
"remixicon": "^4.6.0", "remixicon": "^4.7.0",
"sass": "^1.86.0", "sass": "^1.96.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.19",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"tunajs": "^1.0.15", "tunajs": "^1.0.15",
"typescript": "^5.5.2", "typescript": "^5.9.3",
"unplugin-auto-import": "^19.1.1", "unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.4.1", "unplugin-vue-components": "^28.8.0",
"vite": "^6.2.2", "vite": "^6.4.1",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.7.2", "vite-plugin-vue-devtools": "7.7.2",
"vue": "^3.5.13", "vue": "^3.5.25",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-router": "^4.5.0", "vue-router": "^4.6.4",
"vue-tsc": "^2.0.22" "vue-tsc": "^2.2.12"
}, },
"build": { "build": {
"appId": "com.alger.music", "appId": "com.alger.music",
+58 -53
View File
@@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { musicDB } from '@/hooks/MusicHook'; import { musicDB } from '@/hooks/MusicHook';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import { useSettingsStore } from '@/store'; import { useSettingsStore } from '@/store';
import type { SongResult } from '@/types/music'; import type { SongResult } from '@/types/music';
import { isElectron } from '@/utils'; import { isElectron } from '@/utils';
@@ -33,13 +34,18 @@ export interface MusicParseResult {
const CACHE_CONFIG = { const CACHE_CONFIG = {
// 音乐URL缓存时间:30分钟 // 音乐URL缓存时间:30分钟
MUSIC_URL_CACHE_TIME: 30 * 60 * 1000, MUSIC_URL_CACHE_TIME: 30 * 60 * 1000,
// 失败缓存时间:5分钟 // 失败缓存时间:1分钟(减少到 1 分钟以便更快恢复)
FAILED_CACHE_TIME: 5 * 60 * 1000, FAILED_CACHE_TIME: 1 * 60 * 1000,
// 重试配置 // 重试配置
MAX_RETRY_COUNT: 2, MAX_RETRY_COUNT: 2,
RETRY_DELAY: 1000 RETRY_DELAY: 1000
}; };
/**
* 内存失败缓存(替代 IndexedDB,更轻量且应用重启后自动失效)
*/
const failedCacheMap = new Map<string, number>();
/** /**
* 缓存管理器 * 缓存管理器
*/ */
@@ -104,39 +110,46 @@ export class CacheManager {
} }
/** /**
* 检查是否在失败缓存期内 * 检查是否在失败缓存期内(使用内存缓存)
*/ */
static async isInFailedCache(id: number, strategyName: string): Promise<boolean> { static isInFailedCache(id: number, strategyName: string): boolean {
try { const cacheKey = `${id}_${strategyName}`;
const cacheKey = `${id}_${strategyName}`; const cachedTime = failedCacheMap.get(cacheKey);
const cached = await getData('music_failed_cache', cacheKey); if (cachedTime && Date.now() - cachedTime < CACHE_CONFIG.FAILED_CACHE_TIME) {
if (cached?.createTime && Date.now() - cached.createTime < CACHE_CONFIG.FAILED_CACHE_TIME) { console.log(`策略 ${strategyName} 在失败缓存期内,跳过`);
console.log(`策略 ${strategyName} 在失败缓存期内,跳过`); return true;
return true; }
} // 清理过期缓存
// 清理过期缓存 if (cachedTime) {
if (cached) { failedCacheMap.delete(cacheKey);
await deleteData('music_failed_cache', cacheKey);
}
} catch (error) {
console.warn('检查失败缓存失败:', error);
} }
return false; return false;
} }
/** /**
* 添加失败缓存 * 添加失败缓存(使用内存缓存)
*/ */
static async addFailedCache(id: number, strategyName: string): Promise<void> { static addFailedCache(id: number, strategyName: string): void {
try { const cacheKey = `${id}_${strategyName}`;
const cacheKey = `${id}_${strategyName}`; failedCacheMap.set(cacheKey, Date.now());
await saveData('music_failed_cache', { console.log(
id: cacheKey, `添加失败缓存成功: ${strategyName} (缓存时间: ${CACHE_CONFIG.FAILED_CACHE_TIME / 1000}秒)`
createTime: Date.now() );
}); }
console.log(`添加失败缓存成功: ${strategyName}`);
} catch (error) { /**
console.error('添加失败缓存失败:', error); * 清除指定歌曲的失败缓存
*/
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}`);
} }
} }
@@ -322,7 +335,7 @@ class CustomApiStrategy implements MusicSourceStrategy {
async parse(id: number, data: SongResult, quality = 'higher'): Promise<MusicParseResult | null> { async parse(id: number, data: SongResult, quality = 'higher'): Promise<MusicParseResult | null> {
// 检查失败缓存 // 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) { if (CacheManager.isInFailedCache(id, this.name)) {
return null; return null;
} }
@@ -339,11 +352,11 @@ class CustomApiStrategy implements MusicSourceStrategy {
} }
// 解析失败,添加失败缓存 // 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} catch (error) { } catch (error) {
console.error('自定义API解析失败:', error); console.error('自定义API解析失败:', error);
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} }
} }
@@ -362,7 +375,7 @@ class BilibiliStrategy implements MusicSourceStrategy {
async parse(id: number, data: SongResult): Promise<MusicParseResult | null> { async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {
// 检查失败缓存 // 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) { if (CacheManager.isInFailedCache(id, this.name)) {
return null; return null;
} }
@@ -379,11 +392,11 @@ class BilibiliStrategy implements MusicSourceStrategy {
} }
// 解析失败,添加失败缓存 // 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} catch (error) { } catch (error) {
console.error('Bilibili解析失败:', error); console.error('Bilibili解析失败:', error);
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} }
} }
@@ -402,7 +415,7 @@ class GDMusicStrategy implements MusicSourceStrategy {
async parse(id: number, data: SongResult): Promise<MusicParseResult | null> { async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {
// 检查失败缓存 // 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) { if (CacheManager.isInFailedCache(id, this.name)) {
return null; return null;
} }
@@ -419,11 +432,11 @@ class GDMusicStrategy implements MusicSourceStrategy {
} }
// 解析失败,添加失败缓存 // 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} catch (error) { } catch (error) {
console.error('GD音乐台解析失败:', error); console.error('GD音乐台解析失败:', error);
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} }
} }
@@ -450,7 +463,7 @@ class UnblockMusicStrategy implements MusicSourceStrategy {
sources?: string[] sources?: string[]
): Promise<MusicParseResult | null> { ): Promise<MusicParseResult | null> {
// 检查失败缓存 // 检查失败缓存
if (await CacheManager.isInFailedCache(id, this.name)) { if (CacheManager.isInFailedCache(id, this.name)) {
return null; return null;
} }
@@ -471,11 +484,11 @@ class UnblockMusicStrategy implements MusicSourceStrategy {
} }
// 解析失败,添加失败缓存 // 解析失败,添加失败缓存
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} catch (error) { } catch (error) {
console.error('UnblockMusic解析失败:', error); console.error('UnblockMusic解析失败:', error);
await CacheManager.addFailedCache(id, this.name); CacheManager.addFailedCache(id, this.name);
return null; return null;
} }
} }
@@ -512,23 +525,15 @@ class MusicSourceStrategyFactory {
* @returns 音源列表和音质设置 * @returns 音源列表和音质设置
*/ */
const getMusicConfig = (id: number, settingsStore?: any) => { const getMusicConfig = (id: number, settingsStore?: any) => {
const songId = String(id);
let musicSources: string[] = []; let musicSources: string[] = [];
let quality = 'higher'; let quality = 'higher';
try { try {
// 尝试获取歌曲自定义音源 // 尝试获取歌曲自定义音源(使用 SongSourceConfigManager
const savedSourceStr = localStorage.getItem(`song_source_${songId}`); const songConfig = SongSourceConfigManager.getConfig(id);
if (savedSourceStr) { if (songConfig && songConfig.sources.length > 0) {
try { musicSources = songConfig.sources;
const customSources = JSON.parse(savedSourceStr); console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
if (Array.isArray(customSources)) {
musicSources = customSources;
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
}
} catch (error) {
console.error('解析自定义音源设置失败:', error);
}
} }
// 如果没有自定义音源,使用全局设置 // 如果没有自定义音源,使用全局设置
@@ -81,7 +81,7 @@ import { useI18n } from 'vue-i18n';
import { CacheManager } from '@/api/musicParser'; import { CacheManager } from '@/api/musicParser';
import { playMusic } from '@/hooks/MusicHook'; import { playMusic } from '@/hooks/MusicHook';
import { audioService } from '@/services/audioService'; import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import { usePlayerStore } from '@/store/modules/player'; import { usePlayerStore } from '@/store/modules/player';
import type { Platform } from '@/types/music'; import type { Platform } from '@/types/music';
@@ -130,16 +130,11 @@ const getSourceIcon = (source: Platform) => {
// 初始化选中的音源 // 初始化选中的音源
const initSelectedSources = () => { const initSelectedSources = () => {
const songId = String(playMusic.value.id); const songId = playMusic.value.id;
const savedSource = localStorage.getItem(`song_source_${songId}`); const config = SongSourceConfigManager.getConfig(songId);
if (savedSource) { if (config) {
try { selectedSourcesValue.value = config.sources;
selectedSourcesValue.value = JSON.parse(savedSource);
} catch (e) {
console.error('解析保存的音源设置失败:', e);
selectedSourcesValue.value = [];
}
} else { } else {
selectedSourcesValue.value = []; selectedSourcesValue.value = [];
} }
@@ -147,7 +142,7 @@ const initSelectedSources = () => {
// 清除自定义音源 // 清除自定义音源
const clearCustomSource = () => { const clearCustomSource = () => {
localStorage.removeItem(`song_source_${String(playMusic.value.id)}`); SongSourceConfigManager.clearConfig(playMusic.value.id);
selectedSourcesValue.value = []; selectedSourcesValue.value = [];
}; };
@@ -168,11 +163,8 @@ const directReparseMusic = async (source: Platform) => {
// 更新选中的音源值为当前点击的音源 // 更新选中的音源值为当前点击的音源
selectedSourcesValue.value = [source]; selectedSourcesValue.value = [source];
// 保存到localStorage // 使用 SongSourceConfigManager 保存配置(手动选择)
localStorage.setItem( SongSourceConfigManager.setConfig(songId, [source], 'manual');
`song_source_${String(songId)}`,
JSON.stringify(selectedSourcesValue.value)
);
const success = await playerStore.reparseCurrentSong(source, false); const success = await playerStore.reparseCurrentSong(source, false);
@@ -200,49 +192,6 @@ watch(
}, },
{ immediate: true } { immediate: true }
); );
// 监听歌曲变化,检查是否有自定义音源
watch(
() => playMusic.value.id,
async (newId) => {
if (newId) {
const songId = String(newId);
const savedSource = localStorage.getItem(`song_source_${songId}`);
// 如果有保存的音源设置但当前不是使用自定义解析的播放,尝试应用
if (savedSource && playMusic.value.source !== 'bilibili') {
try {
const sources = JSON.parse(savedSource) as Platform[];
console.log(`检测到歌曲ID ${songId} 有自定义音源设置:`, sources);
// 当URL加载失败或过期时,自动应用自定义音源重新加载
audioService.on('url_expired', async (trackInfo) => {
if (trackInfo && trackInfo.id === playMusic.value.id) {
console.log('URL已过期,自动应用自定义音源重新加载');
try {
isReparsing.value = true;
const songId = String(playMusic.value.id);
const sourceType = localStorage.getItem(`song_source_type_${songId}`);
const isAuto = sourceType === 'auto';
const success = await playerStore.reparseCurrentSong(sources[0], isAuto);
if (!success) {
message.error(t('player.reparse.failed'));
}
} catch (e) {
console.error('自动重新解析失败:', e);
message.error(t('player.reparse.failed'));
} finally {
isReparsing.value = false;
}
}
});
} catch (e) {
console.error('解析保存的音源设置失败:', e);
}
}
}
}
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+5 -13
View File
@@ -5,6 +5,7 @@ import i18n from '@/../i18n/renderer';
import { getBilibiliAudioUrl } from '@/api/bilibili'; import { getBilibiliAudioUrl } from '@/api/bilibili';
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music'; import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { playbackRequestManager } from '@/services/playbackRequestManager'; import { playbackRequestManager } from '@/services/playbackRequestManager';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import type { ILyric, ILyricText, IWordData, SongResult } from '@/types/music'; import type { ILyric, ILyricText, IWordData, SongResult } from '@/types/music';
import { getImgUrl } from '@/utils'; import { getImgUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor'; import { getImageLinearBackground } from '@/utils/linearColor';
@@ -64,17 +65,8 @@ export const getSongUrl = async (
const globalSources = settingsStore.setData.enabledMusicSources || []; const globalSources = settingsStore.setData.enabledMusicSources || [];
const useCustomApiGlobally = globalSources.includes('custom'); const useCustomApiGlobally = globalSources.includes('custom');
const songId = String(id); const songConfig = SongSourceConfigManager.getConfig(id);
const savedSourceStr = localStorage.getItem(`song_source_${songId}`); const useCustomApiForSong = songConfig?.sources.includes('custom' as any) ?? false;
let useCustomApiForSong = false;
if (savedSourceStr) {
try {
const songSources = JSON.parse(savedSourceStr);
useCustomApiForSong = songSources.includes('custom');
} catch (e) {
console.error('解析歌曲音源设置失败:', e);
}
}
// 如果全局或歌曲专属设置中启用了自定义API,则最优先尝试 // 如果全局或歌曲专属设置中启用了自定义API,则最优先尝试
if ((useCustomApiGlobally || useCustomApiForSong) && settingsStore.setData.customApiPlugin) { if ((useCustomApiGlobally || useCustomApiForSong) && settingsStore.setData.customApiPlugin) {
@@ -116,9 +108,9 @@ export const getSongUrl = async (
} }
// 如果有自定义音源设置,直接使用getParsingMusicUrl获取URL // 如果有自定义音源设置,直接使用getParsingMusicUrl获取URL
if (savedSourceStr && songData.source !== 'bilibili') { if (songConfig && songData.source !== 'bilibili') {
try { try {
console.log(`使用自定义音源解析歌曲 ID: ${songId}`); console.log(`使用自定义音源解析歌曲 ID: ${id}`);
const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
console.log('res', res); console.log('res', res);
@@ -0,0 +1,198 @@
/**
* 歌曲音源配置管理器
*
* 职责:
* 1. 统一管理每首歌曲的自定义音源配置
* 2. 提供清晰的读取/写入/清除 API
* 3. 区分"手动"和"自动"设置的音源
* 4. 管理已尝试的音源列表(按歌曲隔离)
*/
import type { Platform } from '@/types/music';
// 歌曲音源配置类型
export type SongSourceConfig = {
sources: Platform[];
type: 'manual' | 'auto';
updatedAt: number;
};
// 内存中缓存已尝试的音源(按歌曲隔离)
const triedSourcesMap = new Map<string, Set<string>>();
const triedSourceDiffsMap = new Map<string, Map<string, number>>();
// localStorage key 前缀
const STORAGE_KEY_PREFIX = 'song_source_';
const STORAGE_TYPE_KEY_PREFIX = 'song_source_type_';
/**
* 歌曲音源配置管理器
*/
export class SongSourceConfigManager {
/**
* 获取歌曲的自定义音源配置
*/
static getConfig(songId: number | string): SongSourceConfig | null {
const id = String(songId);
const sourcesStr = localStorage.getItem(`${STORAGE_KEY_PREFIX}${id}`);
const typeStr = localStorage.getItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`);
if (!sourcesStr) {
return null;
}
try {
const sources = JSON.parse(sourcesStr) as Platform[];
if (!Array.isArray(sources) || sources.length === 0) {
return null;
}
return {
sources,
type: typeStr === 'auto' ? 'auto' : 'manual',
updatedAt: Date.now()
};
} catch (error) {
console.error(`[SongSourceConfigManager] 解析歌曲 ${id} 配置失败:`, error);
return null;
}
}
/**
* 设置歌曲的自定义音源配置
*/
static setConfig(
songId: number | string,
sources: Platform[],
type: 'manual' | 'auto' = 'manual'
): void {
const id = String(songId);
if (!sources || sources.length === 0) {
this.clearConfig(songId);
return;
}
try {
localStorage.setItem(`${STORAGE_KEY_PREFIX}${id}`, JSON.stringify(sources));
localStorage.setItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`, type);
console.log(`[SongSourceConfigManager] 设置歌曲 ${id} 音源: ${sources.join(', ')} (${type})`);
} catch (error) {
console.error(`[SongSourceConfigManager] 保存歌曲 ${id} 配置失败:`, error);
}
}
/**
* 清除歌曲的自定义配置
*/
static clearConfig(songId: number | string): void {
const id = String(songId);
localStorage.removeItem(`${STORAGE_KEY_PREFIX}${id}`);
localStorage.removeItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`);
// 同时清除内存中的已尝试音源
this.clearTriedSources(songId);
console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 配置`);
}
/**
* 检查歌曲是否有自定义配置
*/
static hasConfig(songId: number | string): boolean {
return this.getConfig(songId) !== null;
}
/**
* 检查配置类型是否为手动设置
*/
static isManualConfig(songId: number | string): boolean {
const config = this.getConfig(songId);
return config?.type === 'manual';
}
// ==================== 已尝试音源管理 ====================
/**
* 获取歌曲已尝试的音源列表
*/
static getTriedSources(songId: number | string): Set<string> {
const id = String(songId);
if (!triedSourcesMap.has(id)) {
triedSourcesMap.set(id, new Set());
}
return triedSourcesMap.get(id)!;
}
/**
* 添加已尝试的音源
*/
static addTriedSource(songId: number | string, source: string): void {
const id = String(songId);
const tried = this.getTriedSources(id);
tried.add(source);
console.log(`[SongSourceConfigManager] 歌曲 ${id} 添加已尝试音源: ${source}`);
}
/**
* 清除歌曲的已尝试音源
*/
static clearTriedSources(songId: number | string): void {
const id = String(songId);
triedSourcesMap.delete(id);
triedSourceDiffsMap.delete(id);
console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 已尝试音源`);
}
/**
* 获取歌曲已尝试音源的时长差异
*/
static getTriedSourceDiffs(songId: number | string): Map<string, number> {
const id = String(songId);
if (!triedSourceDiffsMap.has(id)) {
triedSourceDiffsMap.set(id, new Map());
}
return triedSourceDiffsMap.get(id)!;
}
/**
* 设置音源的时长差异
*/
static setTriedSourceDiff(songId: number | string, source: string, diff: number): void {
const id = String(songId);
const diffs = this.getTriedSourceDiffs(id);
diffs.set(source, diff);
}
/**
* 查找最佳匹配的音源(时长差异最小)
*/
static findBestMatchingSource(songId: number | string): { source: string; diff: number } | null {
const diffs = this.getTriedSourceDiffs(songId);
if (diffs.size === 0) {
return null;
}
let bestSource = '';
let minDiff = Infinity;
for (const [source, diff] of diffs.entries()) {
if (diff < minDiff) {
minDiff = diff;
bestSource = source;
}
}
return bestSource ? { source: bestSource, diff: minDiff } : null;
}
/**
* 清理所有内存缓存(用于测试或重置)
*/
static clearAllMemoryCache(): void {
triedSourcesMap.clear();
triedSourceDiffsMap.clear();
console.log('[SongSourceConfigManager] 清除所有内存缓存');
}
}
// 导出单例实例方便使用
export const songSourceConfig = SongSourceConfigManager;
+6 -153
View File
@@ -1,7 +1,5 @@
import { Howl } from 'howler'; import { Howl } from 'howler';
import { cloneDeep } from 'lodash';
import { getParsingMusicUrl } from '@/api/music';
import type { SongResult } from '@/types/music'; import type { SongResult } from '@/types/music';
class PreloadService { class PreloadService {
@@ -60,167 +58,22 @@ class PreloadService {
} }
// 创建初始音频实例 // 创建初始音频实例
let sound = await this._createSound(song.playMusicUrl); const sound = await this._createSound(song.playMusicUrl);
// 检查时长 // 检查时长
const duration = sound.duration(); const duration = sound.duration();
const expectedDuration = (song.dt || 0) / 1000; const expectedDuration = (song.dt || 0) / 1000;
// 如果时长差异超过5秒,且不是B站视频,且预期时长大于0 // 时长差异只记录警告,不自动触发重新解析
// 用户可以通过 ReparsePopover 手动选择正确的音源
if ( if (
expectedDuration > 0 && expectedDuration > 0 &&
Math.abs(duration - expectedDuration) > 5 && Math.abs(duration - expectedDuration) > 5 &&
song.source !== 'bilibili' song.source !== 'bilibili'
) { ) {
const songId = String(song.id); console.warn(
const sourceType = localStorage.getItem(`song_source_type_${songId}`); `[PreloadService] 时长差异警告:实际 ${duration.toFixed(1)}s, 预期 ${expectedDuration.toFixed(1)}s (${song.name})`
);
// 如果不是用户手动锁定的音源,尝试自动重新解析
if (sourceType !== 'manual') {
console.warn(
`[PreloadService] 时长不匹配 (实际: ${duration}s, 预期: ${expectedDuration}s),尝试智能解析`
);
// 动态导入 store
const { useSettingsStore } = await import('@/store/modules/settings');
const { usePlaylistStore } = await import('@/store/modules/playlist');
const settingsStore = useSettingsStore();
const playlistStore = usePlaylistStore();
const enabledSources = settingsStore.setData.enabledMusicSources || [
'migu',
'kugou',
'pyncmd',
'gdmusic'
];
const availableSources = enabledSources.filter((s: string) => s !== 'bilibili');
const triedSources = new Set<string>();
const triedSourceDiffs = new Map<string, number>();
// 记录当前音源
let currentSource = 'unknown';
const currentSavedSource = localStorage.getItem(`song_source_${songId}`);
if (currentSavedSource) {
try {
const sources = JSON.parse(currentSavedSource);
if (Array.isArray(sources) && sources.length > 0) {
currentSource = sources[0];
}
} catch (e) {
console.log(
`[PreloadService] 时长不匹配 (实际: ${duration}s, 预期: ${expectedDuration}s),尝试智能解析`,
e
);
}
}
triedSources.add(currentSource);
triedSourceDiffs.set(currentSource, Math.abs(duration - expectedDuration));
// 卸载当前不匹配的音频
sound.unload();
// 尝试其他音源
for (const source of availableSources) {
if (triedSources.has(source)) continue;
console.log(`[PreloadService] 尝试音源: ${source}`);
triedSources.add(source);
try {
const songData = cloneDeep(song);
// 临时保存设置以便 getParsingMusicUrl 使用
localStorage.setItem(`song_source_${songId}`, JSON.stringify([source]));
const res = await getParsingMusicUrl(
typeof song.id === 'string' ? parseInt(song.id) : song.id,
songData
);
if (res && res.data && res.data.data && res.data.data.url) {
const newUrl = res.data.data.url;
const tempSound = await this._createSound(newUrl);
const newDuration = tempSound.duration();
const diff = Math.abs(newDuration - expectedDuration);
triedSourceDiffs.set(source, diff);
if (diff <= 5) {
console.log(`[PreloadService] 找到匹配音源: ${source}, 更新歌曲信息`);
// 更新歌曲信息
const updatedSong = {
...song,
playMusicUrl: newUrl,
expiredAt: Date.now() + 1800000
};
// 更新 store
playlistStore.updateSong(updatedSong);
// 记录新的音源设置
localStorage.setItem(`song_source_${songId}`, JSON.stringify([source]));
localStorage.setItem(`song_source_type_${songId}`, 'auto');
return tempSound;
} else {
tempSound.unload();
}
}
} catch (e) {
console.error(`[PreloadService] 尝试音源 ${source} 失败:`, e);
}
}
// 如果没有找到完美匹配,使用最佳匹配
console.warn('[PreloadService] 未找到完美匹配,寻找最佳匹配');
let bestSource = '';
let minDiff = Infinity;
for (const [source, diff] of triedSourceDiffs.entries()) {
if (diff < minDiff) {
minDiff = diff;
bestSource = source;
}
}
if (bestSource && bestSource !== currentSource) {
console.log(`[PreloadService] 使用最佳匹配音源: ${bestSource} (差异: ${minDiff}s)`);
try {
const songData = cloneDeep(song);
localStorage.setItem(`song_source_${songId}`, JSON.stringify([bestSource]));
const res = await getParsingMusicUrl(
typeof song.id === 'string' ? parseInt(song.id) : song.id,
songData
);
if (res && res.data && res.data.data && res.data.data.url) {
const newUrl = res.data.data.url;
const bestSound = await this._createSound(newUrl);
const updatedSong = {
...song,
playMusicUrl: newUrl,
expiredAt: Date.now() + 1800000
};
playlistStore.updateSong(updatedSong);
localStorage.setItem(`song_source_type_${songId}`, 'auto');
return bestSound;
}
} catch (e) {
console.error(`[PreloadService] 获取最佳匹配音源失败:`, e);
}
}
}
}
// 如果不需要修复或修复失败,重新加载原始音频(因为上面可能unload了)
if (sound.state() === 'unloaded') {
sound = await this._createSound(song.playMusicUrl);
} }
return sound; return sound;
+10 -114
View File
@@ -11,6 +11,7 @@ import { useLyrics, useSongDetail } from '@/hooks/usePlayerHooks';
import { audioService } from '@/services/audioService'; import { audioService } from '@/services/audioService';
import { playbackRequestManager } from '@/services/playbackRequestManager'; import { playbackRequestManager } from '@/services/playbackRequestManager';
import { preloadService } from '@/services/preloadService'; import { preloadService } from '@/services/preloadService';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import type { Platform, SongResult } from '@/types/music'; import type { Platform, SongResult } from '@/types/music';
import { getImgUrl } from '@/utils'; import { getImgUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor'; import { getImageLinearBackground } from '@/utils/linearColor';
@@ -30,8 +31,6 @@ export const usePlayerCoreStore = defineStore(
const isPlay = ref(false); const isPlay = ref(false);
const playMusic = ref<SongResult>({} as SongResult); const playMusic = ref<SongResult>({} as SongResult);
const playMusicUrl = ref(''); const playMusicUrl = ref('');
const triedSources = ref<Set<string>>(new Set());
const triedSourceDiffs = ref<Map<string, number>>(new Map());
const musicFull = ref(false); const musicFull = ref(false);
const playbackRate = ref(1.0); const playbackRate = ref(1.0);
const volume = ref(1); const volume = ref(1);
@@ -165,10 +164,9 @@ export const usePlayerCoreStore = defineStore(
* 核心播放处理函数 * 核心播放处理函数
*/ */
const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => { const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => {
// 如果是新歌曲,重置已尝试的音源 // 如果是新歌曲,重置已尝试的音源(使用 SongSourceConfigManager 按歌曲隔离)
if (music.id !== playMusic.value.id) { if (music.id !== playMusic.value.id) {
triedSources.value.clear(); SongSourceConfigManager.clearTriedSources(music.id);
triedSourceDiffs.value.clear();
} }
// 创建新的播放请求并取消之前的所有请求 // 创建新的播放请求并取消之前的所有请求
@@ -427,110 +425,7 @@ export const usePlayerCoreStore = defineStore(
new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } }) new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } })
); );
// 检查时长是否匹配,如果不匹配则尝试自动重新解析 // 时长检查已在 preloadService.ts 中完成
const duration = newSound.duration();
const expectedDuration = (playMusic.value.dt || 0) / 1000;
// 如果时长差异超过5秒,且不是B站视频,且预期时长大于0
if (
expectedDuration > 0 &&
Math.abs(duration - expectedDuration) > 5 &&
playMusic.value.source !== 'bilibili' &&
playMusic.value.id
) {
const songId = String(playMusic.value.id);
const sourceType = localStorage.getItem(`song_source_type_${songId}`);
// 如果不是用户手动锁定的音源
if (sourceType !== 'manual') {
console.warn(
`时长不匹配 (实际: ${duration}s, 预期: ${expectedDuration}s),尝试自动切换音源`
);
// 记录当前失败的音源
// 注意:这里假设当前使用的音源是 playMusic.value.source,或者是刚刚解析出来的
// 但实际上我们需要知道当前具体是用哪个平台解析成功的,这可能需要从 getSongUrl 的结果中获取
// 暂时简单处理,将当前配置的来源加入已尝试列表
// 获取所有可用音源
const { useSettingsStore } = await import('./settings');
const settingsStore = useSettingsStore();
const enabledSources = settingsStore.setData.enabledMusicSources || [
'migu',
'kugou',
'pyncmd',
'gdmusic'
];
const availableSources: Platform[] = enabledSources.filter(
(s: string) => s !== 'bilibili'
);
// 将当前正在使用的音源加入已尝试列表
let currentSource = 'unknown';
const currentSavedSource = localStorage.getItem(`song_source_${songId}`);
if (currentSavedSource) {
try {
const sources = JSON.parse(currentSavedSource);
if (Array.isArray(sources) && sources.length > 0) {
currentSource = sources[0];
triedSources.value.add(currentSource);
}
} catch {
console.error(`解析当前音源失败: ${currentSource}`);
}
}
// 找到下一个未尝试的音源
const nextSource = availableSources.find((s) => !triedSources.value.has(s));
// 记录当前音源的时间差
if (currentSource !== 'unknown') {
triedSourceDiffs.value.set(currentSource, Math.abs(duration - expectedDuration));
}
if (nextSource) {
console.log(`自动切换到音源: ${nextSource}`);
newSound.stop();
newSound.unload();
// 递归调用 reparseCurrentSong
// 注意:这里是异步调用,不会阻塞当前函数返回,但我们已经停止了播放
const success = await reparseCurrentSong(nextSource, true);
if (success) {
return audioService.getCurrentSound();
}
return null;
} else {
console.warn('所有音源都已尝试,寻找最接近时长的版本');
// 找出时间差最小的音源
let bestSource = '';
let minDiff = Infinity;
for (const [source, diff] of triedSourceDiffs.value.entries()) {
if (diff < minDiff) {
minDiff = diff;
bestSource = source;
}
}
// 如果找到了最佳音源,且不是当前正在播放的音源
if (bestSource && bestSource !== currentSource) {
console.log(`切换到最佳匹配音源: ${bestSource} (差异: ${minDiff}s)`);
newSound.stop();
newSound.unload();
const success = await reparseCurrentSong(bestSource as Platform, true);
if (success) {
return audioService.getCurrentSound();
}
return null;
}
console.log(`当前音源 ${currentSource} 已经是最佳匹配 (差异: ${minDiff}s),保留播放`);
}
}
}
return newSound; return newSound;
} catch (error) { } catch (error) {
@@ -630,11 +525,12 @@ export const usePlayerCoreStore = defineStore(
return false; return false;
} }
const songId = String(currentSong.id); // 使用 SongSourceConfigManager 保存配置
localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform])); SongSourceConfigManager.setConfig(
currentSong.id,
// 记录音源设置类型(自动/手动) [sourcePlatform],
localStorage.setItem(`song_source_type_${songId}`, isAuto ? 'auto' : 'manual'); isAuto ? 'auto' : 'manual'
);
const currentSound = audioService.getCurrentSound(); const currentSound = audioService.getCurrentSound();
if (currentSound) { if (currentSound) {