From 33fc4f768cb81225ee9a9b3940442bed677ff846 Mon Sep 17 00:00:00 2001 From: alger Date: Sun, 29 Mar 2026 13:30:36 +0800 Subject: [PATCH 1/3] =?UTF-8?q?1.=20=E5=AE=9E=E7=8E=B0linux=E4=B8=8B?= =?UTF-8?q?=E7=9A=84mpris=E5=92=8Cgnome=E7=8A=B6=E6=80=81=E6=A0=8F?= =?UTF-8?q?=E6=AD=8C=E8=AF=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 + src/main/index.ts | 11 ++ src/main/modules/mpris.ts | 269 +++++++++++++++++++++++++++++ src/renderer/hooks/MusicHook.ts | 53 ++++++ src/renderer/utils/appShortcuts.ts | 15 ++ 5 files changed, 354 insertions(+) create mode 100644 src/main/modules/mpris.ts diff --git a/package.json b/package.json index 8caa6e7..b873b8a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", + "@httptoolkit/dbus-native": "^0.1.5", "@unblockneteasemusic/server": "^0.27.10", "cors": "^2.8.5", "crypto-js": "^4.2.0", @@ -49,6 +50,7 @@ "form-data": "^4.0.5", "husky": "^9.1.7", "jsencrypt": "^3.5.4", + "mpris-service": "^2.1.2", "music-metadata": "^11.10.3", "netease-cloud-music-api-alger": "^4.30.0", "node-fetch": "^2.7.0", @@ -221,5 +223,9 @@ "electron", "esbuild" ] + }, + "optionalDependencies": { + "jsbi": "^4.3.2", + "x11": "^2.3.0" } } diff --git a/src/main/index.ts b/src/main/index.ts index b47e8e4..40190df 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,6 +13,7 @@ import { initializeFonts } from './modules/fonts'; import { initializeLocalMusicScanner } from './modules/localMusicScanner'; import { initializeLoginWindow } from './modules/loginWindow'; import { initLxMusicHttp } from './modules/lxMusicHttp'; +import { initializeMpris, updateMprisCurrentSong, updateMprisPlayState } from './modules/mpris'; import { initializeOtherApi } from './modules/otherApi'; import { initializeRemoteControl } from './modules/remoteControl'; import { initializeShortcuts } from './modules/shortcuts'; @@ -82,6 +83,9 @@ function initialize(configStore: any) { // 初始化远程控制服务 initializeRemoteControl(mainWindow); + // 初始化 MPRIS 服务 (Linux) + initializeMpris(mainWindow); + // 初始化更新处理程序 setupUpdateHandlers(mainWindow); } @@ -92,6 +96,11 @@ const isSingleInstance = app.requestSingleInstanceLock(); if (!isSingleInstance) { app.quit(); } else { + // 禁用 Chromium 内置的 MediaSession MPRIS 服务,避免重复显示 + if (process.platform === 'linux') { + app.commandLine.appendSwitch('disable-features', 'MediaSessionService'); + } + // 在应用准备就绪前初始化GPU加速设置 // 必须在 app.ready 之前调用 disableHardwareAcceleration try { @@ -171,11 +180,13 @@ if (!isSingleInstance) { // 监听播放状态变化 ipcMain.on('update-play-state', (_, playing: boolean) => { updatePlayState(playing); + updateMprisPlayState(playing); }); // 监听当前歌曲变化 ipcMain.on('update-current-song', (_, song: any) => { updateCurrentSong(song); + updateMprisCurrentSong(song); }); // 所有窗口关闭时的处理 diff --git a/src/main/modules/mpris.ts b/src/main/modules/mpris.ts new file mode 100644 index 0000000..92a4903 --- /dev/null +++ b/src/main/modules/mpris.ts @@ -0,0 +1,269 @@ +import { app, BrowserWindow, ipcMain } from 'electron'; +import Player from 'mpris-service'; +const dbus = require('@httptoolkit/dbus-native'); + +interface SongInfo { + id?: number | string; + name: string; + picUrl?: string; + ar?: Array<{ name: string }>; + artists?: Array<{ name: string }>; + al?: { name: string }; + album?: { name: string }; + duration?: number; + dt?: number; + song?: { + artists?: Array<{ name: string }>; + album?: { name: string }; + duration?: number; + picUrl?: string; + }; + [key: string]: any; +} + +let mprisPlayer: Player | null = null; +let mainWindow: BrowserWindow | null = null; +let currentPosition = 0; +let trayLyricIface: any = null; +let trayLyricBus: any = null; + +export function initializeMpris(mainWindowRef: BrowserWindow) { + if (process.platform !== 'linux') return; + + if (mprisPlayer) { + console.log('[MPRIS] Already initialized, skipping'); + return; + } + + mainWindow = mainWindowRef; + + try { + mprisPlayer = Player({ + name: 'AlgerMusicPlayer', + identity: 'Alger Music Player', + supportedUriSchemes: ['file', 'http', 'https'], + supportedMimeTypes: [ + 'audio/mpeg', + 'audio/mp3', + 'audio/flac', + 'audio/wav', + 'audio/ogg', + 'audio/aac', + 'audio/m4a' + ], + supportedInterfaces: ['player'] + }); + + mprisPlayer.on('quit', () => { + app.quit(); + }); + + mprisPlayer.on('raise', () => { + if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); + } + }); + + mprisPlayer.on('next', () => { + if (mainWindow) { + mainWindow.webContents.send('global-shortcut', 'nextPlay'); + } + }); + + mprisPlayer.on('previous', () => { + if (mainWindow) { + mainWindow.webContents.send('global-shortcut', 'prevPlay'); + } + }); + + mprisPlayer.on('pause', () => { + if (mainWindow) { + mainWindow.webContents.send('global-shortcut', 'togglePlay'); + } + }); + + mprisPlayer.on('play', () => { + if (mainWindow) { + mainWindow.webContents.send('global-shortcut', 'togglePlay'); + } + }); + + mprisPlayer.on('playpause', () => { + if (mainWindow) { + mainWindow.webContents.send('global-shortcut', 'togglePlay'); + } + }); + + mprisPlayer.on('stop', () => { + if (mainWindow) { + mainWindow.webContents.send('global-shortcut', 'togglePlay'); + } + }); + + mprisPlayer.getPosition = (): number => { + console.log('[MPRIS] getPosition called, returning:', currentPosition); + return currentPosition; + }; + + mprisPlayer.on('seek', (offset: number) => { + if (mainWindow) { + const newPosition = Math.max(0, currentPosition + offset / 1000000); + mainWindow.webContents.send('mpris-seek', newPosition); + } + }); + + mprisPlayer.on('position', (event: { trackId: string; position: number }) => { + if (mainWindow) { + mainWindow.webContents.send('mpris-set-position', event.position / 1000000); + } + }); + + ipcMain.on('mpris-position-update', (_, position: number) => { + currentPosition = position * 1000 * 1000; + mprisPlayer.seeked(position * 1000 * 1000); + mprisPlayer.getPosition = () => position * 1000 * 1000; + mprisPlayer.position = position * 1000 * 1000; + }); + + ipcMain.on('tray-lyric-update', async (_, lrcObj: string) => { + sendTrayLyric(lrcObj); + }); + + initTrayLyric(); + + console.log('[MPRIS] Service initialized'); + } catch (error) { + console.error('[MPRIS] Failed to initialize:', error); + } +} + +export function updateMprisPlayState(playing: boolean) { + if (!mprisPlayer || process.platform !== 'linux') return; + mprisPlayer.playbackStatus = playing ? 'Playing' : 'Paused'; +} + +export function updateMprisCurrentSong(song: SongInfo | null) { + if (!mprisPlayer || process.platform !== 'linux') return; + + if (!song) { + mprisPlayer.metadata = {}; + mprisPlayer.playbackStatus = 'Stopped'; + return; + } + + const artists = + song.ar?.map((a) => a.name).join(', ') || + song.artists?.map((a) => a.name).join(', ') || + song.song?.artists?.map((a) => a.name).join(', ') || + ''; + const album = song.al?.name || song.album?.name || song.song?.album?.name || ''; + const duration = song.duration || song.dt || song.song?.duration || 0; + + mprisPlayer.metadata = { + 'mpris:trackid': mprisPlayer.objectPath(`track/${song.id || 0}`), + 'mpris:length': duration * 1000, + 'mpris:artUrl': song.picUrl || '', + 'xesam:title': song.name || '', + 'xesam:album': album, + 'xesam:artist': artists ? [artists] : [] + }; +} + +export function updateMprisPosition(position: number) { + if (!mprisPlayer || process.platform !== 'linux') return; + mprisPlayer.seeked(position * 1000000); +} + +export function destroyMpris() { + if (mprisPlayer) { + mprisPlayer.quit(); + mprisPlayer = null; + } +} + +function initTrayLyric() { + if (process.platform !== 'linux') { + console.log('[TrayLyric] Not Linux, skipping'); + return; + } + + console.log('[TrayLyric] Initializing...'); + + const serviceName = 'org.gnome.Shell.TrayLyric'; + + try { + const sessionBus = dbus.sessionBus({}); + trayLyricBus = sessionBus; + console.log('[TrayLyric] Session bus created, type:'); + + // 使用 invoke 方法调用 D-Bus 方法 + const dbusPath = '/org/freedesktop/DBus'; + const dbusInterface = 'org.freedesktop.DBus'; + + // 先尝试直接获取接口并使用 signals + sessionBus.invoke( + { + path: dbusPath, + interface: dbusInterface, + member: 'GetNameOwner', + destination: 'org.freedesktop.DBus', + signature: 's', + body: [serviceName] + }, + (err: any, result: any) => { + console.log('[TrayLyric] GetNameOwner result:', err, result); + if (err || !result) { + console.log('[TrayLyric] Service not running yet'); + } else { + console.log('[TrayLyric] Service is running, owner:', result[0]); + onServiceAvailable(); + } + } + ); + } catch (err) { + console.error('[TrayLyric] Exception during init:', err); + } + + function onServiceAvailable() { + console.log('[TrayLyric] onServiceAvailable called'); + if (!trayLyricBus) { + console.log('[TrayLyric] Bus not available'); + return; + } + console.log('[TrayLyric] Getting service interface...'); + const path = '/' + serviceName.replace(/\./g, '/'); + trayLyricBus.getService(serviceName).getInterface(path, serviceName, (err: any, iface: any) => { + if (err) { + console.error('[TrayLyric] Failed to get service interface:', err); + return; + } + trayLyricIface = iface; + console.log('[TrayLyric] Service interface ready'); + }); + } +} + +function sendTrayLyric(lrcObj: string) { + if (!trayLyricIface || !trayLyricBus) { + console.log('[TrayLyric] Interface or bus not ready, skipping'); + return; + } + + // 使用 invoke 方法调用 D-Bus 方法 + trayLyricBus.invoke( + { + path: '/org/gnome/Shell/TrayLyric', + interface: 'org.gnome.Shell.TrayLyric', + member: 'UpdateLyric', + destination: 'org.gnome.Shell.TrayLyric', + signature: 's', + body: [lrcObj] + }, + (err: any, _result: any) => { + if (err) { + console.error('[TrayLyric] Failed to invoke UpdateLyric:', err); + } + } + ); +} diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 145e25e..06d6d36 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -52,6 +52,8 @@ export const textColors = ref(getTextColors()); export let playMusic: ComputedRef; export let artistList: ComputedRef; +let lastIndex = -1; + export const musicDB = await useIndexedDB( 'musicDB', [ @@ -329,6 +331,12 @@ const setupAudioListeners = () => { sendLyricToWin(); } } + if (isElectron && lrcArray.value[nowIndex.value]) { + if (lastIndex !== nowIndex.value) { + sendTrayLyric(nowIndex.value); + lastIndex = nowIndex.value; + } + } // === 逐字歌词行内进度 === const { start, end } = currentLrcTiming.value; @@ -372,6 +380,15 @@ const setupAudioListeners = () => { ); } } + + // === MPRIS 进度更新(每 ~1 秒)=== + if (isElectron && lyricThrottleCounter % 20 === 0) { + try { + window.electron.ipcRenderer.send('mpris-position-update', currentTime); + } catch { + // 忽略发送失败 + } + } } catch (error) { console.error('进度更新 interval 出错:', error); // 出错时不清除 interval,让下一次 tick 继续尝试 @@ -420,6 +437,11 @@ const setupAudioListeners = () => { if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) { nowTime.value = currentTime; + // === MPRIS seek 时同步进度 === + if (isElectron) { + window.electron.ipcRenderer.send('mpris-position-update', currentTime); + } + // 检查是否需要更新歌词 const newIndex = getLrcIndex(nowTime.value); if (newIndex !== nowIndex.value) { @@ -807,6 +829,37 @@ export const sendLyricToWin = () => { } }; +// 发送歌词到系统托盘歌词(TrayLyric) +const sendTrayLyric = (index: number) => { + const platformValue = window.electron.ipcRenderer.sendSync('get-platform'); + console.log( + '[TrayLyric] sendTrayLyric called, isElectron:', + isElectron, + 'platform:', + platformValue + ); + if (!isElectron || platformValue !== 'linux') return; + + try { + const lyric = lrcArray.value[index]; + if (!lyric) return; + + const currentTime = lrcTimeArray.value[index] || 0; + const nextTime = lrcTimeArray.value[index + 1] || currentTime + 3; + const duration = nextTime - currentTime; + + const lrcObj = JSON.stringify({ + content: lyric.text || '', + time: duration.toFixed(1), + sender: 'AlgerMusicPlayer' + }); + + window.electron.ipcRenderer.send('tray-lyric-update', lrcObj); + } catch (error) { + console.error('[TrayLyric] Failed to send:', error); + } +}; + // 歌词同步定时器 let lyricSyncInterval: any = null; diff --git a/src/renderer/utils/appShortcuts.ts b/src/renderer/utils/appShortcuts.ts index 491a19f..a85cbeb 100644 --- a/src/renderer/utils/appShortcuts.ts +++ b/src/renderer/utils/appShortcuts.ts @@ -1,6 +1,7 @@ import { onMounted, onUnmounted } from 'vue'; import i18n from '@/../i18n/renderer'; +import { audioService } from '@/services/audioService'; import { usePlayerStore, useSettingsStore } from '@/store'; import { @@ -37,6 +38,18 @@ const onUpdateAppShortcuts = (_event: unknown, shortcuts: unknown) => { updateAppShortcuts(shortcuts); }; +const onMprisSeek = (_event: unknown, position: number) => { + if (audioService) { + audioService.seek(position); + } +}; + +const onMprisSetPosition = (_event: unknown, position: number) => { + if (audioService) { + audioService.seek(position); + } +}; + function shouldSkipAction(action: ShortcutAction): boolean { const now = Date.now(); const lastTimestamp = actionTimestamps.get(action) ?? 0; @@ -192,6 +205,8 @@ export function initAppShortcuts() { window.electron.ipcRenderer.on('global-shortcut', onGlobalShortcut); window.electron.ipcRenderer.on('update-app-shortcuts', onUpdateAppShortcuts); + window.electron.ipcRenderer.on('mpris-seek', onMprisSeek); + window.electron.ipcRenderer.on('mpris-set-position', onMprisSetPosition); const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts'); updateAppShortcuts(storedShortcuts); From 3f31278131e858de12da3c2ca2b522365247797d Mon Sep 17 00:00:00 2001 From: stark81 <849966181@qq.com> Date: Sat, 11 Apr 2026 16:01:06 +0800 Subject: [PATCH 2/3] fix-sandbox --- fix-sandbox.js | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 fix-sandbox.js diff --git a/fix-sandbox.js b/fix-sandbox.js new file mode 100644 index 0000000..cca78cd --- /dev/null +++ b/fix-sandbox.js @@ -0,0 +1,9 @@ +const { execSync } = require('child_process'); + +if (process.platform === 'linux') { + // You need to make sure that + // /home/runner/work/VutronMusic/VutronMusic/node_modules/electron/dist/chrome-sandbox + // is owned by root and has mode 4755. + execSync('sudo chown root:root ./node_modules/electron/dist/chrome-sandbox'); + execSync('sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox'); +} diff --git a/package.json b/package.json index b873b8a..e5419c5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dev": "electron-vite dev", "dev:web": "vite dev", "build": "electron-vite build", - "postinstall": "electron-builder install-app-deps", + "postinstall": "node fix-sandbox.js && electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "build:win": "npm run build && electron-builder --win --publish never", "build:mac": "npm run build && electron-builder --mac --x64 --publish never && cp dist/latest-mac.yml dist/latest-mac-x64.yml && electron-builder --mac --arm64 --publish never && cp dist/latest-mac.yml dist/latest-mac-arm64.yml && node scripts/merge_latest_mac_yml.mjs dist/latest-mac-x64.yml dist/latest-mac-arm64.yml dist/latest-mac.yml", From 030a1f1c8546c9afcc9f2ef237fcb4625e4d0941 Mon Sep 17 00:00:00 2001 From: alger Date: Sat, 11 Apr 2026 22:37:26 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(mpris):=20=E4=BF=AE=E5=A4=8D=20MPRIS=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=A4=9A=E9=A1=B9=E5=AE=89=E5=85=A8=E5=92=8C?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 fix-sandbox.js 从 postinstall 移除,避免 npm install 时执行 sudo - 修复 play/pause/stop 事件语义错误,不再全部映射到 togglePlay - 缓存平台信息避免 sendSync 阻塞渲染进程 - 修复 cleanupAppShortcuts 中缺少 MPRIS 监听器清理导致的事件泄漏 - destroyMpris 中添加 IPC 监听器清理 - 清理冗余调试日志,安全加载 dbus-native 模块 - 添加 mpris-service 类型声明解决跨平台类型检查问题 --- fix-sandbox.js | 22 +++++++-- package.json | 3 +- src/main/modules/mpris.ts | 77 +++++++++++++++--------------- src/main/types/mpris-service.d.ts | 23 +++++++++ src/renderer/hooks/MusicHook.ts | 12 ++--- src/renderer/utils/appShortcuts.ts | 26 +++++++--- 6 files changed, 105 insertions(+), 58 deletions(-) create mode 100644 src/main/types/mpris-service.d.ts diff --git a/fix-sandbox.js b/fix-sandbox.js index cca78cd..2e99251 100644 --- a/fix-sandbox.js +++ b/fix-sandbox.js @@ -1,9 +1,21 @@ +/** + * 修复 Linux 下 Electron sandbox 权限问题 + * chrome-sandbox 需要 root 拥有且权限为 4755 + * + * 注意:此脚本需要 sudo 权限,仅在 CI 环境或手动执行时使用 + * 用法:sudo node fix-sandbox.js + */ const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); if (process.platform === 'linux') { - // You need to make sure that - // /home/runner/work/VutronMusic/VutronMusic/node_modules/electron/dist/chrome-sandbox - // is owned by root and has mode 4755. - execSync('sudo chown root:root ./node_modules/electron/dist/chrome-sandbox'); - execSync('sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox'); + const sandboxPath = path.resolve('./node_modules/electron/dist/chrome-sandbox'); + if (fs.existsSync(sandboxPath)) { + execSync(`sudo chown root:root ${sandboxPath}`); + execSync(`sudo chmod 4755 ${sandboxPath}`); + console.log('[fix-sandbox] chrome-sandbox permissions fixed'); + } else { + console.log('[fix-sandbox] chrome-sandbox not found, skipping'); + } } diff --git a/package.json b/package.json index e5419c5..2483a87 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "dev": "electron-vite dev", "dev:web": "vite dev", "build": "electron-vite build", - "postinstall": "node fix-sandbox.js && electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps", + "fix-sandbox": "node fix-sandbox.js", "build:unpack": "npm run build && electron-builder --dir", "build:win": "npm run build && electron-builder --win --publish never", "build:mac": "npm run build && electron-builder --mac --x64 --publish never && cp dist/latest-mac.yml dist/latest-mac-x64.yml && electron-builder --mac --arm64 --publish never && cp dist/latest-mac.yml dist/latest-mac-arm64.yml && node scripts/merge_latest_mac_yml.mjs dist/latest-mac-x64.yml dist/latest-mac-arm64.yml dist/latest-mac.yml", diff --git a/src/main/modules/mpris.ts b/src/main/modules/mpris.ts index 92a4903..f48e15c 100644 --- a/src/main/modules/mpris.ts +++ b/src/main/modules/mpris.ts @@ -1,6 +1,12 @@ import { app, BrowserWindow, ipcMain } from 'electron'; import Player from 'mpris-service'; -const dbus = require('@httptoolkit/dbus-native'); + +let dbusModule: any; +try { + dbusModule = require('@httptoolkit/dbus-native'); +} catch { + // dbus-native 不可用(非 Linux 环境) +} interface SongInfo { id?: number | string; @@ -27,11 +33,14 @@ let currentPosition = 0; let trayLyricIface: any = null; let trayLyricBus: any = null; +// 保存 IPC 处理函数引用,用于清理 +let onPositionUpdate: ((event: any, position: number) => void) | null = null; +let onTrayLyricUpdate: ((event: any, lrcObj: string) => void) | null = null; + export function initializeMpris(mainWindowRef: BrowserWindow) { if (process.platform !== 'linux') return; if (mprisPlayer) { - console.log('[MPRIS] Already initialized, skipping'); return; } @@ -79,13 +88,13 @@ export function initializeMpris(mainWindowRef: BrowserWindow) { mprisPlayer.on('pause', () => { if (mainWindow) { - mainWindow.webContents.send('global-shortcut', 'togglePlay'); + mainWindow.webContents.send('mpris-pause'); } }); mprisPlayer.on('play', () => { if (mainWindow) { - mainWindow.webContents.send('global-shortcut', 'togglePlay'); + mainWindow.webContents.send('mpris-play'); } }); @@ -97,12 +106,11 @@ export function initializeMpris(mainWindowRef: BrowserWindow) { mprisPlayer.on('stop', () => { if (mainWindow) { - mainWindow.webContents.send('global-shortcut', 'togglePlay'); + mainWindow.webContents.send('mpris-pause'); } }); mprisPlayer.getPosition = (): number => { - console.log('[MPRIS] getPosition called, returning:', currentPosition); return currentPosition; }; @@ -119,16 +127,20 @@ export function initializeMpris(mainWindowRef: BrowserWindow) { } }); - ipcMain.on('mpris-position-update', (_, position: number) => { + onPositionUpdate = (_, position: number) => { currentPosition = position * 1000 * 1000; - mprisPlayer.seeked(position * 1000 * 1000); - mprisPlayer.getPosition = () => position * 1000 * 1000; - mprisPlayer.position = position * 1000 * 1000; - }); + if (mprisPlayer) { + mprisPlayer.seeked(position * 1000 * 1000); + mprisPlayer.getPosition = () => position * 1000 * 1000; + mprisPlayer.position = position * 1000 * 1000; + } + }; + ipcMain.on('mpris-position-update', onPositionUpdate); - ipcMain.on('tray-lyric-update', async (_, lrcObj: string) => { + onTrayLyricUpdate = (_, lrcObj: string) => { sendTrayLyric(lrcObj); - }); + }; + ipcMain.on('tray-lyric-update', onTrayLyricUpdate); initTrayLyric(); @@ -176,6 +188,14 @@ export function updateMprisPosition(position: number) { } export function destroyMpris() { + if (onPositionUpdate) { + ipcMain.removeListener('mpris-position-update', onPositionUpdate); + onPositionUpdate = null; + } + if (onTrayLyricUpdate) { + ipcMain.removeListener('tray-lyric-update', onTrayLyricUpdate); + onTrayLyricUpdate = null; + } if (mprisPlayer) { mprisPlayer.quit(); mprisPlayer = null; @@ -183,25 +203,17 @@ export function destroyMpris() { } function initTrayLyric() { - if (process.platform !== 'linux') { - console.log('[TrayLyric] Not Linux, skipping'); - return; - } - - console.log('[TrayLyric] Initializing...'); + if (process.platform !== 'linux' || !dbusModule) return; const serviceName = 'org.gnome.Shell.TrayLyric'; try { - const sessionBus = dbus.sessionBus({}); + const sessionBus = dbusModule.sessionBus({}); trayLyricBus = sessionBus; - console.log('[TrayLyric] Session bus created, type:'); - // 使用 invoke 方法调用 D-Bus 方法 const dbusPath = '/org/freedesktop/DBus'; const dbusInterface = 'org.freedesktop.DBus'; - // 先尝试直接获取接口并使用 signals sessionBus.invoke( { path: dbusPath, @@ -212,26 +224,19 @@ function initTrayLyric() { body: [serviceName] }, (err: any, result: any) => { - console.log('[TrayLyric] GetNameOwner result:', err, result); if (err || !result) { - console.log('[TrayLyric] Service not running yet'); + console.log('[TrayLyric] Service not running'); } else { - console.log('[TrayLyric] Service is running, owner:', result[0]); onServiceAvailable(); } } ); } catch (err) { - console.error('[TrayLyric] Exception during init:', err); + console.error('[TrayLyric] Failed to init:', err); } function onServiceAvailable() { - console.log('[TrayLyric] onServiceAvailable called'); - if (!trayLyricBus) { - console.log('[TrayLyric] Bus not available'); - return; - } - console.log('[TrayLyric] Getting service interface...'); + if (!trayLyricBus) return; const path = '/' + serviceName.replace(/\./g, '/'); trayLyricBus.getService(serviceName).getInterface(path, serviceName, (err: any, iface: any) => { if (err) { @@ -245,12 +250,8 @@ function initTrayLyric() { } function sendTrayLyric(lrcObj: string) { - if (!trayLyricIface || !trayLyricBus) { - console.log('[TrayLyric] Interface or bus not ready, skipping'); - return; - } + if (!trayLyricIface || !trayLyricBus) return; - // 使用 invoke 方法调用 D-Bus 方法 trayLyricBus.invoke( { path: '/org/gnome/Shell/TrayLyric', diff --git a/src/main/types/mpris-service.d.ts b/src/main/types/mpris-service.d.ts new file mode 100644 index 0000000..bb92c97 --- /dev/null +++ b/src/main/types/mpris-service.d.ts @@ -0,0 +1,23 @@ +declare module 'mpris-service' { + interface PlayerOptions { + name: string; + identity: string; + supportedUriSchemes?: string[]; + supportedMimeTypes?: string[]; + supportedInterfaces?: string[]; + } + + interface Player { + on(event: string, callback: (...args: any[]) => void): void; + playbackStatus: string; + metadata: Record; + position: number; + getPosition: () => number; + seeked(position: number): void; + objectPath(path: string): string; + quit(): void; + } + + function Player(options: PlayerOptions): Player; + export = Player; +} diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 06d6d36..49a7b37 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -54,6 +54,9 @@ export let artistList: ComputedRef; let lastIndex = -1; +// 缓存平台信息,避免每次歌词变化时同步 IPC 调用 +const cachedPlatform = isElectron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web'; + export const musicDB = await useIndexedDB( 'musicDB', [ @@ -831,14 +834,7 @@ export const sendLyricToWin = () => { // 发送歌词到系统托盘歌词(TrayLyric) const sendTrayLyric = (index: number) => { - const platformValue = window.electron.ipcRenderer.sendSync('get-platform'); - console.log( - '[TrayLyric] sendTrayLyric called, isElectron:', - isElectron, - 'platform:', - platformValue - ); - if (!isElectron || platformValue !== 'linux') return; + if (!isElectron || cachedPlatform !== 'linux') return; try { const lyric = lrcArray.value[index]; diff --git a/src/renderer/utils/appShortcuts.ts b/src/renderer/utils/appShortcuts.ts index a85cbeb..819a3c9 100644 --- a/src/renderer/utils/appShortcuts.ts +++ b/src/renderer/utils/appShortcuts.ts @@ -38,15 +38,23 @@ const onUpdateAppShortcuts = (_event: unknown, shortcuts: unknown) => { updateAppShortcuts(shortcuts); }; -const onMprisSeek = (_event: unknown, position: number) => { +const onMprisSeekOrSetPosition = (_event: unknown, position: number) => { if (audioService) { audioService.seek(position); } }; -const onMprisSetPosition = (_event: unknown, position: number) => { - if (audioService) { - audioService.seek(position); +const onMprisPlay = async () => { + const playerStore = usePlayerStore(); + if (!playerStore.play && playerStore.playMusic?.id) { + await playerStore.setPlay({ ...playerStore.playMusic }); + } +}; + +const onMprisPause = async () => { + const playerStore = usePlayerStore(); + if (playerStore.play) { + await playerStore.handlePause(); } }; @@ -205,8 +213,10 @@ export function initAppShortcuts() { window.electron.ipcRenderer.on('global-shortcut', onGlobalShortcut); window.electron.ipcRenderer.on('update-app-shortcuts', onUpdateAppShortcuts); - window.electron.ipcRenderer.on('mpris-seek', onMprisSeek); - window.electron.ipcRenderer.on('mpris-set-position', onMprisSetPosition); + window.electron.ipcRenderer.on('mpris-seek', onMprisSeekOrSetPosition); + window.electron.ipcRenderer.on('mpris-set-position', onMprisSeekOrSetPosition); + window.electron.ipcRenderer.on('mpris-play', onMprisPlay); + window.electron.ipcRenderer.on('mpris-pause', onMprisPause); const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts'); updateAppShortcuts(storedShortcuts); @@ -226,6 +236,10 @@ export function cleanupAppShortcuts() { window.electron.ipcRenderer.removeListener('global-shortcut', onGlobalShortcut); window.electron.ipcRenderer.removeListener('update-app-shortcuts', onUpdateAppShortcuts); + window.electron.ipcRenderer.removeListener('mpris-seek', onMprisSeekOrSetPosition); + window.electron.ipcRenderer.removeListener('mpris-set-position', onMprisSeekOrSetPosition); + window.electron.ipcRenderer.removeListener('mpris-play', onMprisPlay); + window.electron.ipcRenderer.removeListener('mpris-pause', onMprisPause); document.removeEventListener('keydown', handleKeyDown); }