diff --git a/.gitignore b/.gitignore index 666f77d..ec206c2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ android/app/release .cursor .windsurf +.agent .auto-imports.d.ts diff --git a/package.json b/package.json index 9a5b1ea..7200583 100644 --- a/package.json +++ b/package.json @@ -33,81 +33,81 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", - "@unblockneteasemusic/server": "^0.27.8-patch.1", + "@unblockneteasemusic/server": "^0.27.10", "cors": "^2.8.5", - "electron-store": "^8.1.0", + "electron-store": "^8.2.0", "electron-updater": "^6.6.2", "electron-window-state": "^5.0.3", - "express": "^4.18.2", - "file-type": "^21.0.0", + "express": "^4.22.1", + "file-type": "^21.1.1", "flac-tagger": "^1.0.7", - "font-list": "^1.5.1", + "font-list": "^1.6.0", "husky": "^9.1.7", - "music-metadata": "^11.2.3", + "music-metadata": "^11.10.3", "netease-cloud-music-api-alger": "^4.26.1", "node-id3": "^0.2.9", "node-machine-id": "^1.1.12", - "pinia-plugin-persistedstate": "^4.5.0", - "sharp": "^0.34.3", - "vue-i18n": "^11.1.3" + "pinia-plugin-persistedstate": "^4.7.1", + "sharp": "^0.34.5", + "vue-i18n": "^11.2.2" }, "devDependencies": { "@electron-toolkit/eslint-config": "^2.1.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^1.0.1", - "@eslint/js": "^9.31.0", - "@rushstack/eslint-patch": "^1.10.3", + "@eslint/js": "^9.39.2", + "@rushstack/eslint-patch": "^1.15.0", "@types/howler": "^2.2.12", - "@types/node": "^20.14.8", + "@types/node": "^20.19.26", "@types/tinycolor2": "^1.4.6", - "@typescript-eslint/eslint-plugin": "^8.30.1", - "@typescript-eslint/parser": "^8.30.1", - "@vitejs/plugin-vue": "^5.0.5", - "@vue/compiler-sfc": "^3.5.0", + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.49.0", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/compiler-sfc": "^3.5.25", "@vue/eslint-config-prettier": "^10.2.0", - "@vue/eslint-config-typescript": "^14.5.0", - "@vue/runtime-core": "^3.5.0", + "@vue/eslint-config-typescript": "^14.6.0", + "@vue/runtime-core": "^3.5.25", "@vueuse/core": "^11.3.0", - "@vueuse/electron": "^13.8.0", + "@vueuse/electron": "^13.9.0", "animate.css": "^4.1.1", - "autoprefixer": "^10.4.20", - "axios": "^1.7.7", + "autoprefixer": "^10.4.22", + "axios": "^1.13.2", "cross-env": "^7.0.3", - "electron": "^38.1.2", + "electron": "^39.2.7", "electron-builder": "^26.0.12", - "electron-vite": "^4.0.0", - "eslint": "^9.34.0", + "electron-vite": "^4.0.1", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "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-vue": "^10.3.0", - "eslint-plugin-vue-scoped-css": "^2.11.0", - "globals": "^16.3.0", + "eslint-plugin-vue": "^10.6.2", + "eslint-plugin-vue-scoped-css": "^2.12.0", + "globals": "^16.5.0", "howler": "^2.2.4", - "lint-staged": "^15.2.10", + "lint-staged": "^15.5.2", "lodash": "^4.17.21", - "marked": "^15.0.4", - "naive-ui": "^2.41.0", - "pinia": "^3.0.1", - "pinyin-match": "^1.2.6", - "postcss": "^8.4.47", - "prettier": "^3.6.2", - "remixicon": "^4.6.0", - "sass": "^1.86.0", - "tailwindcss": "^3.4.17", + "marked": "^15.0.12", + "naive-ui": "^2.43.2", + "pinia": "^3.0.4", + "pinyin-match": "^1.2.10", + "postcss": "^8.5.6", + "prettier": "^3.7.4", + "remixicon": "^4.7.0", + "sass": "^1.96.0", + "tailwindcss": "^3.4.19", "tinycolor2": "^1.6.0", "tunajs": "^1.0.15", - "typescript": "^5.5.2", - "unplugin-auto-import": "^19.1.1", - "unplugin-vue-components": "^28.4.1", - "vite": "^6.2.2", + "typescript": "^5.9.3", + "unplugin-auto-import": "^19.3.0", + "unplugin-vue-components": "^28.8.0", + "vite": "^6.4.1", "vite-plugin-compression": "^0.5.1", "vite-plugin-vue-devtools": "7.7.2", - "vue": "^3.5.13", + "vue": "^3.5.25", "vue-eslint-parser": "^10.2.0", - "vue-router": "^4.5.0", - "vue-tsc": "^2.0.22" + "vue-router": "^4.6.4", + "vue-tsc": "^2.2.12" }, "build": { "appId": "com.alger.music", diff --git a/src/renderer/api/musicParser.ts b/src/renderer/api/musicParser.ts index 73f0970..7be2358 100644 --- a/src/renderer/api/musicParser.ts +++ b/src/renderer/api/musicParser.ts @@ -1,6 +1,7 @@ import { cloneDeep } from 'lodash'; import { musicDB } from '@/hooks/MusicHook'; +import { SongSourceConfigManager } from '@/services/SongSourceConfigManager'; import { useSettingsStore } from '@/store'; import type { SongResult } from '@/types/music'; import { isElectron } from '@/utils'; @@ -33,13 +34,18 @@ export interface MusicParseResult { const CACHE_CONFIG = { // 音乐URL缓存时间:30分钟 MUSIC_URL_CACHE_TIME: 30 * 60 * 1000, - // 失败缓存时间:5分钟 - FAILED_CACHE_TIME: 5 * 60 * 1000, + // 失败缓存时间:1分钟(减少到 1 分钟以便更快恢复) + FAILED_CACHE_TIME: 1 * 60 * 1000, // 重试配置 MAX_RETRY_COUNT: 2, RETRY_DELAY: 1000 }; +/** + * 内存失败缓存(替代 IndexedDB,更轻量且应用重启后自动失效) + */ +const failedCacheMap = new Map(); + /** * 缓存管理器 */ @@ -104,39 +110,46 @@ export class CacheManager { } /** - * 检查是否在失败缓存期内 + * 检查是否在失败缓存期内(使用内存缓存) */ - static async isInFailedCache(id: number, strategyName: string): Promise { - 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); + static isInFailedCache(id: number, strategyName: string): boolean { + const cacheKey = `${id}_${strategyName}`; + const cachedTime = failedCacheMap.get(cacheKey); + if (cachedTime && Date.now() - cachedTime < CACHE_CONFIG.FAILED_CACHE_TIME) { + console.log(`策略 ${strategyName} 在失败缓存期内,跳过`); + return true; + } + // 清理过期缓存 + if (cachedTime) { + failedCacheMap.delete(cacheKey); } return false; } /** - * 添加失败缓存 + * 添加失败缓存(使用内存缓存) */ - static async addFailedCache(id: number, strategyName: string): Promise { - try { - const cacheKey = `${id}_${strategyName}`; - await saveData('music_failed_cache', { - id: cacheKey, - createTime: Date.now() - }); - console.log(`添加失败缓存成功: ${strategyName}`); - } catch (error) { - console.error('添加失败缓存失败:', error); + static addFailedCache(id: number, strategyName: string): void { + const cacheKey = `${id}_${strategyName}`; + failedCacheMap.set(cacheKey, Date.now()); + console.log( + `添加失败缓存成功: ${strategyName} (缓存时间: ${CACHE_CONFIG.FAILED_CACHE_TIME / 1000}秒)` + ); + } + + /** + * 清除指定歌曲的失败缓存 + */ + 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 { // 检查失败缓存 - if (await CacheManager.isInFailedCache(id, this.name)) { + if (CacheManager.isInFailedCache(id, this.name)) { return null; } @@ -339,11 +352,11 @@ class CustomApiStrategy implements MusicSourceStrategy { } // 解析失败,添加失败缓存 - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('自定义API解析失败:', error); - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } } @@ -362,7 +375,7 @@ class BilibiliStrategy implements MusicSourceStrategy { async parse(id: number, data: SongResult): Promise { // 检查失败缓存 - if (await CacheManager.isInFailedCache(id, this.name)) { + if (CacheManager.isInFailedCache(id, this.name)) { return null; } @@ -379,11 +392,11 @@ class BilibiliStrategy implements MusicSourceStrategy { } // 解析失败,添加失败缓存 - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('Bilibili解析失败:', error); - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } } @@ -402,7 +415,7 @@ class GDMusicStrategy implements MusicSourceStrategy { async parse(id: number, data: SongResult): Promise { // 检查失败缓存 - if (await CacheManager.isInFailedCache(id, this.name)) { + if (CacheManager.isInFailedCache(id, this.name)) { return null; } @@ -419,11 +432,11 @@ class GDMusicStrategy implements MusicSourceStrategy { } // 解析失败,添加失败缓存 - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('GD音乐台解析失败:', error); - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } } @@ -450,7 +463,7 @@ class UnblockMusicStrategy implements MusicSourceStrategy { sources?: string[] ): Promise { // 检查失败缓存 - if (await CacheManager.isInFailedCache(id, this.name)) { + if (CacheManager.isInFailedCache(id, this.name)) { return null; } @@ -471,11 +484,11 @@ class UnblockMusicStrategy implements MusicSourceStrategy { } // 解析失败,添加失败缓存 - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('UnblockMusic解析失败:', error); - await CacheManager.addFailedCache(id, this.name); + CacheManager.addFailedCache(id, this.name); return null; } } @@ -512,23 +525,15 @@ class MusicSourceStrategyFactory { * @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); - } + // 尝试获取歌曲自定义音源(使用 SongSourceConfigManager) + const songConfig = SongSourceConfigManager.getConfig(id); + if (songConfig && songConfig.sources.length > 0) { + musicSources = songConfig.sources; + console.log(`使用歌曲 ${id} 自定义音源:`, musicSources); } // 如果没有自定义音源,使用全局设置 diff --git a/src/renderer/components/player/ReparsePopover.vue b/src/renderer/components/player/ReparsePopover.vue index a70ad1f..eb2cf6a 100644 --- a/src/renderer/components/player/ReparsePopover.vue +++ b/src/renderer/components/player/ReparsePopover.vue @@ -81,7 +81,7 @@ import { useI18n } from 'vue-i18n'; import { CacheManager } from '@/api/musicParser'; import { playMusic } from '@/hooks/MusicHook'; -import { audioService } from '@/services/audioService'; +import { SongSourceConfigManager } from '@/services/SongSourceConfigManager'; import { usePlayerStore } from '@/store/modules/player'; import type { Platform } from '@/types/music'; @@ -130,16 +130,11 @@ const getSourceIcon = (source: Platform) => { // 初始化选中的音源 const initSelectedSources = () => { - const songId = String(playMusic.value.id); - const savedSource = localStorage.getItem(`song_source_${songId}`); + const songId = playMusic.value.id; + const config = SongSourceConfigManager.getConfig(songId); - if (savedSource) { - try { - selectedSourcesValue.value = JSON.parse(savedSource); - } catch (e) { - console.error('解析保存的音源设置失败:', e); - selectedSourcesValue.value = []; - } + if (config) { + selectedSourcesValue.value = config.sources; } else { selectedSourcesValue.value = []; } @@ -147,7 +142,7 @@ const initSelectedSources = () => { // 清除自定义音源 const clearCustomSource = () => { - localStorage.removeItem(`song_source_${String(playMusic.value.id)}`); + SongSourceConfigManager.clearConfig(playMusic.value.id); selectedSourcesValue.value = []; }; @@ -168,11 +163,8 @@ const directReparseMusic = async (source: Platform) => { // 更新选中的音源值为当前点击的音源 selectedSourcesValue.value = [source]; - // 保存到localStorage - localStorage.setItem( - `song_source_${String(songId)}`, - JSON.stringify(selectedSourcesValue.value) - ); + // 使用 SongSourceConfigManager 保存配置(手动选择) + SongSourceConfigManager.setConfig(songId, [source], 'manual'); const success = await playerStore.reparseCurrentSong(source, false); @@ -200,49 +192,6 @@ watch( }, { 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); - } - } - } - } -);