diff --git a/public/img/icons/menu-dark@88.png b/public/img/icons/menu-dark@88.png new file mode 100644 index 0000000..a2feb00 Binary files /dev/null and b/public/img/icons/menu-dark@88.png differ diff --git a/public/img/icons/menu@88.png b/public/img/icons/menu-light@88.png similarity index 100% rename from public/img/icons/menu@88.png rename to public/img/icons/menu-light@88.png diff --git a/src/background.js b/src/background.js index 1d4fdba..94dbbc0 100644 --- a/src/background.js +++ b/src/background.js @@ -31,7 +31,8 @@ import { EventEmitter } from 'events'; import express from 'express'; import expressProxy from 'express-http-proxy'; import Store from 'electron-store'; -import { createMpris } from '@/electron/mpris'; +import { createMpris, createDbus } from '@/electron/mpris'; +import { spawn } from 'child_process'; const clc = require('cli-color'); const log = text => { console.log(`${clc.blueBright('[background.js]')} ${text}`); @@ -420,6 +421,21 @@ class Background { registerGlobalShortcut(this.window, this.store); } + // try to start osdlyrics process on start + if (this.store.get('settings.enableOsdlyricsSupport')) { + await createDbus(this.window); + log('try to start osdlyrics process'); + const osdlyricsProcess = spawn('osdlyrics'); + + osdlyricsProcess.on('error', err => { + log(`failed to start osdlyrics: ${err.message}`); + }); + + osdlyricsProcess.on('exit', (code, signal) => { + log(`osdlyrics process exited with code ${code}, signal ${signal}`); + }); + } + // create mpris if (isCreateMpris) { createMpris(this.window); diff --git a/src/electron/mpris.js b/src/electron/mpris.js index f1272d7..c6cfeee 100644 --- a/src/electron/mpris.js +++ b/src/electron/mpris.js @@ -1,3 +1,4 @@ +import dbus from 'dbus-next'; import { ipcMain, app } from 'electron'; export function createMpris(window) { @@ -28,6 +29,8 @@ export function createMpris(window) { }); ipcMain.on('metadata', (e, metadata) => { + // 更新 Mpris 状态前将位置设为0, 否则 OSDLyrics 获取到的进度是上首音乐切换时的进度 + player.getPosition = () => 0; player.metadata = { 'mpris:trackid': player.objectPath('track/' + metadata.trackId), 'mpris:artUrl': metadata.artwork[0].src, @@ -41,6 +44,11 @@ export function createMpris(window) { ipcMain.on('playerCurrentTrackTime', (e, position) => { player.getPosition = () => position * 1000 * 1000; + player.seeked(position * 1000 * 1000); + }); + + ipcMain.on('seeked', (e, position) => { + player.seeked(position * 1000 * 1000); }); ipcMain.on('switchRepeatMode', (e, mode) => { @@ -61,3 +69,26 @@ export function createMpris(window) { player.shuffle = shuffle; }); } + +export async function createDbus(window) { + const bus = dbus.sessionBus(); + const Variant = dbus.Variant; + + const osdService = await bus.getProxyObject( + 'org.osdlyrics.Daemon', + '/org/osdlyrics/Lyrics' + ); + + const osdInterface = osdService.getInterface('org.osdlyrics.Lyrics'); + + ipcMain.on('sendLyrics', async (e, { track, lyrics }) => { + const metadata = { + title: new Variant('s', track.name), + artist: new Variant('s', track.ar.map(ar => ar.name).join(', ')), + }; + + await osdInterface.SetLyricContent(metadata, Buffer.from(lyrics)); + + window.webContents.send('saveLyricFinished'); + }); +} diff --git a/src/electron/tray.js b/src/electron/tray.js index ba74d3c..ed314f2 100644 --- a/src/electron/tray.js +++ b/src/electron/tray.js @@ -1,6 +1,6 @@ /* global __static */ import path from 'path'; -import { app, nativeImage, Tray, Menu } from 'electron'; +import { app, nativeImage, Tray, Menu, nativeTheme } from 'electron'; import { isLinux } from '@/utils/platform'; function createMenuTemplate(win) { @@ -197,8 +197,11 @@ class YPMTrayWindowsImpl { } export function createTray(win, eventEmitter) { + // 感觉图标颜色应该不属于界面主题范畴,只需要跟随系统主题 + let iconTheme = nativeTheme.shouldUseDarkColors ? 'light' : 'dark'; + let icon = nativeImage - .createFromPath(path.join(__static, 'img/icons/menu@88.png')) + .createFromPath(path.join(__static, `img/icons/menu-${iconTheme}@88.png`)) .resize({ height: 20, width: 20, diff --git a/src/locale/lang/en.js b/src/locale/lang/en.js index 4a63758..6728b8a 100644 --- a/src/locale/lang/en.js +++ b/src/locale/lang/en.js @@ -193,6 +193,13 @@ export default { exit: 'Exit', minimizeToTray: 'Minimize to tray', }, + enableOsdlyricsSupport: { + title: 'desktop lyrics support', + desc1: + 'Only takes effect under Linux. After enabled, it downloads the lyrics file to the local, and tries to launch OSDLyrics at startup.', + desc2: + 'Please ensure that you have installed OSDLyrics before turning on this.', + }, unm: { enable: 'Enable', audioSource: { diff --git a/src/locale/lang/zh-CN.js b/src/locale/lang/zh-CN.js index 266fead..9fbcafc 100644 --- a/src/locale/lang/zh-CN.js +++ b/src/locale/lang/zh-CN.js @@ -194,6 +194,12 @@ export default { exit: '退出', minimizeToTray: '最小化到托盘', }, + enableOsdlyricsSupport: { + title: '桌面歌词支持', + desc1: + '仅 Linux 下生效。启用后会将歌词文件下载到本地,并在开启播放器时尝试拉起 OSDLyrics。', + desc2: '请在开启之前确保您已经正确安装了 OSDLyrics。', + }, unm: { enable: '启用', audioSource: { diff --git a/src/locale/lang/zh-TW.js b/src/locale/lang/zh-TW.js index 6434199..0882921 100644 --- a/src/locale/lang/zh-TW.js +++ b/src/locale/lang/zh-TW.js @@ -191,6 +191,12 @@ export default { exit: '退出', minimizeToTray: '最小化到工作列角落', }, + enableOsdlyricsSupport: { + title: '桌面歌詞支援', + desc1: + '只在 Linux 環境下生效。啟用後會將歌詞檔案下載至本機位置,並在開啟播放器時嘗試連帶啟動 OSDLyrics。', + desc2: '請在開啟之前確保您已經正確安裝了 OSDLyrics。', + }, unm: { enable: '啟用', audioSource: { diff --git a/src/store/initLocalStorage.js b/src/store/initLocalStorage.js index d5b3005..ce0e802 100644 --- a/src/store/initLocalStorage.js +++ b/src/store/initLocalStorage.js @@ -23,6 +23,7 @@ let localStorage = { nyancatStyle: false, showLyricsTranslation: true, lyricsBackground: true, + enableOsdlyricsSupport: false, closeAppOption: 'ask', enableDiscordRichPresence: false, enableGlobalShortcut: true, diff --git a/src/utils/Player.js b/src/utils/Player.js index c4b6992..4e64e91 100644 --- a/src/utils/Player.js +++ b/src/utils/Player.js @@ -3,7 +3,7 @@ import { getArtist } from '@/api/artist'; import { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm'; import { fmTrash, personalFM } from '@/api/others'; import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist'; -import { getMP3, getTrackDetail, scrobble } from '@/api/track'; +import { getLyric, getMP3, getTrackDetail, scrobble } from '@/api/track'; import store from '@/store'; import { isAccountLoggedIn } from '@/utils/auth'; import { cacheTrackSource, getTrackSource } from '@/utils/db'; @@ -201,6 +201,9 @@ export default class { set progress(value) { if (this._howler) { this._howler.seek(value); + if (isCreateMpris) { + ipcRenderer?.send('seeked', this._howler.seek()); + } } } get isCurrentTrackLiked() { @@ -622,9 +625,30 @@ export default class { navigator.mediaSession.metadata = new window.MediaMetadata(metadata); if (isCreateMpris) { - ipcRenderer?.send('metadata', metadata); + this._updateMprisState(track, metadata); } } + // OSDLyrics 会检测 Mpris 状态并寻找对应歌词文件,所以要在更新 Mpris 状态之前保证歌词下载完成 + async _updateMprisState(track, metadata) { + if (!store.state.settings.enableOsdlyricsSupport) { + return ipcRenderer?.send('metadata', metadata); + } + + let lyricContent = await getLyric(track.id); + + if (!lyricContent.lrc || !lyricContent.lrc.lyric) { + return ipcRenderer?.send('metadata', metadata); + } + + ipcRenderer.send('sendLyrics', { + track, + lyrics: lyricContent.lrc.lyric, + }); + + ipcRenderer.on('saveLyricFinished', () => { + ipcRenderer?.send('metadata', metadata); + }); + } _updateMediaSessionPositionState() { if ('mediaSession' in navigator === false) { return; @@ -822,11 +846,14 @@ export default class { this.play(); } } - seek(time = null) { + seek(time = null, sendMpris = true) { + if (isCreateMpris && sendMpris && time) { + ipcRenderer?.send('seeked', time); + } if (time !== null) { this._howler?.seek(time); if (this._playing) - this._playDiscordPresence(this._currentTrack, this.seek()); + this._playDiscordPresence(this._currentTrack, this.seek(null, false)); } return this._howler === null ? 0 : this._howler.seek(); } diff --git a/src/views/lyrics.vue b/src/views/lyrics.vue index 40acc7f..a46695c 100644 --- a/src/views/lyrics.vue +++ b/src/views/lyrics.vue @@ -566,7 +566,7 @@ export default { }, setLyricsInterval() { this.lyricsInterval = setInterval(() => { - const progress = this.player.seek() ?? 0; + const progress = this.player.seek(null, false) ?? 0; let oldHighlightLyricIndex = this.highlightLyricIndex; this.highlightLyricIndex = this.lyric.findIndex((l, index) => { const nextLyric = this.lyric[index + 1]; diff --git a/src/views/settings.vue b/src/views/settings.vue index 2fe7604..82ad74c 100644 --- a/src/views/settings.vue +++ b/src/views/settings.vue @@ -251,6 +251,33 @@ +
+
+
+ {{ $t('settings.unm.enable') }} + OSDLyrics + {{ $t('settings.enableOsdlyricsSupport.title') }} +
+
+ {{ $t('settings.enableOsdlyricsSupport.desc1') }} +
+ {{ $t('settings.enableOsdlyricsSupport.desc2') }} +
+
+
+
+ + +
+
+

UnblockNeteaseMusic

@@ -975,6 +1002,17 @@ export default { }); }, }, + enableOsdlyricsSupport: { + get() { + return this.settings.enableOsdlyricsSupport; + }, + set(value) { + this.$store.commit('updateSettings', { + key: 'enableOsdlyricsSupport', + value, + }); + }, + }, closeAppOption: { get() { return this.settings.closeAppOption; @@ -1477,6 +1515,7 @@ h3 { select { min-width: 192px; + max-width: 600px; font-weight: 600; border: none; padding: 8px 12px 8px 12px;