diff --git a/package-lock.json b/package-lock.json index 94b564dd..5689d9db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "dayjs": "^1.11.6", "electron-debug": "^3.2.0", "electron-localshortcut": "^3.2.1", - "electron-log": "^4.4.6", + "electron-log": "^5.1.1", "electron-store": "^8.1.0", "electron-updater": "^4.6.5", "fast-average-color": "^9.3.0", @@ -9098,9 +9098,12 @@ } }, "node_modules/electron-log": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.6.tgz", - "integrity": "sha512-nirYgRdY+F+vclr8ijdwy2vW03IzFpDHTaKNWu76dEN21Y76+smcES5knS7cgHUUB0qNLOi8vZO36taakjbSXA==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.1.1.tgz", + "integrity": "sha512-If7HU4Slbh2xfjOXOLxifkbgu6HmWDNJyXPLW+XNTOHMfFKisg0trA3d/7syyu25S+lHosfsd0VMfDSjGn1+Pw==", + "engines": { + "node": ">= 14" + } }, "node_modules/electron-notarize": { "version": "1.2.1", @@ -19963,9 +19966,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -28093,9 +28096,9 @@ } }, "electron-log": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.6.tgz", - "integrity": "sha512-nirYgRdY+F+vclr8ijdwy2vW03IzFpDHTaKNWu76dEN21Y76+smcES5knS7cgHUUB0qNLOi8vZO36taakjbSXA==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.1.1.tgz", + "integrity": "sha512-If7HU4Slbh2xfjOXOLxifkbgu6HmWDNJyXPLW+XNTOHMfFKisg0trA3d/7syyu25S+lHosfsd0VMfDSjGn1+Pw==" }, "electron-notarize": { "version": "1.2.1", @@ -36192,9 +36195,9 @@ } }, "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 4172ed9a..7be6527b 100644 --- a/package.json +++ b/package.json @@ -313,7 +313,7 @@ "dayjs": "^1.11.6", "electron-debug": "^3.2.0", "electron-localshortcut": "^3.2.1", - "electron-log": "^4.4.6", + "electron-log": "^5.1.1", "electron-store": "^8.1.0", "electron-updater": "^4.6.5", "fast-average-color": "^9.3.0", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 41a4528d..0b9e23e3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -84,6 +84,7 @@ "random": "random", "rating": "rating", "refresh": "refresh", + "reload": "reload", "reset": "reset", "resetToDefault": "reset to default", "restartRequired": "restart required", @@ -506,9 +507,9 @@ "minimumScrobbleSeconds": "minimum scrobble (seconds)", "minimumScrobbleSeconds_description": "the minimum duration in seconds of the song that must be played before it is scrobbled", "mpvExecutablePath": "mpv executable path", - "mpvExecutablePath_description": "sets the path to the mpv executable", - "mpvExecutablePath_help": "one per line", + "mpvExecutablePath_description": "sets the path to the mpv executable. if left empty, the default path will be used", "mpvExtraParameters": "mpv parameters", + "mpvExtraParameters_help": "one per line", "passwordStore": "passwords/secret store", "passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.", "playbackStyle": "playback style", diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts index facd1b1a..26307093 100644 --- a/src/main/features/core/player/index.ts +++ b/src/main/features/core/player/index.ts @@ -1,7 +1,11 @@ import console from 'console'; -import { ipcMain } from 'electron'; -import { getMpvInstance } from '../../../main'; +import { app, ipcMain } from 'electron'; +import uniq from 'lodash/uniq'; +import MpvAPI from 'node-mpv'; +import { getMainWindow, sendToastToRenderer } from '../../../main'; import { PlayerData } from '/@/renderer/store'; +import { createLog, isWindows } from '../../../utils'; +import { store } from '../settings'; declare module 'node-mpv'; @@ -13,6 +17,208 @@ declare module 'node-mpv'; // }); // } +let mpvInstance: MpvAPI | null = null; + +const NodeMpvErrorCode = { + 0: 'Unable to load file or stream', + 1: 'Invalid argument', + 2: 'Binary not found', + 3: 'IPC command invalid', + 4: 'Unable to bind IPC socket', + 5: 'Connection timeout', + 6: 'MPV is already running', + 7: 'Could not send IPC message', + 8: 'MPV is not running', + 9: 'Unsupported protocol', +}; + +type NodeMpvError = { + errcode: number; + method: string; + stackTrace: string; + verbose: string; +}; + +const mpvLog = ( + data: { action: string; toast?: 'info' | 'success' | 'warning' }, + err?: NodeMpvError, +) => { + const { action, toast } = data; + + if (err) { + const message = `[AUDIO PLAYER] ${action} - mpv errorcode ${err.errcode} - ${ + NodeMpvErrorCode[err.errcode as keyof typeof NodeMpvErrorCode] + }`; + + sendToastToRenderer({ message, type: 'error' }); + createLog({ message, type: 'error' }); + } + + const message = `[AUDIO PLAYER] ${action}`; + createLog({ message, type: 'error' }); + if (toast) { + sendToastToRenderer({ message, type: toast }); + } +}; + +const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined; +const isDevelopment = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; + +const prefetchPlaylistParams = [ + '--prefetch-playlist=no', + '--prefetch-playlist=yes', + '--prefetch-playlist', +]; + +const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => { + const parameters = ['--idle=yes', '--no-config', '--load-scripts=no']; + + if (!extraParameters?.some((param) => prefetchPlaylistParams.includes(param))) { + parameters.push('--prefetch-playlist=yes'); + } + + return parameters; +}; + +const createMpv = async (data: { + binaryPath?: string; + extraParameters?: string[]; + properties?: Record; +}): Promise => { + const { extraParameters, properties, binaryPath } = data; + + const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]); + + const extra = isDevelopment ? '-dev' : ''; + + const mpv = new MpvAPI( + { + audio_only: true, + auto_restart: false, + binary: binaryPath || MPV_BINARY_PATH || undefined, + socket: isWindows() ? `\\\\.\\pipe\\mpvserver${extra}` : `/tmp/node-mpv${extra}.sock`, + time_update: 1, + }, + params, + ); + + try { + await mpv.start(); + } catch (error: any) { + console.log('mpv failed to start', error); + } finally { + await mpv.setMultipleProperties(properties || {}); + } + + mpv.on('status', (status) => { + if (status.property === 'playlist-pos') { + if (status.value === -1) { + mpv?.stop(); + } + + if (status.value !== 0) { + getMainWindow()?.webContents.send('renderer-player-auto-next'); + } + } + }); + + // Automatically updates the play button when the player is playing + mpv.on('resumed', () => { + getMainWindow()?.webContents.send('renderer-player-play'); + }); + + // Automatically updates the play button when the player is stopped + mpv.on('stopped', () => { + getMainWindow()?.webContents.send('renderer-player-stop'); + }); + + // Automatically updates the play button when the player is paused + mpv.on('paused', () => { + getMainWindow()?.webContents.send('renderer-player-pause'); + }); + + // Event output every interval set by time_update, used to update the current time + mpv.on('timeposition', (time: number) => { + getMainWindow()?.webContents.send('renderer-player-current-time', time); + }); + + return mpv; +}; + +export const getMpvInstance = () => { + return mpvInstance; +}; + +const setAudioPlayerFallback = (isError: boolean) => { + getMainWindow()?.webContents.send('renderer-player-fallback', isError); +}; + +ipcMain.on('player-set-properties', async (_event, data: Record) => { + mpvLog({ action: `Setting properties: ${JSON.stringify(data)}` }); + if (data.length === 0) { + return; + } + + try { + if (data.length === 1) { + getMpvInstance()?.setProperty(Object.keys(data)[0], Object.values(data)[0]); + } else { + getMpvInstance()?.setMultipleProperties(data); + } + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to set properties: ${JSON.stringify(data)}` }, err); + } +}); + +ipcMain.handle( + 'player-restart', + async (_event, data: { extraParameters?: string[]; properties?: Record }) => { + try { + mpvLog({ + action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`, + }); + + // Clean up previous mpv instance + getMpvInstance()?.stop(); + getMpvInstance()?.quit(); + mpvInstance = null; + + mpvInstance = await createMpv(data); + mpvLog({ action: 'Restarted mpv', toast: 'success' }); + setAudioPlayerFallback(false); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to restart mpv, falling back to web player' }, err); + setAudioPlayerFallback(true); + } + }, +); + +ipcMain.handle( + 'player-initialize', + async (_event, data: { extraParameters?: string[]; properties?: Record }) => { + try { + mpvLog({ + action: `Attempting to initialize mpv with parameters: ${JSON.stringify(data)}`, + }); + mpvInstance = await createMpv(data); + setAudioPlayerFallback(false); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to initialize mpv, falling back to web player' }, err); + setAudioPlayerFallback(true); + } + }, +); + +ipcMain.on('player-quit', async () => { + try { + getMpvInstance()?.stop(); + getMpvInstance()?.quit(); + mpvInstance = null; + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to quit mpv' }, err); + } +}); + ipcMain.handle('player-is-running', async () => { return getMpvInstance()?.isRunning(); }); @@ -23,99 +229,93 @@ ipcMain.handle('player-clean-up', async () => { }); ipcMain.on('player-start', async () => { - await getMpvInstance() - ?.play() - .catch((err) => { - console.log('MPV failed to play', err); - }); + try { + await getMpvInstance()?.play(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to start mpv playback' }, err); + } }); // Starts the player ipcMain.on('player-play', async () => { - await getMpvInstance() - ?.play() - .catch((err) => { - console.log('MPV failed to play', err); - }); + try { + await getMpvInstance()?.play(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to start mpv playback' }, err); + } }); // Pauses the player ipcMain.on('player-pause', async () => { - await getMpvInstance() - ?.pause() - .catch((err) => { - console.log('MPV failed to pause', err); - }); + try { + await getMpvInstance()?.pause(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to pause mpv playback' }, err); + } }); // Stops the player ipcMain.on('player-stop', async () => { - await getMpvInstance() - ?.stop() - .catch((err) => { - console.log('MPV failed to stop', err); - }); + try { + await getMpvInstance()?.stop(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to stop mpv playback' }, err); + } }); // Goes to the next track in the playlist ipcMain.on('player-next', async () => { - await getMpvInstance() - ?.next() - .catch((err) => { - console.log('MPV failed to go to next', err); - }); + try { + await getMpvInstance()?.next(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to go to next track' }, err); + } }); // Goes to the previous track in the playlist ipcMain.on('player-previous', async () => { - await getMpvInstance() - ?.prev() - .catch((err) => { - console.log('MPV failed to go to previous', err); - }); + try { + await getMpvInstance()?.prev(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: 'Failed to go to previous track' }, err); + } }); // Seeks forward or backward by the given amount of seconds ipcMain.on('player-seek', async (_event, time: number) => { - await getMpvInstance() - ?.seek(time) - .catch((err) => { - console.log('MPV failed to seek', err); - }); + try { + await getMpvInstance()?.seek(time); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to seek by ${time} seconds` }, err); + } }); // Seeks to the given time in seconds ipcMain.on('player-seek-to', async (_event, time: number) => { - await getMpvInstance() - ?.goToPosition(time) - .catch((err) => { - console.log(`MPV failed to seek to ${time}`, err); - }); + try { + await getMpvInstance()?.goToPosition(time); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to seek to ${time} seconds` }, err); + } }); // Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => { if (!data.queue.current && !data.queue.next) { - await getMpvInstance() - ?.clearPlaylist() - .catch((err) => { - console.log('MPV failed to clear playlist', err); - }); - - await getMpvInstance() - ?.pause() - .catch((err) => { - console.log('MPV failed to pause', err); - }); - return; + try { + await getMpvInstance()?.clearPlaylist(); + await getMpvInstance()?.pause(); + return; + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to clear play queue` }, err); + } } try { if (data.queue.current) { await getMpvInstance() ?.load(data.queue.current.streamUrl, 'replace') - .catch((err) => { - console.log('MPV failed to load song', err); + .catch(() => { getMpvInstance()?.play(); }); @@ -123,8 +323,8 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); } } - } catch (err) { - console.error(err); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to set play queue` }, err); } if (pause) { @@ -134,30 +334,22 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) // Replaces the queue in position 1 to the given data ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => { - const size = await getMpvInstance() - ?.getPlaylistSize() - .catch((err) => { - console.log('MPV failed to get playlist size', err); - }); + try { + const size = await getMpvInstance()?.getPlaylistSize(); - if (!size) { - return; - } + if (!size) { + return; + } - if (size > 1) { - await getMpvInstance() - ?.playlistRemove(1) - .catch((err) => { - console.log('MPV failed to remove song from playlist', err); - }); - } + if (size > 1) { + await getMpvInstance()?.playlistRemove(1); + } - if (data.queue.next) { - await getMpvInstance() - ?.load(data.queue.next.streamUrl, 'append') - .catch((err) => { - console.log('MPV failed to load next song', err); - }); + if (data.queue.next) { + await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); + } + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to set play queue` }, err); } }); @@ -166,40 +358,57 @@ ipcMain.on('player-auto-next', async (_event, data: PlayerData) => { // Always keep the current song as position 0 in the mpv queue // This allows us to easily set update the next song in the queue without // disturbing the currently playing song - await getMpvInstance() - ?.playlistRemove(0) - .catch((err) => { - console.log('MPV failed to remove song from playlist', err); - getMpvInstance()?.pause(); - }); - - if (data.queue.next) { + try { await getMpvInstance() - ?.load(data.queue.next.streamUrl, 'append') - .catch((err) => { - console.log('MPV failed to load next song', err); + ?.playlistRemove(0) + .catch(() => { + getMpvInstance()?.pause(); }); + + if (data.queue.next) { + await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); + } + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to load next song` }, err); } }); // Sets the volume to the given value (0-100) ipcMain.on('player-volume', async (_event, value: number) => { - await getMpvInstance() - ?.volume(value) - .catch((err) => { - console.log('MPV failed to set volume', err); - }); + try { + if (!value || value < 0 || value > 100) { + return; + } + + await getMpvInstance()?.volume(value); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to set volume to ${value}` }, err); + } }); // Toggles the mute status ipcMain.on('player-mute', async (_event, mute: boolean) => { - await getMpvInstance() - ?.mute(mute) - .catch((err) => { - console.log('MPV failed to toggle mute', err); - }); + try { + await getMpvInstance()?.mute(mute); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to set mute status` }, err); + } }); ipcMain.handle('player-get-time', async (): Promise => { - return getMpvInstance()?.getTimePosition(); + try { + return getMpvInstance()?.getTimePosition(); + } catch (err: NodeMpvError | any) { + mpvLog({ action: `Failed to get current time` }, err); + return 0; + } +}); + +app.on('before-quit', () => { + getMpvInstance()?.stop(); + getMpvInstance()?.quit(); +}); + +app.on('window-all-closed', () => { + getMpvInstance()?.quit(); }); diff --git a/src/main/main.ts b/src/main/main.ts index a3722c34..848fc3a8 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -26,14 +26,20 @@ import { net, } from 'electron'; import electronLocalShortcut from 'electron-localshortcut'; -import log from 'electron-log'; +import log from 'electron-log/main'; import { autoUpdater } from 'electron-updater'; -import uniq from 'lodash/uniq'; -import MpvAPI from 'node-mpv'; import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys'; import { store } from './features/core/settings/index'; import MenuBuilder from './menu'; -import { hotkeyToElectronAccelerator, isLinux, isMacOS, isWindows, resolveHtmlPath } from './utils'; +import { + hotkeyToElectronAccelerator, + isLinux, + isMacOS, + isWindows, + resolveHtmlPath, + createLog, + autoUpdaterLogInterface, +} from './utils'; import './features'; import type { TitleTheme } from '/@/renderer/types'; @@ -42,7 +48,7 @@ declare module 'node-mpv'; export default class AppUpdater { constructor() { log.transports.file.level = 'info'; - autoUpdater.logger = log; + autoUpdater.logger = autoUpdaterLogInterface; autoUpdater.checkForUpdatesAndNotify(); } } @@ -104,6 +110,19 @@ export const getMainWindow = () => { return mainWindow; }; +export const sendToastToRenderer = ({ + message, + type, +}: { + message: string; + type: 'success' | 'error' | 'warning' | 'info'; +}) => { + getMainWindow()?.webContents.send('toast-from-main', { + message, + type, + }); +}; + const createWinThumbarButtons = () => { if (isWindows()) { getMainWindow()?.setThumbarButtons([ @@ -316,7 +335,7 @@ const createWindow = async () => { } const queue = JSON.parse(data.toString()); - getMainWindow()?.webContents.send('renderer-player-restore-queue', queue); + getMainWindow()?.webContents.send('renderer-restore-queue', queue); }); }); }); @@ -362,7 +381,7 @@ const createWindow = async () => { event.preventDefault(); saved = true; - getMainWindow()?.webContents.send('renderer-player-save-queue'); + getMainWindow()?.webContents.send('renderer-save-queue'); ipcMain.once('player-save-queue', async (_event, data: Record) => { const queueLocation = join(app.getPath('userData'), 'queue'); @@ -433,138 +452,6 @@ const createWindow = async () => { app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService'); -const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined; - -const prefetchPlaylistParams = [ - '--prefetch-playlist=no', - '--prefetch-playlist=yes', - '--prefetch-playlist', -]; - -const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => { - const parameters = ['--idle=yes', '--no-config', '--load-scripts=no']; - - if (!extraParameters?.some((param) => prefetchPlaylistParams.includes(param))) { - parameters.push('--prefetch-playlist=yes'); - } - - return parameters; -}; - -let mpvInstance: MpvAPI | null = null; - -const createMpv = async (data: { - extraParameters?: string[]; - properties?: Record; -}): Promise => { - const { extraParameters, properties } = data; - - const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]); - console.log('Setting mpv params: ', params); - - const extra = isDevelopment ? '-dev' : ''; - - const mpv = new MpvAPI( - { - audio_only: true, - auto_restart: false, - binary: MPV_BINARY_PATH || '', - socket: isWindows() ? `\\\\.\\pipe\\mpvserver${extra}` : `/tmp/node-mpv${extra}.sock`, - time_update: 1, - }, - params, - ); - - try { - await mpv.start(); - } catch (error) { - console.log('MPV failed to start', error); - } finally { - console.log('Setting MPV properties: ', properties); - await mpv.setMultipleProperties(properties || {}); - } - - mpv.on('status', (status, ...rest) => { - console.log('MPV Event: status', status.property, status.value, rest); - if (status.property === 'playlist-pos') { - if (status.value === -1) { - mpv?.stop(); - } - - if (status.value !== 0) { - getMainWindow()?.webContents.send('renderer-player-auto-next'); - } - } - }); - - // Automatically updates the play button when the player is playing - mpv.on('resumed', () => { - console.log('MPV Event: resumed'); - getMainWindow()?.webContents.send('renderer-player-play'); - }); - - // Automatically updates the play button when the player is stopped - mpv.on('stopped', () => { - console.log('MPV Event: stopped'); - getMainWindow()?.webContents.send('renderer-player-stop'); - }); - - // Automatically updates the play button when the player is paused - mpv.on('paused', () => { - console.log('MPV Event: paused'); - getMainWindow()?.webContents.send('renderer-player-pause'); - }); - - // Event output every interval set by time_update, used to update the current time - mpv.on('timeposition', (time: number) => { - getMainWindow()?.webContents.send('renderer-player-current-time', time); - }); - - mpv.on('quit', () => { - console.log('MPV Event: quit'); - }); - - return mpv; -}; - -export const getMpvInstance = () => { - return mpvInstance; -}; - -ipcMain.on('player-set-properties', async (_event, data: Record) => { - if (data.length === 0) { - return; - } - - if (data.length === 1) { - getMpvInstance()?.setProperty(Object.keys(data)[0], Object.values(data)[0]); - } else { - getMpvInstance()?.setMultipleProperties(data); - } -}); - -ipcMain.on( - 'player-restart', - async (_event, data: { extraParameters?: string[]; properties?: Record }) => { - mpvInstance?.quit(); - mpvInstance = await createMpv(data); - }, -); - -ipcMain.handle( - 'player-initialize', - async (_event, data: { extraParameters?: string[]; properties?: Record }) => { - console.log('Initializing MPV with data: ', data); - mpvInstance = await createMpv(data); - }, -); - -ipcMain.on('player-quit', async () => { - mpvInstance?.stop(); - mpvInstance?.quit(); - mpvInstance = null; -}); - // Must duplicate with the one in renderer process settings.store.ts enum BindingActions { GLOBAL_SEARCH = 'globalSearch', @@ -647,14 +534,21 @@ ipcMain.on( }, ); -app.on('before-quit', () => { - getMpvInstance()?.stop(); - getMpvInstance()?.quit(); -}); +ipcMain.on( + 'logger', + ( + _event, + data: { + message: string; + type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info'; + }, + ) => { + createLog(data); + }, +); app.on('window-all-closed', () => { globalShortcut.unregisterAll(); - getMpvInstance()?.quit(); // Respect the OSX convention of having the application in memory even // after all windows have been closed if (isMacOS()) { diff --git a/src/main/preload/local-settings.ts b/src/main/preload/local-settings.ts index 7b1b3732..b8aafba0 100644 --- a/src/main/preload/local-settings.ts +++ b/src/main/preload/local-settings.ts @@ -4,7 +4,15 @@ import type { TitleTheme } from '/@/renderer/types'; const store = new Store(); -const set = (property: string, value: string | Record | boolean | string[]) => { +const set = ( + property: string, + value: string | Record | boolean | string[] | undefined, +) => { + if (value === undefined) { + store.delete(property); + return; + } + store.set(`${property}`, value); }; diff --git a/src/main/preload/mpv-player.ts b/src/main/preload/mpv-player.ts index a53d52d1..78108e29 100644 --- a/src/main/preload/mpv-player.ts +++ b/src/main/preload/mpv-player.ts @@ -1,11 +1,15 @@ import { ipcRenderer, IpcRendererEvent } from 'electron'; -import { PlayerData, PlayerState } from '/@/renderer/store'; +import { PlayerData } from '/@/renderer/store'; const initialize = (data: { extraParameters?: string[]; properties?: Record }) => { return ipcRenderer.invoke('player-initialize', data); }; -const restart = (data: { extraParameters?: string[]; properties?: Record }) => { +const restart = (data: { + binaryPath?: string; + extraParameters?: string[]; + properties?: Record; +}) => { return ipcRenderer.invoke('player-restart', data); }; @@ -18,7 +22,6 @@ const cleanup = () => { }; const setProperties = (data: Record) => { - console.log('Setting property :>>', data); ipcRenderer.send('player-set-properties', data); }; @@ -50,14 +53,6 @@ const previous = () => { ipcRenderer.send('player-previous'); }; -const restoreQueue = () => { - ipcRenderer.send('player-restore-queue'); -}; - -const saveQueue = (data: Record) => { - ipcRenderer.send('player-save-queue', data); -}; - const seek = (seconds: number) => { ipcRenderer.send('player-seek', seconds); }; @@ -154,20 +149,14 @@ const rendererQuit = (cb: (event: IpcRendererEvent) => void) => { ipcRenderer.on('renderer-player-quit', cb); }; -const rendererSaveQueue = (cb: (event: IpcRendererEvent) => void) => { - ipcRenderer.on('renderer-player-save-queue', cb); -}; - -const rendererRestoreQueue = ( - cb: (event: IpcRendererEvent, data: Partial) => void, -) => { - ipcRenderer.on('renderer-player-restore-queue', cb); -}; - const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => { ipcRenderer.on('renderer-player-error', cb); }; +const rendererPlayerFallback = (cb: (event: IpcRendererEvent, data: boolean) => void) => { + ipcRenderer.on('renderer-player-fallback', cb); +}; + export const mpvPlayer = { autoNext, cleanup, @@ -182,8 +171,6 @@ export const mpvPlayer = { previous, quit, restart, - restoreQueue, - saveQueue, seek, seekTo, setProperties, @@ -201,10 +188,9 @@ export const mpvPlayerListener = { rendererPause, rendererPlay, rendererPlayPause, + rendererPlayerFallback, rendererPrevious, rendererQuit, - rendererRestoreQueue, - rendererSaveQueue, rendererSkipBackward, rendererSkipForward, rendererStop, diff --git a/src/main/preload/utils.ts b/src/main/preload/utils.ts index 77bc4dad..12f83ba7 100644 --- a/src/main/preload/utils.ts +++ b/src/main/preload/utils.ts @@ -1,9 +1,59 @@ +import { IpcRendererEvent, ipcRenderer } from 'electron'; import { isMacOS, isWindows, isLinux } from '../utils'; +import { PlayerState } from '/@/renderer/store'; + +const saveQueue = (data: Record) => { + ipcRenderer.send('player-save-queue', data); +}; + +const restoreQueue = () => { + ipcRenderer.send('player-restore-queue'); +}; + +const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => { + ipcRenderer.on('renderer-save-queue', cb); +}; + +const onRestoreQueue = (cb: (event: IpcRendererEvent, data: Partial) => void) => { + ipcRenderer.on('renderer-restore-queue', cb); +}; + +const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number }) => void) => { + ipcRenderer.on('player-error-listener', cb); +}; + +const mainMessageListener = ( + cb: ( + event: IpcRendererEvent, + data: { message: string; type: 'success' | 'error' | 'warning' | 'info' }, + ) => void, +) => { + ipcRenderer.on('toast-from-main', cb); +}; + +const logger = ( + cb: ( + event: IpcRendererEvent, + data: { + message: string; + type: 'debug' | 'verbose' | 'error' | 'warning' | 'info'; + }, + ) => void, +) => { + ipcRenderer.send('logger', cb); +}; export const utils = { isLinux, isMacOS, isWindows, + logger, + mainMessageListener, + onRestoreQueue, + onSaveQueue, + playerErrorListener, + restoreQueue, + saveQueue, }; export type Utils = typeof utils; diff --git a/src/main/utils.ts b/src/main/utils.ts index 5ca169b3..8a29f6ef 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -2,6 +2,7 @@ import path from 'path'; import process from 'process'; import { URL } from 'url'; +import log from 'electron-log/main'; export let resolveHtmlPath: (htmlFileName: string) => string; @@ -50,3 +51,46 @@ export const hotkeyToElectronAccelerator = (hotkey: string) => { return accelerator; }; + +const logMethod = { + debug: log.debug, + error: log.error, + info: log.info, + success: log.info, + verbose: log.verbose, + warning: log.warn, +}; + +const logColor = { + debug: 'blue', + error: 'red', + info: 'blue', + success: 'green', + verbose: 'blue', + warning: 'yellow', +}; + +export const createLog = (data: { + message: string; + type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info'; +}) => { + logMethod[data.type](`%c${data.message}`, `color: ${logColor[data.type]}`); +}; + +export const autoUpdaterLogInterface = { + debug: (message: string) => { + createLog({ message: `[SYSTEM] ${message}`, type: 'debug' }); + }, + + error: (message: string) => { + createLog({ message: `[SYSTEM] ${message}`, type: 'error' }); + }, + + info: (message: string) => { + createLog({ message: `[SYSTEM] ${message}`, type: 'info' }); + }, + + warn: (message: string) => { + createLog({ message: `[SYSTEM] ${message}`, type: 'warning' }); + }, +}; diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index d64b2149..6e2882e7 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -33,9 +33,9 @@ ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule initSimpleImg({ threshold: 0.05 }, true); const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; -const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null; const ipc = isElectron() ? window.electron.ipc : null; const remote = isElectron() ? window.electron.remote : null; +const utils = isElectron() ? window.electron.utils : null; export const App = () => { const theme = useTheme(); @@ -97,28 +97,31 @@ export const App = () => { // Start the mpv instance on startup useEffect(() => { const initializeMpv = async () => { - const isRunning: boolean | undefined = await mpvPlayer?.isRunning(); + if (playbackType === PlaybackType.LOCAL) { + const isRunning: boolean | undefined = await mpvPlayer?.isRunning(); - mpvPlayer?.stop(); + mpvPlayer?.stop(); - if (!isRunning) { - const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters; - const properties: Record = { - speed: usePlayerStore.getState().current.speed, - ...getMpvProperties(useSettingsStore.getState().playback.mpvProperties), - }; + if (!isRunning) { + const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters; + const properties: Record = { + speed: usePlayerStore.getState().current.speed, + ...getMpvProperties(useSettingsStore.getState().playback.mpvProperties), + }; - await mpvPlayer?.initialize({ - extraParameters, - properties, - }); + await mpvPlayer?.initialize({ + extraParameters, + properties, + }); - mpvPlayer?.volume(properties.volume); + mpvPlayer?.volume(properties.volume); + } } - mpvPlayer?.restoreQueue(); + + utils?.restoreQueue(); }; - if (isElectron() && playbackType === PlaybackType.LOCAL) { + if (isElectron()) { initializeMpv(); } @@ -136,8 +139,8 @@ export const App = () => { }, [bindings]); useEffect(() => { - if (isElectron()) { - mpvPlayerListener!.rendererSaveQueue(() => { + if (utils) { + utils.onSaveQueue(() => { const { current, queue } = usePlayerStore.getState(); const stateToSave: Partial> = { current: { @@ -146,10 +149,10 @@ export const App = () => { }, queue, }; - mpvPlayer!.saveQueue(stateToSave); + utils.saveQueue(stateToSave); }); - mpvPlayerListener!.rendererRestoreQueue((_event: any, data) => { + utils.onRestoreQueue((_event: any, data) => { const playerData = restoreQueue(data); if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueue(playerData, true); @@ -158,8 +161,8 @@ export const App = () => { } return () => { - ipc?.removeAllListeners('renderer-player-restore-queue'); - ipc?.removeAllListeners('renderer-player-save-queue'); + ipc?.removeAllListeners('renderer-restore-queue'); + ipc?.removeAllListeners('renderer-save-queue'); }; }, [playbackType, restoreQueue]); diff --git a/src/renderer/components/toast/index.tsx b/src/renderer/components/toast/index.tsx index 088616f1..c4b37d7f 100644 --- a/src/renderer/components/toast/index.tsx +++ b/src/renderer/components/toast/index.tsx @@ -30,7 +30,7 @@ const showToast = ({ type, ...props }: NotificationProps) => { ? 'Error' : 'Info'; - const defaultDuration = type === 'error' ? 2000 : 1000; + const defaultDuration = type === 'error' ? 5000 : 2000; return showNotification({ autoClose: defaultDuration, diff --git a/src/renderer/features/action-required/components/mpv-required.tsx b/src/renderer/features/action-required/components/mpv-required.tsx index 275d5d44..aff386a9 100644 --- a/src/renderer/features/action-required/components/mpv-required.tsx +++ b/src/renderer/features/action-required/components/mpv-required.tsx @@ -1,23 +1,36 @@ import { useEffect, useState } from 'react'; import isElectron from 'is-electron'; -import { FileInput, Text, Button } from '/@/renderer/components'; +import { FileInput, Text, Button, Checkbox } from '/@/renderer/components'; +import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store'; +import { PlaybackType } from '/@/renderer/types'; +import { useTranslation } from 'react-i18next'; const localSettings = isElectron() ? window.electron.localSettings : null; export const MpvRequired = () => { const [mpvPath, setMpvPath] = useState(''); + const settings = usePlaybackSettings(); + const { setSettings } = useSettingsStoreActions(); + const [disabled, setDisabled] = useState(false); + const { t } = useTranslation(); + const handleSetMpvPath = (e: File) => { localSettings?.set('mpv_path', e.path); }; - useEffect(() => { - const getMpvPath = async () => { - if (!localSettings) return setMpvPath(''); - const mpvPath = localSettings.get('mpv_path') as string; - return setMpvPath(mpvPath); - }; + const handleSetDisableMpv = (disabled: boolean) => { + setDisabled(disabled); + localSettings?.set('disable_mpv', disabled); - getMpvPath(); + setSettings({ + playback: { ...settings, type: disabled ? PlaybackType.WEB : PlaybackType.LOCAL }, + }); + }; + + useEffect(() => { + if (!localSettings) return setMpvPath(''); + const mpvPath = localSettings.get('mpv_path') as string; + return setMpvPath(mpvPath); }, []); return ( @@ -34,9 +47,15 @@ export const MpvRequired = () => { + {t('setting.disable_mpv', { context: 'description' })} + handleSetDisableMpv(e.currentTarget.checked)} + /> ); diff --git a/src/renderer/features/action-required/routes/action-required-route.tsx b/src/renderer/features/action-required/routes/action-required-route.tsx index b5f79e5e..458540ed 100644 --- a/src/renderer/features/action-required/routes/action-required-route.tsx +++ b/src/renderer/features/action-required/routes/action-required-route.tsx @@ -1,48 +1,22 @@ -import { useState, useEffect } from 'react'; import { Center, Group, Stack } from '@mantine/core'; -import isElectron from 'is-electron'; import { useTranslation } from 'react-i18next'; import { RiCheckFill } from 'react-icons/ri'; import { Link, Navigate } from 'react-router-dom'; import { Button, PageHeader, Text } from '/@/renderer/components'; import { ActionRequiredContainer } from '/@/renderer/features/action-required/components/action-required-container'; -import { MpvRequired } from '/@/renderer/features/action-required/components/mpv-required'; import { ServerCredentialRequired } from '/@/renderer/features/action-required/components/server-credential-required'; import { ServerRequired } from '/@/renderer/features/action-required/components/server-required'; import { AnimatedPage } from '/@/renderer/features/shared'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer } from '/@/renderer/store'; -const localSettings = isElectron() ? window.electron.localSettings : null; - const ActionRequiredRoute = () => { const { t } = useTranslation(); const currentServer = useCurrentServer(); - const [isMpvRequired, setIsMpvRequired] = useState(false); const isServerRequired = !currentServer; const isCredentialRequired = false; - useEffect(() => { - const getMpvPath = async () => { - if (!localSettings) return setIsMpvRequired(false); - const mpvPath = await localSettings.get('mpv_path'); - - if (mpvPath) { - return setIsMpvRequired(false); - } - - return setIsMpvRequired(true); - }; - - getMpvPath(); - }, []); - const checks = [ - { - component: , - title: t('error.mpvRequired', { postProcess: 'sentenceCase' }), - valid: !isMpvRequired, - }, { component: , title: t('error.credentialsRequired', { postProcess: 'sentenceCase' }), diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 4ebe749a..83cce029 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -51,7 +51,7 @@ import { usePlayerStore, useQueueControls, } from '/@/renderer/store'; -import { usePlayerType } from '/@/renderer/store/settings.store'; +import { usePlaybackType } from '/@/renderer/store/settings.store'; import { Play, PlaybackType } from '/@/renderer/types'; type ContextMenuContextProps = { @@ -575,7 +575,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { [ctx.data, ctx.dataNodes, updateRatingMutation], ); - const playerType = usePlayerType(); + const playbackType = usePlaybackType(); const { moveToBottomOfQueue, moveToTopOfQueue, removeFromQueue } = useQueueControls(); const handleMoveToBottom = useCallback(() => { @@ -584,10 +584,10 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { const playerData = moveToBottomOfQueue(uniqueIds); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueueNext(playerData); } - }, [ctx.dataNodes, moveToBottomOfQueue, playerType]); + }, [ctx.dataNodes, moveToBottomOfQueue, playbackType]); const handleMoveToTop = useCallback(() => { const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId); @@ -595,10 +595,10 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { const playerData = moveToTopOfQueue(uniqueIds); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueueNext(playerData); } - }, [ctx.dataNodes, moveToTopOfQueue, playerType]); + }, [ctx.dataNodes, moveToTopOfQueue, playbackType]); const handleRemoveSelected = useCallback(() => { const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId); @@ -608,7 +608,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { const playerData = removeFromQueue(uniqueIds); const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong?.uniqueId); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { if (isCurrentSongRemoved) { mpvPlayer!.setQueue(playerData); } else { @@ -621,7 +621,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { if (isCurrentSongRemoved) { remote?.updateSong({ song: playerData.current.song }); } - }, [ctx.dataNodes, ctx.tableApi, playerType, removeFromQueue]); + }, [ctx.dataNodes, ctx.tableApi, playbackType, removeFromQueue]); const handleDeselectAll = useCallback(() => { ctx.tableApi?.deselectAll(); diff --git a/src/renderer/features/lyrics/synchronized-lyrics.tsx b/src/renderer/features/lyrics/synchronized-lyrics.tsx index 1207798c..e0e0c17e 100644 --- a/src/renderer/features/lyrics/synchronized-lyrics.tsx +++ b/src/renderer/features/lyrics/synchronized-lyrics.tsx @@ -3,7 +3,7 @@ import { useCurrentStatus, useCurrentTime, useLyricsSettings, - usePlayerType, + usePlaybackType, useSeeked, } from '/@/renderer/store'; import { PlaybackType, PlayerStatus } from '/@/renderer/types'; @@ -59,7 +59,7 @@ export const SynchronizedLyrics = ({ }: SynchronizedLyricsProps) => { const playersRef = PlayersRef; const status = useCurrentStatus(); - const playerType = usePlayerType(); + const playbackType = usePlaybackType(); const now = useCurrentTime(); const settings = useLyricsSettings(); @@ -96,7 +96,7 @@ export const SynchronizedLyrics = ({ }; const getCurrentTime = useCallback(async () => { - if (isElectron() && playerType !== PlaybackType.WEB) { + if (isElectron() && playbackType !== PlaybackType.WEB) { if (mpvPlayer) { return mpvPlayer.getCurrentTime(); } @@ -116,7 +116,7 @@ export const SynchronizedLyrics = ({ if (!player) return 0; return player.currentTime; - }, [playerType, playersRef]); + }, [playbackType, playersRef]); const setCurrentLyric = useCallback( (timeInMs: number, epoch?: number, targetIndex?: number) => { @@ -222,7 +222,7 @@ export const SynchronizedLyrics = ({ } return () => {}; - }, [getCurrentTime, lyrics, playerType, setCurrentLyric, status]); + }, [getCurrentTime, lyrics, playbackType, setCurrentLyric, status]); useEffect(() => { // This handler is used to deal with changes to the current delay. If the offset diff --git a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx index 72879812..ea13d0b5 100644 --- a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx +++ b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx @@ -15,7 +15,7 @@ import { import { Song } from '/@/renderer/api/types'; import { usePlayerControls, useQueueControls } from '/@/renderer/store'; import { PlaybackType, TableType } from '/@/renderer/types'; -import { usePlayerType } from '/@/renderer/store/settings.store'; +import { usePlaybackType } from '/@/renderer/store/settings.store'; import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store'; import { TableConfigDropdown } from '/@/renderer/components/virtual-table'; @@ -34,7 +34,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr const { pause } = usePlayerControls(); - const playerType = usePlayerType(); + const playbackType = usePlaybackType(); const setCurrentTime = useSetCurrentTime(); const handleMoveToBottom = () => { @@ -44,7 +44,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr const playerData = moveToBottomOfQueue(uniqueIds); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueueNext(playerData); } }; @@ -56,7 +56,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr const playerData = moveToTopOfQueue(uniqueIds); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueueNext(playerData); } }; @@ -70,7 +70,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr const playerData = removeFromQueue(uniqueIds); const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong.uniqueId); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { if (isCurrentSongRemoved) { mpvPlayer!.setQueue(playerData); } else { @@ -86,7 +86,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr const handleClearQueue = () => { const playerData = clearQueue(); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueue(playerData); mpvPlayer!.pause(); } @@ -100,7 +100,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr const handleShuffleQueue = () => { const playerData = shuffleQueue(); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueueNext(playerData); } }; diff --git a/src/renderer/features/now-playing/components/play-queue.tsx b/src/renderer/features/now-playing/components/play-queue.tsx index 0b8bc581..1c98b6c3 100644 --- a/src/renderer/features/now-playing/components/play-queue.tsx +++ b/src/renderer/features/now-playing/components/play-queue.tsx @@ -19,7 +19,7 @@ import { useVolume, } from '/@/renderer/store'; import { - usePlayerType, + usePlaybackType, useSettingsStore, useSettingsStoreActions, useTableSettings, @@ -56,7 +56,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref) => { const { setAppStore } = useAppStoreActions(); const tableConfig = useTableSettings(type); const [gridApi, setGridApi] = useState(); - const playerType = usePlayerType(); + const playbackType = usePlaybackType(); const { play } = usePlayerControls(); const volume = useVolume(); const isFocused = useAppFocus(); @@ -87,7 +87,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref) => { status: PlayerStatus.PLAYING, }); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.volume(volume); mpvPlayer!.setQueue(playerData); mpvPlayer!.play(); @@ -111,7 +111,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref) => { const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.setQueueNext(playerData); } diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index cb27df61..c2d72733 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -32,7 +32,7 @@ import { } from '/@/renderer/store'; import { useHotkeySettings, - usePlayerType, + usePlaybackType, useSettingsStore, } from '/@/renderer/store/settings.store'; import { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types'; @@ -99,7 +99,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { const currentSong = useCurrentSong(); const skip = useSettingsStore((state) => state.general.skipButtons); const buttonSize = useSettingsStore((state) => state.general.buttonSize); - const playerType = usePlayerType(); + const playbackType = usePlaybackType(); const player1 = playersRef?.current?.player1; const player2 = playersRef?.current?.player2; const status = useCurrentStatus(); @@ -134,7 +134,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { let interval: any; if (status === PlayerStatus.PLAYING && !isSeeking) { - if (!isElectron() || playerType === PlaybackType.WEB) { + if (!isElectron() || playbackType === PlaybackType.WEB) { interval = setInterval(() => { setCurrentTime(currentPlayerRef.getCurrentTime()); }, 1000); @@ -144,7 +144,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { } return () => clearInterval(interval); - }, [currentPlayerRef, isSeeking, setCurrentTime, playerType, status]); + }, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]); const [seekValue, setSeekValue] = useState(0); diff --git a/src/renderer/features/player/components/playerbar.tsx b/src/renderer/features/player/components/playerbar.tsx index b251ad0d..03555297 100644 --- a/src/renderer/features/player/components/playerbar.tsx +++ b/src/renderer/features/player/components/playerbar.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import isElectron from 'is-electron'; import styled from 'styled-components'; -import { useSettingsStore } from '/@/renderer/store/settings.store'; +import { usePlaybackType, useSettingsStore } from '/@/renderer/store/settings.store'; import { PlaybackType } from '/@/renderer/types'; import { AudioPlayer } from '/@/renderer/components'; import { @@ -64,6 +64,7 @@ const remote = isElectron() ? window.electron.remote : null; export const Playerbar = () => { const playersRef = PlayersRef; const settings = useSettingsStore((state) => state.playback); + const playbackType = usePlaybackType(); const volume = useVolume(); const player1 = usePlayer1Data(); const player2 = usePlayer2Data(); @@ -96,7 +97,7 @@ export const Playerbar = () => { - {settings.type === PlaybackType.WEB && ( + {playbackType === PlaybackType.WEB && ( { const { t } = useTranslation(); const { playersRef } = args; - const settings = useSettingsStore((state) => state.playback); const currentPlayer = useCurrentPlayer(); const { setShuffle, setRepeat, play, pause, previous, next, setCurrentIndex, autoNext } = usePlayerControls(); @@ -41,7 +38,7 @@ export const useCenterControls = (args: { playersRef: any }) => { const playerStatus = useCurrentStatus(); const repeatStatus = useRepeatStatus(); const shuffleStatus = useShuffleStatus(); - const playerType = usePlayerType(); + const playbackType = usePlaybackType(); const player1Ref = playersRef?.current?.player1; const player2Ref = playersRef?.current?.player2; const currentPlayerRef = currentPlayer === 1 ? player1Ref : player2Ref; @@ -77,7 +74,7 @@ export const useCenterControls = (args: { playersRef: any }) => { resetPlayers(); }, [player1Ref, player2Ref, resetPlayers]); - const isMpvPlayer = isElectron() && settings.type === PlaybackType.LOCAL; + const isMpvPlayer = isElectron() && playbackType === PlaybackType.LOCAL; const mprisUpdateSong = (args?: { currentTime?: number; @@ -282,13 +279,13 @@ export const useCenterControls = (args: { playersRef: any }) => { switch (repeatStatus) { case PlayerRepeat.NONE: - handleRepeatNone[playerType](); + handleRepeatNone[playbackType](); break; case PlayerRepeat.ALL: - handleRepeatAll[playerType](); + handleRepeatAll[playbackType](); break; case PlayerRepeat.ONE: - handleRepeatOne[playerType](); + handleRepeatOne[playbackType](); break; default: @@ -299,7 +296,7 @@ export const useCenterControls = (args: { playersRef: any }) => { checkIsLastTrack, pause, play, - playerType, + playbackType, repeatStatus, resetPlayers, setCurrentIndex, @@ -380,13 +377,13 @@ export const useCenterControls = (args: { playersRef: any }) => { switch (repeatStatus) { case PlayerRepeat.NONE: - handleRepeatNone[playerType](); + handleRepeatNone[playbackType](); break; case PlayerRepeat.ALL: - handleRepeatAll[playerType](); + handleRepeatAll[playbackType](); break; case PlayerRepeat.ONE: - handleRepeatOne[playerType](); + handleRepeatOne[playbackType](); break; default: @@ -398,7 +395,7 @@ export const useCenterControls = (args: { playersRef: any }) => { checkIsLastTrack, next, pause, - playerType, + playbackType, repeatStatus, resetPlayers, setCurrentIndex, @@ -511,13 +508,13 @@ export const useCenterControls = (args: { playersRef: any }) => { switch (repeatStatus) { case PlayerRepeat.NONE: - handleRepeatNone[playerType](); + handleRepeatNone[playbackType](); break; case PlayerRepeat.ALL: - handleRepeatAll[playerType](); + handleRepeatAll[playbackType](); break; case PlayerRepeat.ONE: - handleRepeatOne[playerType](); + handleRepeatOne[playbackType](); break; default: @@ -531,7 +528,7 @@ export const useCenterControls = (args: { playersRef: any }) => { handleScrobbleFromSongRestart, isMpvPlayer, pause, - playerType, + playbackType, previous, queue.length, repeatStatus, diff --git a/src/renderer/features/player/hooks/use-handle-playqueue-add.ts b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts index a6f0e50a..d4e37433 100644 --- a/src/renderer/features/player/hooks/use-handle-playqueue-add.ts +++ b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts @@ -1,7 +1,7 @@ import { useCallback, useRef } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store'; -import { usePlayerType } from '/@/renderer/store/settings.store'; +import { usePlaybackType } from '/@/renderer/store/settings.store'; import { PlayQueueAddOptions, Play, @@ -65,7 +65,7 @@ const addToQueue = usePlayerStore.getState().actions.addToQueue; export const useHandlePlayQueueAdd = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); - const playerType = usePlayerType(); + const playbackType = usePlaybackType(); const server = useCurrentServer(); const { play } = usePlayerControls(); const timeoutIds = useRef> | null>({}); @@ -170,7 +170,7 @@ export const useHandlePlayQueueAdd = () => { const hadSong = usePlayerStore.getState().queue.default.length > 0; const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs }); - if (playerType === PlaybackType.LOCAL) { + if (playbackType === PlaybackType.LOCAL) { mpvPlayer!.volume(usePlayerStore.getState().volume); if (playType === Play.NOW || !hadSong) { @@ -198,7 +198,7 @@ export const useHandlePlayQueueAdd = () => { return null; }, - [play, playerType, queryClient, server, t], + [play, playbackType, queryClient, server, t], ); return handlePlayQueueAdd; diff --git a/src/renderer/features/settings/components/playback/mpv-settings.tsx b/src/renderer/features/settings/components/playback/mpv-settings.tsx index 15443673..9004a465 100644 --- a/src/renderer/features/settings/components/playback/mpv-settings.tsx +++ b/src/renderer/features/settings/components/playback/mpv-settings.tsx @@ -1,7 +1,15 @@ import { useEffect, useState } from 'react'; -import { Divider, Stack } from '@mantine/core'; +import { Divider, Group, Stack } from '@mantine/core'; import isElectron from 'is-electron'; -import { FileInput, Textarea, Text, Select, NumberInput, Switch } from '/@/renderer/components'; +import { + FileInput, + Textarea, + Text, + Select, + NumberInput, + Switch, + Button, +} from '/@/renderer/components'; import { SettingsSection, SettingOption, @@ -9,10 +17,13 @@ import { import { SettingsState, usePlaybackSettings, + useSettingsStore, useSettingsStoreActions, } from '/@/renderer/store/settings.store'; import { PlaybackType } from '/@/renderer/types'; import { useTranslation } from 'react-i18next'; +import { RiCloseLine, RiRestartLine } from 'react-icons/ri'; +import { usePlayerControls, usePlayerStore, useQueueControls } from '/@/renderer/store'; const localSettings = isElectron() ? window.electron.localSettings : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; @@ -64,11 +75,20 @@ export const MpvSettings = () => { const { t } = useTranslation(); const settings = usePlaybackSettings(); const { setSettings } = useSettingsStoreActions(); + const { pause } = usePlayerControls(); + const { clearQueue } = useQueueControls(); const [mpvPath, setMpvPath] = useState(''); - const handleSetMpvPath = (e: File) => { + const handleSetMpvPath = (e: File | null) => { + if (e === null) { + localSettings?.set('mpv_path', undefined); + setMpvPath(''); + return; + } + localSettings?.set('mpv_path', e.path); + setMpvPath(e.path); }; useEffect(() => { @@ -100,6 +120,22 @@ export const MpvSettings = () => { mpvPlayer?.setProperties(mpvSetting); }; + const handleReloadMpv = () => { + pause(); + clearQueue(); + + const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters; + const properties: Record = { + speed: usePlayerStore.getState().current.speed, + ...getMpvProperties(useSettingsStore.getState().playback.mpvProperties), + }; + mpvPlayer?.restart({ + binaryPath: mpvPath || undefined, + extraParameters, + properties, + }); + }; + const handleSetExtraParameters = (data: string[]) => { setSettings({ playback: { @@ -112,11 +148,38 @@ export const MpvSettings = () => { const options: SettingOption[] = [ { control: ( - + + + handleSetMpvPath(null)} + > + + + ) + } + width={200} + onChange={handleSetMpvPath} + /> + ), description: t('setting.mpvExecutablePath', { context: 'description', diff --git a/src/renderer/router/app-outlet.tsx b/src/renderer/router/app-outlet.tsx index 6bd6daf4..d77a8981 100644 --- a/src/renderer/router/app-outlet.tsx +++ b/src/renderer/router/app-outlet.tsx @@ -1,30 +1,42 @@ -import { useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; import isElectron from 'is-electron'; import { Navigate, Outlet } from 'react-router-dom'; import { AppRoute } from '/@/renderer/router/routes'; -import { useCurrentServer } from '/@/renderer/store'; +import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store'; +import { toast } from '/@/renderer/components'; -const localSettings = isElectron() ? window.electron.localSettings : null; +const ipc = isElectron() ? window.electron.ipc : null; +const utils = isElectron() ? window.electron.utils : null; +const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null; export const AppOutlet = () => { const currentServer = useCurrentServer(); + const setFallback = useSetPlayerFallback(); const isActionsRequired = useMemo(() => { - const isMpvRequired = () => { - if (!localSettings) return false; - const mpvPath = localSettings.get('mpv_path'); - if (mpvPath) return false; - return true; - }; - const isServerRequired = !currentServer; - const actions = [isServerRequired, isMpvRequired()]; + const actions = [isServerRequired]; const isActionRequired = actions.some((c) => c); return isActionRequired; }, [currentServer]); + useEffect(() => { + utils?.mainMessageListener((_event, data) => { + toast.show(data); + }); + + mpvPlayerListener?.rendererPlayerFallback((_event, data) => { + setFallback(data); + }); + + return () => { + ipc?.removeAllListeners('toast-from-main'); + ipc?.removeAllListeners('renderer-player-fallback'); + }; + }, [setFallback]); + if (isActionsRequired) { return ( void; setCurrentTime: (time: number, seek?: boolean) => void; setCurrentTrack: (uniqueId: string) => PlayerData; + setFallback: (fallback: boolean | null) => boolean; setFavorite: (ids: string[], favorite: boolean) => string[]; setMuted: (muted: boolean) => void; setRating: (ids: string[], rating: number | null) => string[]; @@ -806,6 +808,13 @@ export const usePlayerStore = create()( return get().actions.getPlayerData(); }, + setFallback: (fallback) => { + set((state) => { + state.fallback = fallback; + }); + + return fallback || false; + }, setFavorite: (ids, favorite) => { const { default: queue } = get().queue; const foundUniqueIds = []; @@ -953,6 +962,7 @@ export const usePlayerStore = create()( status: PlayerStatus.PAUSED, time: 0, }, + fallback: null, muted: false, queue: { default: [], @@ -973,7 +983,7 @@ export const usePlayerStore = create()( }, name: 'store_player', partialize: (state) => { - const notPersisted = ['queue', 'current', 'entry']; + const notPersisted = ['queue', 'current', 'entry', 'fallback']; return Object.fromEntries( Object.entries(state).filter(([key]) => !notPersisted.includes(key)), ); @@ -1066,6 +1076,10 @@ export const useMuted = () => usePlayerStore((state) => state.muted); export const useSpeed = () => usePlayerStore((state) => state.current.speed); +export const usePlayerFallback = () => usePlayerStore((state) => state.fallback); + +export const useSetPlayerFallback = () => usePlayerStore((state) => state.actions.setFallback); + export const useSetCurrentSpeed = () => usePlayerStore((state) => state.actions.setCurrentSpeed); export const useSetQueueFavorite = () => usePlayerStore((state) => state.actions.setFavorite); diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 0f0b5f92..c698ee50 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -23,6 +23,7 @@ import { } from '/@/renderer/types'; import { randomString } from '/@/renderer/utils'; import i18n from '/@/i18n/i18n'; +import { usePlayerStore } from '/@/renderer/store/player.store'; const utils = isElectron() ? window.electron.utils : null; @@ -381,7 +382,7 @@ const initialState: SettingsState = { scrobbleAtPercentage: 75, }, style: PlaybackStyle.GAPLESS, - type: PlaybackType.LOCAL, + type: PlaybackType.WEB, }, remote: { enabled: false, @@ -616,7 +617,16 @@ export const useTableSettings = (type: TableType) => export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow); -export const usePlayerType = () => useSettingsStore((state) => state.playback.type, shallow); +export const usePlaybackType = () => + useSettingsStore((state) => { + const isFallback = usePlayerStore.getState().fallback; + + if (isFallback) { + return PlaybackType.WEB; + } + + return state.playback.type; + }); export const usePlayButtonBehavior = () => useSettingsStore((state) => state.general.playButtonBehavior, shallow);