Merge pull request #501 from kgarner7/allow-no-mpv
- OOBE default to web player - Allow mpv to run using PATH env - Add improved mpv error logging - Add web player fallback on mpv error
This commit is contained in:
commit
eab11658bb
25 changed files with 706 additions and 418 deletions
29
package-lock.json
generated
29
package-lock.json
generated
|
@ -34,7 +34,7 @@
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-log": "^4.4.6",
|
"electron-log": "^5.1.1",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^4.6.5",
|
"electron-updater": "^4.6.5",
|
||||||
"fast-average-color": "^9.3.0",
|
"fast-average-color": "^9.3.0",
|
||||||
|
@ -9098,9 +9098,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-log": {
|
"node_modules/electron-log": {
|
||||||
"version": "4.4.6",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.1.1.tgz",
|
||||||
"integrity": "sha512-nirYgRdY+F+vclr8ijdwy2vW03IzFpDHTaKNWu76dEN21Y76+smcES5knS7cgHUUB0qNLOi8vZO36taakjbSXA=="
|
"integrity": "sha512-If7HU4Slbh2xfjOXOLxifkbgu6HmWDNJyXPLW+XNTOHMfFKisg0trA3d/7syyu25S+lHosfsd0VMfDSjGn1+Pw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-notarize": {
|
"node_modules/electron-notarize": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
@ -19963,9 +19966,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||||
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
|
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
"node_modules/tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
|
@ -28093,9 +28096,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electron-log": {
|
"electron-log": {
|
||||||
"version": "4.4.6",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.1.1.tgz",
|
||||||
"integrity": "sha512-nirYgRdY+F+vclr8ijdwy2vW03IzFpDHTaKNWu76dEN21Y76+smcES5knS7cgHUUB0qNLOi8vZO36taakjbSXA=="
|
"integrity": "sha512-If7HU4Slbh2xfjOXOLxifkbgu6HmWDNJyXPLW+XNTOHMfFKisg0trA3d/7syyu25S+lHosfsd0VMfDSjGn1+Pw=="
|
||||||
},
|
},
|
||||||
"electron-notarize": {
|
"electron-notarize": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
@ -36192,9 +36195,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||||
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
|
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||||
},
|
},
|
||||||
"tsutils": {
|
"tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
|
|
|
@ -313,7 +313,7 @@
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-log": "^4.4.6",
|
"electron-log": "^5.1.1",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^4.6.5",
|
"electron-updater": "^4.6.5",
|
||||||
"fast-average-color": "^9.3.0",
|
"fast-average-color": "^9.3.0",
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
"random": "random",
|
"random": "random",
|
||||||
"rating": "rating",
|
"rating": "rating",
|
||||||
"refresh": "refresh",
|
"refresh": "refresh",
|
||||||
|
"reload": "reload",
|
||||||
"reset": "reset",
|
"reset": "reset",
|
||||||
"resetToDefault": "reset to default",
|
"resetToDefault": "reset to default",
|
||||||
"restartRequired": "restart required",
|
"restartRequired": "restart required",
|
||||||
|
@ -506,9 +507,9 @@
|
||||||
"minimumScrobbleSeconds": "minimum scrobble (seconds)",
|
"minimumScrobbleSeconds": "minimum scrobble (seconds)",
|
||||||
"minimumScrobbleSeconds_description": "the minimum duration in seconds of the song that must be played before it is scrobbled",
|
"minimumScrobbleSeconds_description": "the minimum duration in seconds of the song that must be played before it is scrobbled",
|
||||||
"mpvExecutablePath": "mpv executable path",
|
"mpvExecutablePath": "mpv executable path",
|
||||||
"mpvExecutablePath_description": "sets the path to the mpv executable",
|
"mpvExecutablePath_description": "sets the path to the mpv executable. if left empty, the default path will be used",
|
||||||
"mpvExecutablePath_help": "one per line",
|
|
||||||
"mpvExtraParameters": "mpv parameters",
|
"mpvExtraParameters": "mpv parameters",
|
||||||
|
"mpvExtraParameters_help": "one per line",
|
||||||
"passwordStore": "passwords/secret store",
|
"passwordStore": "passwords/secret store",
|
||||||
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.",
|
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.",
|
||||||
"playbackStyle": "playback style",
|
"playbackStyle": "playback style",
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import console from 'console';
|
import console from 'console';
|
||||||
import { ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import { getMpvInstance } from '../../../main';
|
import uniq from 'lodash/uniq';
|
||||||
|
import MpvAPI from 'node-mpv';
|
||||||
|
import { getMainWindow, sendToastToRenderer } from '../../../main';
|
||||||
import { PlayerData } from '/@/renderer/store';
|
import { PlayerData } from '/@/renderer/store';
|
||||||
|
import { createLog, isWindows } from '../../../utils';
|
||||||
|
import { store } from '../settings';
|
||||||
|
|
||||||
declare module 'node-mpv';
|
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<string, any>;
|
||||||
|
}): Promise<MpvAPI> => {
|
||||||
|
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<string, any>) => {
|
||||||
|
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<string, any> }) => {
|
||||||
|
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<string, any> }) => {
|
||||||
|
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 () => {
|
ipcMain.handle('player-is-running', async () => {
|
||||||
return getMpvInstance()?.isRunning();
|
return getMpvInstance()?.isRunning();
|
||||||
});
|
});
|
||||||
|
@ -23,99 +229,93 @@ ipcMain.handle('player-clean-up', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('player-start', async () => {
|
ipcMain.on('player-start', async () => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.play()
|
await getMpvInstance()?.play();
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to play', err);
|
mpvLog({ action: 'Failed to start mpv playback' }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Starts the player
|
// Starts the player
|
||||||
ipcMain.on('player-play', async () => {
|
ipcMain.on('player-play', async () => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.play()
|
await getMpvInstance()?.play();
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to play', err);
|
mpvLog({ action: 'Failed to start mpv playback' }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pauses the player
|
// Pauses the player
|
||||||
ipcMain.on('player-pause', async () => {
|
ipcMain.on('player-pause', async () => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.pause()
|
await getMpvInstance()?.pause();
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to pause', err);
|
mpvLog({ action: 'Failed to pause mpv playback' }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stops the player
|
// Stops the player
|
||||||
ipcMain.on('player-stop', async () => {
|
ipcMain.on('player-stop', async () => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.stop()
|
await getMpvInstance()?.stop();
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to stop', err);
|
mpvLog({ action: 'Failed to stop mpv playback' }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Goes to the next track in the playlist
|
// Goes to the next track in the playlist
|
||||||
ipcMain.on('player-next', async () => {
|
ipcMain.on('player-next', async () => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.next()
|
await getMpvInstance()?.next();
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to go to next', err);
|
mpvLog({ action: 'Failed to go to next track' }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Goes to the previous track in the playlist
|
// Goes to the previous track in the playlist
|
||||||
ipcMain.on('player-previous', async () => {
|
ipcMain.on('player-previous', async () => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.prev()
|
await getMpvInstance()?.prev();
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to go to previous', err);
|
mpvLog({ action: 'Failed to go to previous track' }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seeks forward or backward by the given amount of seconds
|
// Seeks forward or backward by the given amount of seconds
|
||||||
ipcMain.on('player-seek', async (_event, time: number) => {
|
ipcMain.on('player-seek', async (_event, time: number) => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.seek(time)
|
await getMpvInstance()?.seek(time);
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to seek', err);
|
mpvLog({ action: `Failed to seek by ${time} seconds` }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seeks to the given time in seconds
|
// Seeks to the given time in seconds
|
||||||
ipcMain.on('player-seek-to', async (_event, time: number) => {
|
ipcMain.on('player-seek-to', async (_event, time: number) => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.goToPosition(time)
|
await getMpvInstance()?.goToPosition(time);
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log(`MPV failed to seek to ${time}`, err);
|
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
|
// 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) => {
|
ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => {
|
||||||
if (!data.queue.current && !data.queue.next) {
|
if (!data.queue.current && !data.queue.next) {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.clearPlaylist()
|
await getMpvInstance()?.clearPlaylist();
|
||||||
.catch((err) => {
|
await getMpvInstance()?.pause();
|
||||||
console.log('MPV failed to clear playlist', err);
|
return;
|
||||||
});
|
} catch (err: NodeMpvError | any) {
|
||||||
|
mpvLog({ action: `Failed to clear play queue` }, err);
|
||||||
await getMpvInstance()
|
}
|
||||||
?.pause()
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('MPV failed to pause', err);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (data.queue.current) {
|
if (data.queue.current) {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.load(data.queue.current.streamUrl, 'replace')
|
?.load(data.queue.current.streamUrl, 'replace')
|
||||||
.catch((err) => {
|
.catch(() => {
|
||||||
console.log('MPV failed to load song', err);
|
|
||||||
getMpvInstance()?.play();
|
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');
|
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.error(err);
|
mpvLog({ action: `Failed to set play queue` }, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pause) {
|
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
|
// Replaces the queue in position 1 to the given data
|
||||||
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
|
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
|
||||||
const size = await getMpvInstance()
|
try {
|
||||||
?.getPlaylistSize()
|
const size = await getMpvInstance()?.getPlaylistSize();
|
||||||
.catch((err) => {
|
|
||||||
console.log('MPV failed to get playlist size', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!size) {
|
if (!size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size > 1) {
|
if (size > 1) {
|
||||||
await getMpvInstance()
|
await getMpvInstance()?.playlistRemove(1);
|
||||||
?.playlistRemove(1)
|
}
|
||||||
.catch((err) => {
|
|
||||||
console.log('MPV failed to remove song from playlist', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.queue.next) {
|
if (data.queue.next) {
|
||||||
await getMpvInstance()
|
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
||||||
?.load(data.queue.next.streamUrl, 'append')
|
}
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to load next song', err);
|
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
|
// 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
|
// This allows us to easily set update the next song in the queue without
|
||||||
// disturbing the currently playing song
|
// disturbing the currently playing song
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.playlistRemove(0)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('MPV failed to remove song from playlist', err);
|
|
||||||
getMpvInstance()?.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.queue.next) {
|
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.load(data.queue.next.streamUrl, 'append')
|
?.playlistRemove(0)
|
||||||
.catch((err) => {
|
.catch(() => {
|
||||||
console.log('MPV failed to load next song', err);
|
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)
|
// Sets the volume to the given value (0-100)
|
||||||
ipcMain.on('player-volume', async (_event, value: number) => {
|
ipcMain.on('player-volume', async (_event, value: number) => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.volume(value)
|
if (!value || value < 0 || value > 100) {
|
||||||
.catch((err) => {
|
return;
|
||||||
console.log('MPV failed to set volume', err);
|
}
|
||||||
});
|
|
||||||
|
await getMpvInstance()?.volume(value);
|
||||||
|
} catch (err: NodeMpvError | any) {
|
||||||
|
mpvLog({ action: `Failed to set volume to ${value}` }, err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggles the mute status
|
// Toggles the mute status
|
||||||
ipcMain.on('player-mute', async (_event, mute: boolean) => {
|
ipcMain.on('player-mute', async (_event, mute: boolean) => {
|
||||||
await getMpvInstance()
|
try {
|
||||||
?.mute(mute)
|
await getMpvInstance()?.mute(mute);
|
||||||
.catch((err) => {
|
} catch (err: NodeMpvError | any) {
|
||||||
console.log('MPV failed to toggle mute', err);
|
mpvLog({ action: `Failed to set mute status` }, err);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
182
src/main/main.ts
182
src/main/main.ts
|
@ -26,14 +26,20 @@ import {
|
||||||
net,
|
net,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import electronLocalShortcut from 'electron-localshortcut';
|
import electronLocalShortcut from 'electron-localshortcut';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log/main';
|
||||||
import { autoUpdater } from 'electron-updater';
|
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 { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||||
import { store } from './features/core/settings/index';
|
import { store } from './features/core/settings/index';
|
||||||
import MenuBuilder from './menu';
|
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 './features';
|
||||||
import type { TitleTheme } from '/@/renderer/types';
|
import type { TitleTheme } from '/@/renderer/types';
|
||||||
|
|
||||||
|
@ -42,7 +48,7 @@ declare module 'node-mpv';
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
log.transports.file.level = 'info';
|
log.transports.file.level = 'info';
|
||||||
autoUpdater.logger = log;
|
autoUpdater.logger = autoUpdaterLogInterface;
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +110,19 @@ export const getMainWindow = () => {
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendToastToRenderer = ({
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
message: string;
|
||||||
|
type: 'success' | 'error' | 'warning' | 'info';
|
||||||
|
}) => {
|
||||||
|
getMainWindow()?.webContents.send('toast-from-main', {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const createWinThumbarButtons = () => {
|
const createWinThumbarButtons = () => {
|
||||||
if (isWindows()) {
|
if (isWindows()) {
|
||||||
getMainWindow()?.setThumbarButtons([
|
getMainWindow()?.setThumbarButtons([
|
||||||
|
@ -316,7 +335,7 @@ const createWindow = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = JSON.parse(data.toString());
|
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();
|
event.preventDefault();
|
||||||
saved = true;
|
saved = true;
|
||||||
|
|
||||||
getMainWindow()?.webContents.send('renderer-player-save-queue');
|
getMainWindow()?.webContents.send('renderer-save-queue');
|
||||||
|
|
||||||
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
|
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
|
||||||
const queueLocation = join(app.getPath('userData'), 'queue');
|
const queueLocation = join(app.getPath('userData'), 'queue');
|
||||||
|
@ -433,138 +452,6 @@ const createWindow = async () => {
|
||||||
|
|
||||||
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
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<string, any>;
|
|
||||||
}): Promise<MpvAPI> => {
|
|
||||||
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<string, any>) => {
|
|
||||||
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<string, any> }) => {
|
|
||||||
mpvInstance?.quit();
|
|
||||||
mpvInstance = await createMpv(data);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
'player-initialize',
|
|
||||||
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
|
||||||
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
|
// Must duplicate with the one in renderer process settings.store.ts
|
||||||
enum BindingActions {
|
enum BindingActions {
|
||||||
GLOBAL_SEARCH = 'globalSearch',
|
GLOBAL_SEARCH = 'globalSearch',
|
||||||
|
@ -647,14 +534,21 @@ ipcMain.on(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
ipcMain.on(
|
||||||
getMpvInstance()?.stop();
|
'logger',
|
||||||
getMpvInstance()?.quit();
|
(
|
||||||
});
|
_event,
|
||||||
|
data: {
|
||||||
|
message: string;
|
||||||
|
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
createLog(data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
getMpvInstance()?.quit();
|
|
||||||
// Respect the OSX convention of having the application in memory even
|
// Respect the OSX convention of having the application in memory even
|
||||||
// after all windows have been closed
|
// after all windows have been closed
|
||||||
if (isMacOS()) {
|
if (isMacOS()) {
|
||||||
|
|
|
@ -4,7 +4,15 @@ import type { TitleTheme } from '/@/renderer/types';
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
|
||||||
const set = (property: string, value: string | Record<string, unknown> | boolean | string[]) => {
|
const set = (
|
||||||
|
property: string,
|
||||||
|
value: string | Record<string, unknown> | boolean | string[] | undefined,
|
||||||
|
) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
store.delete(property);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
store.set(`${property}`, value);
|
store.set(`${property}`, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import { PlayerData, PlayerState } from '/@/renderer/store';
|
import { PlayerData } from '/@/renderer/store';
|
||||||
|
|
||||||
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
||||||
return ipcRenderer.invoke('player-initialize', data);
|
return ipcRenderer.invoke('player-initialize', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const restart = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
const restart = (data: {
|
||||||
|
binaryPath?: string;
|
||||||
|
extraParameters?: string[];
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
}) => {
|
||||||
return ipcRenderer.invoke('player-restart', data);
|
return ipcRenderer.invoke('player-restart', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,7 +22,6 @@ const cleanup = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setProperties = (data: Record<string, any>) => {
|
const setProperties = (data: Record<string, any>) => {
|
||||||
console.log('Setting property :>>', data);
|
|
||||||
ipcRenderer.send('player-set-properties', data);
|
ipcRenderer.send('player-set-properties', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -50,14 +53,6 @@ const previous = () => {
|
||||||
ipcRenderer.send('player-previous');
|
ipcRenderer.send('player-previous');
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreQueue = () => {
|
|
||||||
ipcRenderer.send('player-restore-queue');
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveQueue = (data: Record<string, any>) => {
|
|
||||||
ipcRenderer.send('player-save-queue', data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const seek = (seconds: number) => {
|
const seek = (seconds: number) => {
|
||||||
ipcRenderer.send('player-seek', seconds);
|
ipcRenderer.send('player-seek', seconds);
|
||||||
};
|
};
|
||||||
|
@ -154,20 +149,14 @@ const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
ipcRenderer.on('renderer-player-quit', cb);
|
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<PlayerState>) => void,
|
|
||||||
) => {
|
|
||||||
ipcRenderer.on('renderer-player-restore-queue', cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
|
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
|
||||||
ipcRenderer.on('renderer-player-error', cb);
|
ipcRenderer.on('renderer-player-error', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rendererPlayerFallback = (cb: (event: IpcRendererEvent, data: boolean) => void) => {
|
||||||
|
ipcRenderer.on('renderer-player-fallback', cb);
|
||||||
|
};
|
||||||
|
|
||||||
export const mpvPlayer = {
|
export const mpvPlayer = {
|
||||||
autoNext,
|
autoNext,
|
||||||
cleanup,
|
cleanup,
|
||||||
|
@ -182,8 +171,6 @@ export const mpvPlayer = {
|
||||||
previous,
|
previous,
|
||||||
quit,
|
quit,
|
||||||
restart,
|
restart,
|
||||||
restoreQueue,
|
|
||||||
saveQueue,
|
|
||||||
seek,
|
seek,
|
||||||
seekTo,
|
seekTo,
|
||||||
setProperties,
|
setProperties,
|
||||||
|
@ -201,10 +188,9 @@ export const mpvPlayerListener = {
|
||||||
rendererPause,
|
rendererPause,
|
||||||
rendererPlay,
|
rendererPlay,
|
||||||
rendererPlayPause,
|
rendererPlayPause,
|
||||||
|
rendererPlayerFallback,
|
||||||
rendererPrevious,
|
rendererPrevious,
|
||||||
rendererQuit,
|
rendererQuit,
|
||||||
rendererRestoreQueue,
|
|
||||||
rendererSaveQueue,
|
|
||||||
rendererSkipBackward,
|
rendererSkipBackward,
|
||||||
rendererSkipForward,
|
rendererSkipForward,
|
||||||
rendererStop,
|
rendererStop,
|
||||||
|
|
|
@ -1,9 +1,59 @@
|
||||||
|
import { IpcRendererEvent, ipcRenderer } from 'electron';
|
||||||
import { isMacOS, isWindows, isLinux } from '../utils';
|
import { isMacOS, isWindows, isLinux } from '../utils';
|
||||||
|
import { PlayerState } from '/@/renderer/store';
|
||||||
|
|
||||||
|
const saveQueue = (data: Record<string, any>) => {
|
||||||
|
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<PlayerState>) => 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 = {
|
export const utils = {
|
||||||
isLinux,
|
isLinux,
|
||||||
isMacOS,
|
isMacOS,
|
||||||
isWindows,
|
isWindows,
|
||||||
|
logger,
|
||||||
|
mainMessageListener,
|
||||||
|
onRestoreQueue,
|
||||||
|
onSaveQueue,
|
||||||
|
playerErrorListener,
|
||||||
|
restoreQueue,
|
||||||
|
saveQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Utils = typeof utils;
|
export type Utils = typeof utils;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
import log from 'electron-log/main';
|
||||||
|
|
||||||
export let resolveHtmlPath: (htmlFileName: string) => string;
|
export let resolveHtmlPath: (htmlFileName: string) => string;
|
||||||
|
|
||||||
|
@ -50,3 +51,46 @@ export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
||||||
|
|
||||||
return accelerator;
|
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' });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -33,9 +33,9 @@ ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule
|
||||||
initSimpleImg({ threshold: 0.05 }, true);
|
initSimpleImg({ threshold: 0.05 }, true);
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
|
||||||
const ipc = isElectron() ? window.electron.ipc : null;
|
const ipc = isElectron() ? window.electron.ipc : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
const remote = isElectron() ? window.electron.remote : null;
|
||||||
|
const utils = isElectron() ? window.electron.utils : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
@ -97,28 +97,31 @@ export const App = () => {
|
||||||
// Start the mpv instance on startup
|
// Start the mpv instance on startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeMpv = async () => {
|
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) {
|
if (!isRunning) {
|
||||||
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||||
const properties: Record<string, any> = {
|
const properties: Record<string, any> = {
|
||||||
speed: usePlayerStore.getState().current.speed,
|
speed: usePlayerStore.getState().current.speed,
|
||||||
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||||
};
|
};
|
||||||
|
|
||||||
await mpvPlayer?.initialize({
|
await mpvPlayer?.initialize({
|
||||||
extraParameters,
|
extraParameters,
|
||||||
properties,
|
properties,
|
||||||
});
|
});
|
||||||
|
|
||||||
mpvPlayer?.volume(properties.volume);
|
mpvPlayer?.volume(properties.volume);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mpvPlayer?.restoreQueue();
|
|
||||||
|
utils?.restoreQueue();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isElectron() && playbackType === PlaybackType.LOCAL) {
|
if (isElectron()) {
|
||||||
initializeMpv();
|
initializeMpv();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,8 +139,8 @@ export const App = () => {
|
||||||
}, [bindings]);
|
}, [bindings]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isElectron()) {
|
if (utils) {
|
||||||
mpvPlayerListener!.rendererSaveQueue(() => {
|
utils.onSaveQueue(() => {
|
||||||
const { current, queue } = usePlayerStore.getState();
|
const { current, queue } = usePlayerStore.getState();
|
||||||
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
||||||
current: {
|
current: {
|
||||||
|
@ -146,10 +149,10 @@ export const App = () => {
|
||||||
},
|
},
|
||||||
queue,
|
queue,
|
||||||
};
|
};
|
||||||
mpvPlayer!.saveQueue(stateToSave);
|
utils.saveQueue(stateToSave);
|
||||||
});
|
});
|
||||||
|
|
||||||
mpvPlayerListener!.rendererRestoreQueue((_event: any, data) => {
|
utils.onRestoreQueue((_event: any, data) => {
|
||||||
const playerData = restoreQueue(data);
|
const playerData = restoreQueue(data);
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
mpvPlayer!.setQueue(playerData, true);
|
||||||
|
@ -158,8 +161,8 @@ export const App = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ipc?.removeAllListeners('renderer-player-restore-queue');
|
ipc?.removeAllListeners('renderer-restore-queue');
|
||||||
ipc?.removeAllListeners('renderer-player-save-queue');
|
ipc?.removeAllListeners('renderer-save-queue');
|
||||||
};
|
};
|
||||||
}, [playbackType, restoreQueue]);
|
}, [playbackType, restoreQueue]);
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ const showToast = ({ type, ...props }: NotificationProps) => {
|
||||||
? 'Error'
|
? 'Error'
|
||||||
: 'Info';
|
: 'Info';
|
||||||
|
|
||||||
const defaultDuration = type === 'error' ? 2000 : 1000;
|
const defaultDuration = type === 'error' ? 5000 : 2000;
|
||||||
|
|
||||||
return showNotification({
|
return showNotification({
|
||||||
autoClose: defaultDuration,
|
autoClose: defaultDuration,
|
||||||
|
|
|
@ -1,23 +1,36 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import isElectron from 'is-electron';
|
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;
|
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
|
|
||||||
export const MpvRequired = () => {
|
export const MpvRequired = () => {
|
||||||
const [mpvPath, setMpvPath] = useState('');
|
const [mpvPath, setMpvPath] = useState('');
|
||||||
|
const settings = usePlaybackSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
const [disabled, setDisabled] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSetMpvPath = (e: File) => {
|
const handleSetMpvPath = (e: File) => {
|
||||||
localSettings?.set('mpv_path', e.path);
|
localSettings?.set('mpv_path', e.path);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSetDisableMpv = (disabled: boolean) => {
|
||||||
const getMpvPath = async () => {
|
setDisabled(disabled);
|
||||||
if (!localSettings) return setMpvPath('');
|
localSettings?.set('disable_mpv', disabled);
|
||||||
const mpvPath = localSettings.get('mpv_path') as string;
|
|
||||||
return setMpvPath(mpvPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
|
@ -34,9 +47,15 @@ export const MpvRequired = () => {
|
||||||
</a>
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
<FileInput
|
<FileInput
|
||||||
|
disabled={disabled}
|
||||||
placeholder={mpvPath}
|
placeholder={mpvPath}
|
||||||
onChange={handleSetMpvPath}
|
onChange={handleSetMpvPath}
|
||||||
/>
|
/>
|
||||||
|
<Text>{t('setting.disable_mpv', { context: 'description' })}</Text>
|
||||||
|
<Checkbox
|
||||||
|
label={t('setting.disableMpv')}
|
||||||
|
onChange={(e) => handleSetDisableMpv(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
<Button onClick={() => localSettings?.restart()}>Restart</Button>
|
<Button onClick={() => localSettings?.restart()}>Restart</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,48 +1,22 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Center, Group, Stack } from '@mantine/core';
|
import { Center, Group, Stack } from '@mantine/core';
|
||||||
import isElectron from 'is-electron';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RiCheckFill } from 'react-icons/ri';
|
import { RiCheckFill } from 'react-icons/ri';
|
||||||
import { Link, Navigate } from 'react-router-dom';
|
import { Link, Navigate } from 'react-router-dom';
|
||||||
import { Button, PageHeader, Text } from '/@/renderer/components';
|
import { Button, PageHeader, Text } from '/@/renderer/components';
|
||||||
import { ActionRequiredContainer } from '/@/renderer/features/action-required/components/action-required-container';
|
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 { ServerCredentialRequired } from '/@/renderer/features/action-required/components/server-credential-required';
|
||||||
import { ServerRequired } from '/@/renderer/features/action-required/components/server-required';
|
import { ServerRequired } from '/@/renderer/features/action-required/components/server-required';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
|
||||||
|
|
||||||
const ActionRequiredRoute = () => {
|
const ActionRequiredRoute = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
const [isMpvRequired, setIsMpvRequired] = useState(false);
|
|
||||||
const isServerRequired = !currentServer;
|
const isServerRequired = !currentServer;
|
||||||
const isCredentialRequired = false;
|
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 = [
|
const checks = [
|
||||||
{
|
|
||||||
component: <MpvRequired />,
|
|
||||||
title: t('error.mpvRequired', { postProcess: 'sentenceCase' }),
|
|
||||||
valid: !isMpvRequired,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
component: <ServerCredentialRequired />,
|
component: <ServerCredentialRequired />,
|
||||||
title: t('error.credentialsRequired', { postProcess: 'sentenceCase' }),
|
title: t('error.credentialsRequired', { postProcess: 'sentenceCase' }),
|
||||||
|
|
|
@ -51,7 +51,7 @@ import {
|
||||||
usePlayerStore,
|
usePlayerStore,
|
||||||
useQueueControls,
|
useQueueControls,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { usePlayerType } from '/@/renderer/store/settings.store';
|
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import { Play, PlaybackType } from '/@/renderer/types';
|
import { Play, PlaybackType } from '/@/renderer/types';
|
||||||
|
|
||||||
type ContextMenuContextProps = {
|
type ContextMenuContextProps = {
|
||||||
|
@ -575,7 +575,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
[ctx.data, ctx.dataNodes, updateRatingMutation],
|
[ctx.data, ctx.dataNodes, updateRatingMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const playerType = usePlayerType();
|
const playbackType = usePlaybackType();
|
||||||
const { moveToBottomOfQueue, moveToTopOfQueue, removeFromQueue } = useQueueControls();
|
const { moveToBottomOfQueue, moveToTopOfQueue, removeFromQueue } = useQueueControls();
|
||||||
|
|
||||||
const handleMoveToBottom = useCallback(() => {
|
const handleMoveToBottom = useCallback(() => {
|
||||||
|
@ -584,10 +584,10 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
|
|
||||||
const playerData = moveToBottomOfQueue(uniqueIds);
|
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
}, [ctx.dataNodes, moveToBottomOfQueue, playerType]);
|
}, [ctx.dataNodes, moveToBottomOfQueue, playbackType]);
|
||||||
|
|
||||||
const handleMoveToTop = useCallback(() => {
|
const handleMoveToTop = useCallback(() => {
|
||||||
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
|
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
|
||||||
|
@ -595,10 +595,10 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
|
|
||||||
const playerData = moveToTopOfQueue(uniqueIds);
|
const playerData = moveToTopOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
}, [ctx.dataNodes, moveToTopOfQueue, playerType]);
|
}, [ctx.dataNodes, moveToTopOfQueue, playbackType]);
|
||||||
|
|
||||||
const handleRemoveSelected = useCallback(() => {
|
const handleRemoveSelected = useCallback(() => {
|
||||||
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
|
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
|
||||||
|
@ -608,7 +608,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
const playerData = removeFromQueue(uniqueIds);
|
const playerData = removeFromQueue(uniqueIds);
|
||||||
const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong?.uniqueId);
|
const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong?.uniqueId);
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
if (isCurrentSongRemoved) {
|
if (isCurrentSongRemoved) {
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
} else {
|
} else {
|
||||||
|
@ -621,7 +621,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
if (isCurrentSongRemoved) {
|
if (isCurrentSongRemoved) {
|
||||||
remote?.updateSong({ song: playerData.current.song });
|
remote?.updateSong({ song: playerData.current.song });
|
||||||
}
|
}
|
||||||
}, [ctx.dataNodes, ctx.tableApi, playerType, removeFromQueue]);
|
}, [ctx.dataNodes, ctx.tableApi, playbackType, removeFromQueue]);
|
||||||
|
|
||||||
const handleDeselectAll = useCallback(() => {
|
const handleDeselectAll = useCallback(() => {
|
||||||
ctx.tableApi?.deselectAll();
|
ctx.tableApi?.deselectAll();
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
useCurrentStatus,
|
useCurrentStatus,
|
||||||
useCurrentTime,
|
useCurrentTime,
|
||||||
useLyricsSettings,
|
useLyricsSettings,
|
||||||
usePlayerType,
|
usePlaybackType,
|
||||||
useSeeked,
|
useSeeked,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
||||||
|
@ -59,7 +59,7 @@ export const SynchronizedLyrics = ({
|
||||||
}: SynchronizedLyricsProps) => {
|
}: SynchronizedLyricsProps) => {
|
||||||
const playersRef = PlayersRef;
|
const playersRef = PlayersRef;
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
const playerType = usePlayerType();
|
const playbackType = usePlaybackType();
|
||||||
const now = useCurrentTime();
|
const now = useCurrentTime();
|
||||||
const settings = useLyricsSettings();
|
const settings = useLyricsSettings();
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ export const SynchronizedLyrics = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentTime = useCallback(async () => {
|
const getCurrentTime = useCallback(async () => {
|
||||||
if (isElectron() && playerType !== PlaybackType.WEB) {
|
if (isElectron() && playbackType !== PlaybackType.WEB) {
|
||||||
if (mpvPlayer) {
|
if (mpvPlayer) {
|
||||||
return mpvPlayer.getCurrentTime();
|
return mpvPlayer.getCurrentTime();
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ export const SynchronizedLyrics = ({
|
||||||
if (!player) return 0;
|
if (!player) return 0;
|
||||||
|
|
||||||
return player.currentTime;
|
return player.currentTime;
|
||||||
}, [playerType, playersRef]);
|
}, [playbackType, playersRef]);
|
||||||
|
|
||||||
const setCurrentLyric = useCallback(
|
const setCurrentLyric = useCallback(
|
||||||
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
||||||
|
@ -222,7 +222,7 @@ export const SynchronizedLyrics = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [getCurrentTime, lyrics, playerType, setCurrentLyric, status]);
|
}, [getCurrentTime, lyrics, playbackType, setCurrentLyric, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This handler is used to deal with changes to the current delay. If the offset
|
// This handler is used to deal with changes to the current delay. If the offset
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import { Song } from '/@/renderer/api/types';
|
import { Song } from '/@/renderer/api/types';
|
||||||
import { usePlayerControls, useQueueControls } from '/@/renderer/store';
|
import { usePlayerControls, useQueueControls } from '/@/renderer/store';
|
||||||
import { PlaybackType, TableType } from '/@/renderer/types';
|
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 { usePlayerStore, useSetCurrentTime } from '../../../store/player.store';
|
||||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
|
|
||||||
const { pause } = usePlayerControls();
|
const { pause } = usePlayerControls();
|
||||||
|
|
||||||
const playerType = usePlayerType();
|
const playbackType = usePlaybackType();
|
||||||
const setCurrentTime = useSetCurrentTime();
|
const setCurrentTime = useSetCurrentTime();
|
||||||
|
|
||||||
const handleMoveToBottom = () => {
|
const handleMoveToBottom = () => {
|
||||||
|
@ -44,7 +44,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
|
|
||||||
const playerData = moveToBottomOfQueue(uniqueIds);
|
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -56,7 +56,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
|
|
||||||
const playerData = moveToTopOfQueue(uniqueIds);
|
const playerData = moveToTopOfQueue(uniqueIds);
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -70,7 +70,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
const playerData = removeFromQueue(uniqueIds);
|
const playerData = removeFromQueue(uniqueIds);
|
||||||
const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong.uniqueId);
|
const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong.uniqueId);
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
if (isCurrentSongRemoved) {
|
if (isCurrentSongRemoved) {
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,7 +86,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
const handleClearQueue = () => {
|
const handleClearQueue = () => {
|
||||||
const playerData = clearQueue();
|
const playerData = clearQueue();
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
mpvPlayer!.pause();
|
mpvPlayer!.pause();
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
||||||
const handleShuffleQueue = () => {
|
const handleShuffleQueue = () => {
|
||||||
const playerData = shuffleQueue();
|
const playerData = shuffleQueue();
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
useVolume,
|
useVolume,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import {
|
import {
|
||||||
usePlayerType,
|
usePlaybackType,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useSettingsStoreActions,
|
useSettingsStoreActions,
|
||||||
useTableSettings,
|
useTableSettings,
|
||||||
|
@ -56,7 +56,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
const { setAppStore } = useAppStoreActions();
|
const { setAppStore } = useAppStoreActions();
|
||||||
const tableConfig = useTableSettings(type);
|
const tableConfig = useTableSettings(type);
|
||||||
const [gridApi, setGridApi] = useState<AgGridReactType | undefined>();
|
const [gridApi, setGridApi] = useState<AgGridReactType | undefined>();
|
||||||
const playerType = usePlayerType();
|
const playbackType = usePlaybackType();
|
||||||
const { play } = usePlayerControls();
|
const { play } = usePlayerControls();
|
||||||
const volume = useVolume();
|
const volume = useVolume();
|
||||||
const isFocused = useAppFocus();
|
const isFocused = useAppFocus();
|
||||||
|
@ -87,7 +87,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
status: PlayerStatus.PLAYING,
|
status: PlayerStatus.PLAYING,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.volume(volume);
|
mpvPlayer!.volume(volume);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
mpvPlayer!.play();
|
mpvPlayer!.play();
|
||||||
|
@ -111,7 +111,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
|
|
||||||
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
|
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ import {
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import {
|
import {
|
||||||
useHotkeySettings,
|
useHotkeySettings,
|
||||||
usePlayerType,
|
usePlaybackType,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
} from '/@/renderer/store/settings.store';
|
} from '/@/renderer/store/settings.store';
|
||||||
import { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types';
|
import { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types';
|
||||||
|
@ -99,7 +99,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const skip = useSettingsStore((state) => state.general.skipButtons);
|
const skip = useSettingsStore((state) => state.general.skipButtons);
|
||||||
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
|
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
|
||||||
const playerType = usePlayerType();
|
const playbackType = usePlaybackType();
|
||||||
const player1 = playersRef?.current?.player1;
|
const player1 = playersRef?.current?.player1;
|
||||||
const player2 = playersRef?.current?.player2;
|
const player2 = playersRef?.current?.player2;
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
|
@ -134,7 +134,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
let interval: any;
|
let interval: any;
|
||||||
|
|
||||||
if (status === PlayerStatus.PLAYING && !isSeeking) {
|
if (status === PlayerStatus.PLAYING && !isSeeking) {
|
||||||
if (!isElectron() || playerType === PlaybackType.WEB) {
|
if (!isElectron() || playbackType === PlaybackType.WEB) {
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
setCurrentTime(currentPlayerRef.getCurrentTime());
|
setCurrentTime(currentPlayerRef.getCurrentTime());
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
@ -144,7 +144,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [currentPlayerRef, isSeeking, setCurrentTime, playerType, status]);
|
}, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]);
|
||||||
|
|
||||||
const [seekValue, setSeekValue] = useState(0);
|
const [seekValue, setSeekValue] = useState(0);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import styled from 'styled-components';
|
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 { PlaybackType } from '/@/renderer/types';
|
||||||
import { AudioPlayer } from '/@/renderer/components';
|
import { AudioPlayer } from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
|
@ -64,6 +64,7 @@ const remote = isElectron() ? window.electron.remote : null;
|
||||||
export const Playerbar = () => {
|
export const Playerbar = () => {
|
||||||
const playersRef = PlayersRef;
|
const playersRef = PlayersRef;
|
||||||
const settings = useSettingsStore((state) => state.playback);
|
const settings = useSettingsStore((state) => state.playback);
|
||||||
|
const playbackType = usePlaybackType();
|
||||||
const volume = useVolume();
|
const volume = useVolume();
|
||||||
const player1 = usePlayer1Data();
|
const player1 = usePlayer1Data();
|
||||||
const player2 = usePlayer2Data();
|
const player2 = usePlayer2Data();
|
||||||
|
@ -96,7 +97,7 @@ export const Playerbar = () => {
|
||||||
<RightControls />
|
<RightControls />
|
||||||
</RightGridItem>
|
</RightGridItem>
|
||||||
</PlayerbarControlsGrid>
|
</PlayerbarControlsGrid>
|
||||||
{settings.type === PlaybackType.WEB && (
|
{playbackType === PlaybackType.WEB && (
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
ref={playersRef}
|
ref={playersRef}
|
||||||
autoNext={autoNextFn}
|
autoNext={autoNextFn}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// import { write, writeFile } from 'fs';
|
|
||||||
// import { deflate } from 'zlib';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/renderer/types';
|
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/renderer/types';
|
||||||
|
@ -13,7 +11,7 @@ import {
|
||||||
useSetCurrentTime,
|
useSetCurrentTime,
|
||||||
useShuffleStatus,
|
useShuffleStatus,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { usePlayerType, useSettingsStore } from '/@/renderer/store/settings.store';
|
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { QueueSong } from '/@/renderer/api/types';
|
import { QueueSong } from '/@/renderer/api/types';
|
||||||
|
@ -32,7 +30,6 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { playersRef } = args;
|
const { playersRef } = args;
|
||||||
|
|
||||||
const settings = useSettingsStore((state) => state.playback);
|
|
||||||
const currentPlayer = useCurrentPlayer();
|
const currentPlayer = useCurrentPlayer();
|
||||||
const { setShuffle, setRepeat, play, pause, previous, next, setCurrentIndex, autoNext } =
|
const { setShuffle, setRepeat, play, pause, previous, next, setCurrentIndex, autoNext } =
|
||||||
usePlayerControls();
|
usePlayerControls();
|
||||||
|
@ -41,7 +38,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
const playerStatus = useCurrentStatus();
|
const playerStatus = useCurrentStatus();
|
||||||
const repeatStatus = useRepeatStatus();
|
const repeatStatus = useRepeatStatus();
|
||||||
const shuffleStatus = useShuffleStatus();
|
const shuffleStatus = useShuffleStatus();
|
||||||
const playerType = usePlayerType();
|
const playbackType = usePlaybackType();
|
||||||
const player1Ref = playersRef?.current?.player1;
|
const player1Ref = playersRef?.current?.player1;
|
||||||
const player2Ref = playersRef?.current?.player2;
|
const player2Ref = playersRef?.current?.player2;
|
||||||
const currentPlayerRef = currentPlayer === 1 ? player1Ref : player2Ref;
|
const currentPlayerRef = currentPlayer === 1 ? player1Ref : player2Ref;
|
||||||
|
@ -77,7 +74,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}, [player1Ref, player2Ref, resetPlayers]);
|
}, [player1Ref, player2Ref, resetPlayers]);
|
||||||
|
|
||||||
const isMpvPlayer = isElectron() && settings.type === PlaybackType.LOCAL;
|
const isMpvPlayer = isElectron() && playbackType === PlaybackType.LOCAL;
|
||||||
|
|
||||||
const mprisUpdateSong = (args?: {
|
const mprisUpdateSong = (args?: {
|
||||||
currentTime?: number;
|
currentTime?: number;
|
||||||
|
@ -282,13 +279,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
|
|
||||||
switch (repeatStatus) {
|
switch (repeatStatus) {
|
||||||
case PlayerRepeat.NONE:
|
case PlayerRepeat.NONE:
|
||||||
handleRepeatNone[playerType]();
|
handleRepeatNone[playbackType]();
|
||||||
break;
|
break;
|
||||||
case PlayerRepeat.ALL:
|
case PlayerRepeat.ALL:
|
||||||
handleRepeatAll[playerType]();
|
handleRepeatAll[playbackType]();
|
||||||
break;
|
break;
|
||||||
case PlayerRepeat.ONE:
|
case PlayerRepeat.ONE:
|
||||||
handleRepeatOne[playerType]();
|
handleRepeatOne[playbackType]();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -299,7 +296,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
checkIsLastTrack,
|
checkIsLastTrack,
|
||||||
pause,
|
pause,
|
||||||
play,
|
play,
|
||||||
playerType,
|
playbackType,
|
||||||
repeatStatus,
|
repeatStatus,
|
||||||
resetPlayers,
|
resetPlayers,
|
||||||
setCurrentIndex,
|
setCurrentIndex,
|
||||||
|
@ -380,13 +377,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
|
|
||||||
switch (repeatStatus) {
|
switch (repeatStatus) {
|
||||||
case PlayerRepeat.NONE:
|
case PlayerRepeat.NONE:
|
||||||
handleRepeatNone[playerType]();
|
handleRepeatNone[playbackType]();
|
||||||
break;
|
break;
|
||||||
case PlayerRepeat.ALL:
|
case PlayerRepeat.ALL:
|
||||||
handleRepeatAll[playerType]();
|
handleRepeatAll[playbackType]();
|
||||||
break;
|
break;
|
||||||
case PlayerRepeat.ONE:
|
case PlayerRepeat.ONE:
|
||||||
handleRepeatOne[playerType]();
|
handleRepeatOne[playbackType]();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -398,7 +395,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
checkIsLastTrack,
|
checkIsLastTrack,
|
||||||
next,
|
next,
|
||||||
pause,
|
pause,
|
||||||
playerType,
|
playbackType,
|
||||||
repeatStatus,
|
repeatStatus,
|
||||||
resetPlayers,
|
resetPlayers,
|
||||||
setCurrentIndex,
|
setCurrentIndex,
|
||||||
|
@ -511,13 +508,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
|
|
||||||
switch (repeatStatus) {
|
switch (repeatStatus) {
|
||||||
case PlayerRepeat.NONE:
|
case PlayerRepeat.NONE:
|
||||||
handleRepeatNone[playerType]();
|
handleRepeatNone[playbackType]();
|
||||||
break;
|
break;
|
||||||
case PlayerRepeat.ALL:
|
case PlayerRepeat.ALL:
|
||||||
handleRepeatAll[playerType]();
|
handleRepeatAll[playbackType]();
|
||||||
break;
|
break;
|
||||||
case PlayerRepeat.ONE:
|
case PlayerRepeat.ONE:
|
||||||
handleRepeatOne[playerType]();
|
handleRepeatOne[playbackType]();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -531,7 +528,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
handleScrobbleFromSongRestart,
|
handleScrobbleFromSongRestart,
|
||||||
isMpvPlayer,
|
isMpvPlayer,
|
||||||
pause,
|
pause,
|
||||||
playerType,
|
playbackType,
|
||||||
previous,
|
previous,
|
||||||
queue.length,
|
queue.length,
|
||||||
repeatStatus,
|
repeatStatus,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
||||||
import { usePlayerType } from '/@/renderer/store/settings.store';
|
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import {
|
import {
|
||||||
PlayQueueAddOptions,
|
PlayQueueAddOptions,
|
||||||
Play,
|
Play,
|
||||||
|
@ -65,7 +65,7 @@ const addToQueue = usePlayerStore.getState().actions.addToQueue;
|
||||||
export const useHandlePlayQueueAdd = () => {
|
export const useHandlePlayQueueAdd = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const playerType = usePlayerType();
|
const playbackType = usePlaybackType();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { play } = usePlayerControls();
|
const { play } = usePlayerControls();
|
||||||
const timeoutIds = useRef<Record<string, ReturnType<typeof setTimeout>> | null>({});
|
const timeoutIds = useRef<Record<string, ReturnType<typeof setTimeout>> | null>({});
|
||||||
|
@ -170,7 +170,7 @@ export const useHandlePlayQueueAdd = () => {
|
||||||
const hadSong = usePlayerStore.getState().queue.default.length > 0;
|
const hadSong = usePlayerStore.getState().queue.default.length > 0;
|
||||||
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
|
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
|
||||||
|
|
||||||
if (playerType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.volume(usePlayerStore.getState().volume);
|
mpvPlayer!.volume(usePlayerStore.getState().volume);
|
||||||
|
|
||||||
if (playType === Play.NOW || !hadSong) {
|
if (playType === Play.NOW || !hadSong) {
|
||||||
|
@ -198,7 +198,7 @@ export const useHandlePlayQueueAdd = () => {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[play, playerType, queryClient, server, t],
|
[play, playbackType, queryClient, server, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
return handlePlayQueueAdd;
|
return handlePlayQueueAdd;
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Divider, Stack } from '@mantine/core';
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
import isElectron from 'is-electron';
|
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 {
|
import {
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingOption,
|
SettingOption,
|
||||||
|
@ -9,10 +17,13 @@ import {
|
||||||
import {
|
import {
|
||||||
SettingsState,
|
SettingsState,
|
||||||
usePlaybackSettings,
|
usePlaybackSettings,
|
||||||
|
useSettingsStore,
|
||||||
useSettingsStoreActions,
|
useSettingsStoreActions,
|
||||||
} from '/@/renderer/store/settings.store';
|
} from '/@/renderer/store/settings.store';
|
||||||
import { PlaybackType } from '/@/renderer/types';
|
import { PlaybackType } from '/@/renderer/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
@ -64,11 +75,20 @@ export const MpvSettings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const settings = usePlaybackSettings();
|
const settings = usePlaybackSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
const { pause } = usePlayerControls();
|
||||||
|
const { clearQueue } = useQueueControls();
|
||||||
|
|
||||||
const [mpvPath, setMpvPath] = useState('');
|
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);
|
localSettings?.set('mpv_path', e.path);
|
||||||
|
setMpvPath(e.path);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -100,6 +120,22 @@ export const MpvSettings = () => {
|
||||||
mpvPlayer?.setProperties(mpvSetting);
|
mpvPlayer?.setProperties(mpvSetting);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReloadMpv = () => {
|
||||||
|
pause();
|
||||||
|
clearQueue();
|
||||||
|
|
||||||
|
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||||
|
const properties: Record<string, any> = {
|
||||||
|
speed: usePlayerStore.getState().current.speed,
|
||||||
|
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||||
|
};
|
||||||
|
mpvPlayer?.restart({
|
||||||
|
binaryPath: mpvPath || undefined,
|
||||||
|
extraParameters,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleSetExtraParameters = (data: string[]) => {
|
const handleSetExtraParameters = (data: string[]) => {
|
||||||
setSettings({
|
setSettings({
|
||||||
playback: {
|
playback: {
|
||||||
|
@ -112,11 +148,38 @@ export const MpvSettings = () => {
|
||||||
const options: SettingOption[] = [
|
const options: SettingOption[] = [
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<FileInput
|
<Group spacing="sm">
|
||||||
placeholder={mpvPath}
|
<Button
|
||||||
width={225}
|
tooltip={{
|
||||||
onChange={handleSetMpvPath}
|
label: t('common.reload', { postProcess: 'titleCase' }),
|
||||||
/>
|
openDelay: 0,
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={handleReloadMpv}
|
||||||
|
>
|
||||||
|
<RiRestartLine />
|
||||||
|
</Button>
|
||||||
|
<FileInput
|
||||||
|
placeholder={mpvPath}
|
||||||
|
rightSection={
|
||||||
|
mpvPath && (
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
tooltip={{
|
||||||
|
label: t('common.clear', { postProcess: 'titleCase' }),
|
||||||
|
openDelay: 0,
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => handleSetMpvPath(null)}
|
||||||
|
>
|
||||||
|
<RiCloseLine />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
width={200}
|
||||||
|
onChange={handleSetMpvPath}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
),
|
),
|
||||||
description: t('setting.mpvExecutablePath', {
|
description: t('setting.mpvExecutablePath', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
|
|
|
@ -1,30 +1,42 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useEffect } from 'react';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { Navigate, Outlet } from 'react-router-dom';
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
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 = () => {
|
export const AppOutlet = () => {
|
||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
|
const setFallback = useSetPlayerFallback();
|
||||||
|
|
||||||
const isActionsRequired = useMemo(() => {
|
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 isServerRequired = !currentServer;
|
||||||
|
|
||||||
const actions = [isServerRequired, isMpvRequired()];
|
const actions = [isServerRequired];
|
||||||
const isActionRequired = actions.some((c) => c);
|
const isActionRequired = actions.some((c) => c);
|
||||||
|
|
||||||
return isActionRequired;
|
return isActionRequired;
|
||||||
}, [currentServer]);
|
}, [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) {
|
if (isActionsRequired) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
|
|
|
@ -22,6 +22,7 @@ export interface PlayerState {
|
||||||
status: PlayerStatus;
|
status: PlayerStatus;
|
||||||
time: number;
|
time: number;
|
||||||
};
|
};
|
||||||
|
fallback: boolean | null;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
queue: {
|
queue: {
|
||||||
default: QueueSong[];
|
default: QueueSong[];
|
||||||
|
@ -85,6 +86,7 @@ export interface PlayerSlice extends PlayerState {
|
||||||
setCurrentSpeed: (speed: number) => void;
|
setCurrentSpeed: (speed: number) => void;
|
||||||
setCurrentTime: (time: number, seek?: boolean) => void;
|
setCurrentTime: (time: number, seek?: boolean) => void;
|
||||||
setCurrentTrack: (uniqueId: string) => PlayerData;
|
setCurrentTrack: (uniqueId: string) => PlayerData;
|
||||||
|
setFallback: (fallback: boolean | null) => boolean;
|
||||||
setFavorite: (ids: string[], favorite: boolean) => string[];
|
setFavorite: (ids: string[], favorite: boolean) => string[];
|
||||||
setMuted: (muted: boolean) => void;
|
setMuted: (muted: boolean) => void;
|
||||||
setRating: (ids: string[], rating: number | null) => string[];
|
setRating: (ids: string[], rating: number | null) => string[];
|
||||||
|
@ -806,6 +808,13 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
|
|
||||||
return get().actions.getPlayerData();
|
return get().actions.getPlayerData();
|
||||||
},
|
},
|
||||||
|
setFallback: (fallback) => {
|
||||||
|
set((state) => {
|
||||||
|
state.fallback = fallback;
|
||||||
|
});
|
||||||
|
|
||||||
|
return fallback || false;
|
||||||
|
},
|
||||||
setFavorite: (ids, favorite) => {
|
setFavorite: (ids, favorite) => {
|
||||||
const { default: queue } = get().queue;
|
const { default: queue } = get().queue;
|
||||||
const foundUniqueIds = [];
|
const foundUniqueIds = [];
|
||||||
|
@ -953,6 +962,7 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
status: PlayerStatus.PAUSED,
|
status: PlayerStatus.PAUSED,
|
||||||
time: 0,
|
time: 0,
|
||||||
},
|
},
|
||||||
|
fallback: null,
|
||||||
muted: false,
|
muted: false,
|
||||||
queue: {
|
queue: {
|
||||||
default: [],
|
default: [],
|
||||||
|
@ -973,7 +983,7 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
},
|
},
|
||||||
name: 'store_player',
|
name: 'store_player',
|
||||||
partialize: (state) => {
|
partialize: (state) => {
|
||||||
const notPersisted = ['queue', 'current', 'entry'];
|
const notPersisted = ['queue', 'current', 'entry', 'fallback'];
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(state).filter(([key]) => !notPersisted.includes(key)),
|
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 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 useSetCurrentSpeed = () => usePlayerStore((state) => state.actions.setCurrentSpeed);
|
||||||
|
|
||||||
export const useSetQueueFavorite = () => usePlayerStore((state) => state.actions.setFavorite);
|
export const useSetQueueFavorite = () => usePlayerStore((state) => state.actions.setFavorite);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
} from '/@/renderer/types';
|
} from '/@/renderer/types';
|
||||||
import { randomString } from '/@/renderer/utils';
|
import { randomString } from '/@/renderer/utils';
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { usePlayerStore } from '/@/renderer/store/player.store';
|
||||||
|
|
||||||
const utils = isElectron() ? window.electron.utils : null;
|
const utils = isElectron() ? window.electron.utils : null;
|
||||||
|
|
||||||
|
@ -381,7 +382,7 @@ const initialState: SettingsState = {
|
||||||
scrobbleAtPercentage: 75,
|
scrobbleAtPercentage: 75,
|
||||||
},
|
},
|
||||||
style: PlaybackStyle.GAPLESS,
|
style: PlaybackStyle.GAPLESS,
|
||||||
type: PlaybackType.LOCAL,
|
type: PlaybackType.WEB,
|
||||||
},
|
},
|
||||||
remote: {
|
remote: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -616,7 +617,16 @@ export const useTableSettings = (type: TableType) =>
|
||||||
|
|
||||||
export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow);
|
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 = () =>
|
export const usePlayButtonBehavior = () =>
|
||||||
useSettingsStore((state) => state.general.playButtonBehavior, shallow);
|
useSettingsStore((state) => state.general.playButtonBehavior, shallow);
|
||||||
|
|
Reference in a new issue