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