diff --git a/src/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index 0b976f1..ae8840c 100644 --- a/src/main/modules/fileManager.ts +++ b/src/main/modules/fileManager.ts @@ -310,6 +310,47 @@ export function initializeFileManager() { throw new Error(`文件读取或解析失败: ${error.message}`); } }); + + // 处理导入落雪音源脚本的请求 + ipcMain.handle('import-lx-music-script', async () => { + const result = await dialog.showOpenDialog({ + title: '选择落雪音源脚本文件', + filters: [{ name: 'JavaScript Files', extensions: ['js'] }], + properties: ['openFile'] + }); + + if (result.canceled || result.filePaths.length === 0) { + return null; + } + + const filePath = result.filePaths[0]; + try { + const fileContent = fs.readFileSync(filePath, 'utf-8'); + + // 验证脚本格式:检查是否包含落雪音源特征 + if ( + !fileContent.includes('globalThis.lx') && + !fileContent.includes('lx.on') && + !fileContent.includes('EVENT_NAMES') + ) { + throw new Error('无效的落雪音源脚本,未找到 globalThis.lx 相关代码。'); + } + + // 检查是否包含必要的元信息注释 + const hasMetaComment = fileContent.includes('@name'); + if (!hasMetaComment) { + console.warn('警告: 脚本缺少 @name 元信息注释'); + } + + return { + name: path.basename(filePath, '.js'), + content: fileContent + }; + } catch (error: any) { + console.error('读取落雪音源脚本失败:', error); + throw new Error(`脚本读取失败: ${error.message}`); + } + }); } /** diff --git a/src/main/set.json b/src/main/set.json index 5591af1..cf63ae5 100644 --- a/src/main/set.json +++ b/src/main/set.json @@ -24,7 +24,7 @@ "alwaysShowDownloadButton": false, "unlimitedDownload": false, "enableMusicUnblock": true, - "enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili"], + "enabledMusicSources": ["migu", "kugou", "pyncmd"], "showTopAction": false, "contentZoomFactor": 1, "autoTheme": false, @@ -32,5 +32,7 @@ "isMenuExpanded": false, "customApiPlugin": "", "customApiPluginName": "", + "lxMusicScripts": [], + "activeLxMusicApiId": null, "enableGpuAcceleration": true } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 13aac75..2dfcea9 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -22,6 +22,7 @@ interface API { onLanguageChanged: (callback: (locale: string) => void) => void; removeDownloadListeners: () => void; importCustomApiPlugin: () => Promise<{ name: string; content: string } | null>; + importLxMusicScript: () => Promise<{ name: string; content: string } | null>; invoke: (channel: string, ...args: any[]) => Promise; getSearchSuggestions: (keyword: string) => Promise; } diff --git a/src/preload/index.ts b/src/preload/index.ts index b393a1e..8691734 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -19,6 +19,7 @@ const api = { unblockMusic: (id, data, enabledSources) => ipcRenderer.invoke('unblock-music', id, data, enabledSources), importCustomApiPlugin: () => ipcRenderer.invoke('import-custom-api-plugin'), + importLxMusicScript: () => ipcRenderer.invoke('import-lx-music-script'), // 歌词窗口关闭事件 onLyricWindowClosed: (callback: () => void) => { ipcRenderer.on('lyric-window-closed', () => callback()); diff --git a/src/renderer/api/lxMusicStrategy.ts b/src/renderer/api/lxMusicStrategy.ts new file mode 100644 index 0000000..f663f4b --- /dev/null +++ b/src/renderer/api/lxMusicStrategy.ts @@ -0,0 +1,174 @@ +/** + * 落雪音乐 (LX Music) 音源解析策略 + * + * 实现 MusicSourceStrategy 接口,作为落雪音源的解析入口 + */ + +import { getLxMusicRunner, initLxMusicRunner } from '@/services/LxMusicSourceRunner'; +import { useSettingsStore } from '@/store'; +import type { LxMusicInfo, LxQuality, LxSourceKey } from '@/types/lxMusic'; +import { LX_SOURCE_NAMES, QUALITY_TO_LX } from '@/types/lxMusic'; +import type { SongResult } from '@/types/music'; + +import type { MusicParseResult } from './musicParser'; +import { CacheManager } from './musicParser'; + +/** + * 将 SongResult 转换为 LxMusicInfo 格式 + */ +const convertToLxMusicInfo = (songResult: SongResult): LxMusicInfo => { + const artistName = + songResult.ar && songResult.ar.length > 0 + ? songResult.ar.map((a) => a.name).join('、') + : songResult.artists && songResult.artists.length > 0 + ? songResult.artists.map((a) => a.name).join('、') + : ''; + + const albumName = songResult.al?.name || (songResult.album as any)?.name || ''; + + const albumId = songResult.al?.id || (songResult.album as any)?.id || ''; + + // 计算时长(秒转分钟:秒格式) + const duration = songResult.dt || songResult.duration || 0; + const minutes = Math.floor(duration / 60000); + const seconds = Math.floor((duration % 60000) / 1000); + const interval = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + return { + songmid: songResult.id, + name: songResult.name, + singer: artistName, + album: albumName, + albumId, + source: 'wy', // 默认使用网易云作为源,因为我们的数据来自网易云 + interval, + img: songResult.picUrl || songResult.al?.picUrl || '' + }; +}; + +/** + * 获取最佳匹配的落雪音源 + * 因为我们的数据来自网易云,优先尝试 wy 音源 + */ +const getBestMatchingSource = ( + availableSources: LxSourceKey[], + _songSource?: string +): LxSourceKey | null => { + // 优先级顺序:网易云 > 酷我 > 咪咕 > 酷狗 > QQ音乐 + const priority: LxSourceKey[] = ['wy', 'kw', 'mg', 'kg', 'tx']; + + for (const source of priority) { + if (availableSources.includes(source)) { + return source; + } + } + + return availableSources[0] || null; +}; + +/** + * 落雪音乐解析策略 + */ +export class LxMusicStrategy { + name = 'lxMusic'; + priority = 0; // 最高优先级 + + /** + * 检查是否可以处理 + */ + canHandle(sources: string[], settingsStore?: any): boolean { + // 检查是否启用了落雪音源 + if (!sources.includes('lxMusic')) { + return false; + } + + // 检查是否导入了脚本 + const script = settingsStore?.setData?.lxMusicScript; + return Boolean(script); + } + + /** + * 解析音乐 URL + */ + async parse( + id: number, + data: SongResult, + quality?: string, + _sources?: string[] + ): Promise { + // 检查失败缓存 + if (CacheManager.isInFailedCache(id, this.name)) { + return null; + } + + try { + const settingsStore = useSettingsStore(); + const script = settingsStore.setData?.lxMusicScript; + + if (!script) { + console.log('[LxMusicStrategy] 未导入落雪音源脚本'); + return null; + } + + // 获取或初始化执行器 + let runner = getLxMusicRunner(); + if (!runner || !runner.isInitialized()) { + console.log('[LxMusicStrategy] 初始化落雪音源执行器...'); + runner = await initLxMusicRunner(script); + } + + // 获取可用音源 + const sources = runner.getSources(); + const availableSourceKeys = Object.keys(sources) as LxSourceKey[]; + + if (availableSourceKeys.length === 0) { + console.log('[LxMusicStrategy] 没有可用的落雪音源'); + CacheManager.addFailedCache(id, this.name); + return null; + } + + // 选择最佳音源 + const bestSource = getBestMatchingSource(availableSourceKeys); + if (!bestSource) { + console.log('[LxMusicStrategy] 无法找到匹配的音源'); + CacheManager.addFailedCache(id, this.name); + return null; + } + + console.log(`[LxMusicStrategy] 使用音源: ${LX_SOURCE_NAMES[bestSource]} (${bestSource})`); + + // 转换歌曲信息 + const lxMusicInfo = convertToLxMusicInfo(data); + + // 转换音质 + const lxQuality: LxQuality = QUALITY_TO_LX[quality || 'higher'] || '320k'; + + // 获取音乐 URL + const url = await runner.getMusicUrl(bestSource, lxMusicInfo, lxQuality); + + if (!url) { + console.log('[LxMusicStrategy] 获取 URL 失败'); + CacheManager.addFailedCache(id, this.name); + return null; + } + + console.log('[LxMusicStrategy] 解析成功:', url.substring(0, 50) + '...'); + + return { + data: { + code: 200, + message: 'success', + data: { + url, + source: `lx-${bestSource}`, + quality: lxQuality + } + } + }; + } catch (error) { + console.error('[LxMusicStrategy] 解析失败:', error); + CacheManager.addFailedCache(id, this.name); + return null; + } + } +} diff --git a/src/renderer/api/musicParser.ts b/src/renderer/api/musicParser.ts index 7be2358..2b6ac71 100644 --- a/src/renderer/api/musicParser.ts +++ b/src/renderer/api/musicParser.ts @@ -10,6 +10,7 @@ import requestMusic from '@/utils/request_music'; import { searchAndGetBilibiliAudioUrl } from './bilibili'; import type { ParsedMusicResult } from './gdmusic'; import { parseFromGDMusic } from './gdmusic'; +import { LxMusicStrategy } from './lxMusicStrategy'; import { parseFromCustomApi } from './parseFromCustomApi'; const { saveData, getData, deleteData } = musicDB; @@ -499,6 +500,7 @@ class UnblockMusicStrategy implements MusicSourceStrategy { */ class MusicSourceStrategyFactory { private static strategies: MusicSourceStrategy[] = [ + new LxMusicStrategy(), new CustomApiStrategy(), new BilibiliStrategy(), new GDMusicStrategy(), diff --git a/src/renderer/components/player/ReparsePopover.vue b/src/renderer/components/player/ReparsePopover.vue index eb2cf6a..1f02f38 100644 --- a/src/renderer/components/player/ReparsePopover.vue +++ b/src/renderer/components/player/ReparsePopover.vue @@ -122,7 +122,8 @@ const getSourceIcon = (source: Platform) => { pyncmd: 'ri-netease-cloud-music-fill', bilibili: 'ri-bilibili-fill', gdmusic: 'ri-google-fill', - kuwo: 'ri-music-fill' + kuwo: 'ri-music-fill', + lxMusic: 'ri-leaf-fill' }; return iconMap[source] || 'ri-music-2-fill'; diff --git a/src/renderer/components/settings/MusicSourceSettings.vue b/src/renderer/components/settings/MusicSourceSettings.vue index 423cf1d..1c4a8a6 100644 --- a/src/renderer/components/settings/MusicSourceSettings.vue +++ b/src/renderer/components/settings/MusicSourceSettings.vue @@ -10,107 +10,262 @@ @negative-click="handleCancel" style="width: 800px; max-width: 90vw" > - -

- {{ t('settings.playback.musicSourcesDesc') }} -

- - -
-
-
-
-
- {{ source.key }} - - - -
-

- {{ source.description }} +

+ + + + +

+ {{ t('settings.playback.musicSourcesDesc') }}

-
-
- -
-
-
-
- {{ - t('settings.playback.sourceLabels.custom') - }} - - - + +
+
+
+
+
+ {{ source.key }} + + + +
+

+ {{ source.description }} +

+
+
+ + +
+
+
+
+ 落雪音源 + + + +
+

+ {{ + settingsStore.setData.lxMusicScript + ? lxMusicScriptInfo?.name || '已导入' + : '未导入 (请去落雪音源Tab配置)' + }} +

+
+
+ + +
+
+
+
+ {{ + t('settings.playback.sourceLabels.custom') + }} + + + +
+

+ {{ + settingsStore.setData.customApiPlugin + ? t('settings.playback.customApi.status.imported') + : t('settings.playback.customApi.status.notImported') + }} +

+
+
+
+ + + + + +
+
+

已导入的音源脚本

+
+ + + 本地导入 + +
+
+ + +
+
+
+ +
+
+
+ {{ + api.name + }} + + + + + +
+ v{{ api.info.version }} +
+
+ + + +
+
+
+
+ +
+ + +
+

在线导入

+
+ + + + 导入 + +
-

- {{ - settingsStore.setData.customApiPlugin - ? t('settings.playback.customApi.status.imported') - : t('settings.playback.customApi.status.notImported') - }} -

-
-
+ - -
+ + +
+
+

+ {{ t('settings.playback.customApi.sectionTitle') }} +

+

导入兼容的自定义 API 插件以扩展音源

+
- -
-

- {{ t('settings.playback.customApi.sectionTitle') }} -

-
- - - {{ t('settings.playback.customApi.importConfig') }} - -

- {{ t('settings.playback.customApi.currentSource') }}: - {{ settingsStore.setData.customApiPluginName }} -

-

- {{ t('settings.playback.customApi.notImported') }} -

-
-
- +
+ + + {{ t('settings.playback.customApi.importConfig') }} + + +

+ + {{ t('settings.playback.customApi.currentSource') }}: + {{ settingsStore.setData.customApiPluginName }} +

+

+ {{ t('settings.playback.customApi.notImported') }} +

+
+
+
+ +