mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 10:23:33 +00:00
Add MPRIS support (#25)
* Stop mpv on app close for linux/macOS (#20) * Add initial MPRIS support * Fix mpv path check
This commit is contained in:
parent
0f7f4b969f
commit
23f84d68e8
19 changed files with 1672 additions and 144 deletions
|
|
@ -1,84 +1,9 @@
|
|||
import { ipcMain } from 'electron';
|
||||
import uniq from 'lodash/uniq';
|
||||
import MpvAPI from 'node-mpv';
|
||||
import { store } from '../settings';
|
||||
import { getMainWindow } from '../../../main';
|
||||
import { mpv } from '../../../main';
|
||||
import { PlayerData } from '/@/renderer/store';
|
||||
|
||||
declare module 'node-mpv';
|
||||
|
||||
const BINARY_PATH = store.get('mpv_path') as string | undefined;
|
||||
const MPV_PARAMETERS = store.get('mpv_parameters') as Array<string> | undefined;
|
||||
const DEFAULT_MPV_PARAMETERS = () => {
|
||||
const parameters = [];
|
||||
if (
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio=weak') ||
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio=no') ||
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio=yes') ||
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio')
|
||||
) {
|
||||
parameters.push('--gapless-audio=yes');
|
||||
}
|
||||
|
||||
if (
|
||||
!MPV_PARAMETERS?.includes('--prefetch-playlist=no') ||
|
||||
!MPV_PARAMETERS?.includes('--prefetch-playlist=yes') ||
|
||||
!MPV_PARAMETERS?.includes('--prefetch-playlist')
|
||||
) {
|
||||
parameters.push('--prefetch-playlist=yes');
|
||||
}
|
||||
|
||||
return parameters;
|
||||
};
|
||||
|
||||
const mpv = new MpvAPI(
|
||||
{
|
||||
audio_only: true,
|
||||
auto_restart: true,
|
||||
binary: BINARY_PATH || '',
|
||||
time_update: 1,
|
||||
},
|
||||
MPV_PARAMETERS
|
||||
? uniq([...DEFAULT_MPV_PARAMETERS(), ...MPV_PARAMETERS])
|
||||
: DEFAULT_MPV_PARAMETERS(),
|
||||
);
|
||||
|
||||
mpv.start().catch((error) => {
|
||||
console.log('error starting mpv', error);
|
||||
});
|
||||
|
||||
mpv.on('status', (status) => {
|
||||
if (status.property === 'playlist-pos') {
|
||||
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');
|
||||
});
|
||||
|
||||
mpv.on('quit', () => {
|
||||
console.log('mpv quit');
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Starts the player
|
||||
ipcMain.on('player-play', async () => {
|
||||
await mpv.play();
|
||||
|
|
@ -161,5 +86,5 @@ ipcMain.on('player-mute', async () => {
|
|||
});
|
||||
|
||||
ipcMain.on('player-quit', async () => {
|
||||
await mpv.quit();
|
||||
await mpv.stop();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
import './mpris';
|
||||
168
src/main/features/linux/mpris.ts
Normal file
168
src/main/features/linux/mpris.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { ipcMain } from 'electron';
|
||||
import Player from 'mpris-service';
|
||||
import { QueueSong, RelatedArtist } from '../../../renderer/api/types';
|
||||
import { getMainWindow } from '../../main';
|
||||
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/renderer/types';
|
||||
|
||||
const mprisPlayer = Player({
|
||||
identity: 'Feishin',
|
||||
maximumRate: 1.0,
|
||||
minimumRate: 1.0,
|
||||
name: 'Feishin',
|
||||
rate: 1.0,
|
||||
supportedInterfaces: ['player'],
|
||||
supportedMimeTypes: ['audio/mpeg', 'application/ogg'],
|
||||
supportedUriSchemes: ['file'],
|
||||
});
|
||||
|
||||
mprisPlayer.on('quit', () => {
|
||||
process.exit();
|
||||
});
|
||||
|
||||
mprisPlayer.on('stop', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-stop');
|
||||
mprisPlayer.playbackStatus = 'Paused';
|
||||
});
|
||||
|
||||
mprisPlayer.on('pause', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-pause');
|
||||
mprisPlayer.playbackStatus = 'Paused';
|
||||
});
|
||||
|
||||
mprisPlayer.on('play', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-play');
|
||||
mprisPlayer.playbackStatus = 'Playing';
|
||||
});
|
||||
|
||||
mprisPlayer.on('playpause', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-play-pause');
|
||||
if (mprisPlayer.playbackStatus !== 'Playing') {
|
||||
mprisPlayer.playbackStatus = 'Playing';
|
||||
} else {
|
||||
mprisPlayer.playbackStatus = 'Paused';
|
||||
}
|
||||
});
|
||||
|
||||
mprisPlayer.on('next', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-next');
|
||||
|
||||
if (mprisPlayer.playbackStatus !== 'Playing') {
|
||||
mprisPlayer.playbackStatus = 'Playing';
|
||||
}
|
||||
});
|
||||
|
||||
mprisPlayer.on('previous', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-previous');
|
||||
|
||||
if (mprisPlayer.playbackStatus !== 'Playing') {
|
||||
mprisPlayer.playbackStatus = Player.PLAYBACK_STATUS_PLAYING;
|
||||
}
|
||||
});
|
||||
|
||||
mprisPlayer.on('volume', (event: any) => {
|
||||
getMainWindow()?.webContents.send('mpris-request-volume', {
|
||||
volume: event,
|
||||
});
|
||||
});
|
||||
|
||||
mprisPlayer.on('shuffle', (event: boolean) => {
|
||||
getMainWindow()?.webContents.send('mpris-request-toggle-shuffle', { shuffle: event });
|
||||
mprisPlayer.shuffle = event;
|
||||
});
|
||||
|
||||
mprisPlayer.on('loopStatus', (event: string) => {
|
||||
getMainWindow()?.webContents.send('mpris-request-toggle-repeat', { repeat: event });
|
||||
mprisPlayer.loopStatus = event;
|
||||
});
|
||||
|
||||
mprisPlayer.on('position', (event: any) => {
|
||||
getMainWindow()?.webContents.send('mpris-request-position', {
|
||||
position: event.position / 1e6,
|
||||
});
|
||||
});
|
||||
|
||||
mprisPlayer.on('seek', (event: number) => {
|
||||
getMainWindow()?.webContents.send('mpris-request-seek', {
|
||||
offset: event / 1e6,
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('mpris-update-position', (_event, arg) => {
|
||||
mprisPlayer.getPosition = () => arg * 1e6;
|
||||
});
|
||||
|
||||
ipcMain.on('mpris-update-seek', (_event, arg) => {
|
||||
mprisPlayer.seeked(arg * 1e6);
|
||||
});
|
||||
|
||||
ipcMain.on('mpris-update-volume', (_event, arg) => {
|
||||
mprisPlayer.volume = Number(arg);
|
||||
});
|
||||
|
||||
ipcMain.on('mpris-update-repeat', (_event, arg) => {
|
||||
mprisPlayer.loopStatus = arg;
|
||||
});
|
||||
|
||||
ipcMain.on('mpris-update-shuffle', (_event, arg) => {
|
||||
mprisPlayer.shuffle = arg;
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
'mpris-update-song',
|
||||
(
|
||||
_event,
|
||||
args: {
|
||||
currentTime: number;
|
||||
repeat: PlayerRepeat;
|
||||
shuffle: PlayerShuffle;
|
||||
song: QueueSong;
|
||||
status: PlayerStatus;
|
||||
},
|
||||
) => {
|
||||
const { song, status, repeat, shuffle } = args || {};
|
||||
|
||||
try {
|
||||
mprisPlayer.playbackStatus = status;
|
||||
|
||||
if (repeat) {
|
||||
mprisPlayer.loopStatus =
|
||||
repeat === 'all' ? 'Playlist' : repeat === 'one' ? 'Track' : 'None';
|
||||
}
|
||||
|
||||
if (shuffle) {
|
||||
mprisPlayer.shuffle = shuffle;
|
||||
}
|
||||
|
||||
if (!song) return;
|
||||
|
||||
const upsizedImageUrl = song.imageUrl
|
||||
? song.imageUrl
|
||||
?.replace(/&size=\d+/, '&size=300')
|
||||
.replace(/\?width=\d+/, '?width=300')
|
||||
.replace(/&height=\d+/, '&height=300')
|
||||
: null;
|
||||
|
||||
mprisPlayer.metadata = {
|
||||
'mpris:artUrl': upsizedImageUrl,
|
||||
'mpris:length': song.duration ? Math.round((song.duration || 0) * 1e6) : null,
|
||||
'mpris:trackid': song?.id
|
||||
? mprisPlayer.objectPath(`track/${song.id?.replace('-', '')}`)
|
||||
: '',
|
||||
'xesam:album': song.album || null,
|
||||
'xesam:albumArtist': song.albumArtists?.length ? song.albumArtists[0].name : null,
|
||||
'xesam:artist':
|
||||
song.artists?.length !== 0
|
||||
? song.artists?.map((artist: RelatedArtist) => artist.name)
|
||||
: null,
|
||||
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
||||
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
|
||||
'xesam:title': song.name || null,
|
||||
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
||||
'xesam:useCount':
|
||||
song.playCount !== null && song.playCount !== undefined ? song.playCount : null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -13,12 +13,16 @@ import { app, BrowserWindow, shell, ipcMain, globalShortcut } from 'electron';
|
|||
import electronLocalShortcut from 'electron-localshortcut';
|
||||
import log from 'electron-log';
|
||||
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 { resolveHtmlPath } from './utils';
|
||||
import './features';
|
||||
|
||||
declare module 'node-mpv';
|
||||
|
||||
export default class AppUpdater {
|
||||
constructor() {
|
||||
log.transports.file.level = 'info';
|
||||
|
|
@ -95,6 +99,7 @@ const createWindow = async () => {
|
|||
|
||||
ipcMain.on('window-maximize', () => {
|
||||
mainWindow?.maximize();
|
||||
mainWindow?.webContents.send('renderer-player-quit');
|
||||
});
|
||||
|
||||
ipcMain.on('window-unmaximize', () => {
|
||||
|
|
@ -142,6 +147,7 @@ const createWindow = async () => {
|
|||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
// mainWindow?.webContents.send('renderer-player-quit');
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
|
|
@ -169,16 +175,88 @@ export const getMainWindow = () => {
|
|||
return mainWindow;
|
||||
};
|
||||
|
||||
const BINARY_PATH = store.get('mpv_path') as string | undefined;
|
||||
const MPV_PARAMETERS = store.get('mpv_parameters') as Array<string> | undefined;
|
||||
const DEFAULT_MPV_PARAMETERS = () => {
|
||||
const parameters = [];
|
||||
if (
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio=weak') ||
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio=no') ||
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio=yes') ||
|
||||
!MPV_PARAMETERS?.includes('--gapless-audio')
|
||||
) {
|
||||
parameters.push('--gapless-audio=yes');
|
||||
}
|
||||
|
||||
if (
|
||||
!MPV_PARAMETERS?.includes('--prefetch-playlist=no') ||
|
||||
!MPV_PARAMETERS?.includes('--prefetch-playlist=yes') ||
|
||||
!MPV_PARAMETERS?.includes('--prefetch-playlist')
|
||||
) {
|
||||
parameters.push('--prefetch-playlist=yes');
|
||||
}
|
||||
|
||||
return parameters;
|
||||
};
|
||||
|
||||
export const mpv = new MpvAPI(
|
||||
{
|
||||
audio_only: true,
|
||||
auto_restart: true,
|
||||
binary: BINARY_PATH || '',
|
||||
time_update: 1,
|
||||
},
|
||||
MPV_PARAMETERS
|
||||
? uniq([...DEFAULT_MPV_PARAMETERS(), ...MPV_PARAMETERS])
|
||||
: DEFAULT_MPV_PARAMETERS(),
|
||||
);
|
||||
|
||||
mpv.start().catch((error) => {
|
||||
console.log('error starting mpv', error);
|
||||
});
|
||||
|
||||
mpv.on('status', (status) => {
|
||||
if (status.property === 'playlist-pos') {
|
||||
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);
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
mainWindow?.webContents.send('renderer-player-quit');
|
||||
mpv.stop();
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
// Respect the OSX convention of having the application in memory even
|
||||
// after all windows have been closed
|
||||
globalShortcut.unregisterAll();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
} else {
|
||||
mpv.stop();
|
||||
mainWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { PlayerData } from '../renderer/store';
|
|||
import { browser } from './preload/browser';
|
||||
import { ipc } from './preload/ipc';
|
||||
import { localSettings } from './preload/local-settings';
|
||||
import { mpris } from './preload/mpris';
|
||||
import { mpvPlayer, mpvPlayerListener } from './preload/mpv-player';
|
||||
import { utils } from './preload/utils';
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
browser,
|
||||
|
|
@ -104,6 +106,8 @@ contextBridge.exposeInMainWorld('electron', {
|
|||
},
|
||||
},
|
||||
localSettings,
|
||||
mpris,
|
||||
mpvPlayer,
|
||||
mpvPlayerListener,
|
||||
utils,
|
||||
});
|
||||
|
|
|
|||
70
src/main/preload/mpris.ts
Normal file
70
src/main/preload/mpris.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { IpcRendererEvent, ipcRenderer } from 'electron';
|
||||
import { QueueSong } from '/@/renderer/api/types';
|
||||
|
||||
const updateSong = (args: { currentTime: number; song: QueueSong }) => {
|
||||
ipcRenderer.send('mpris-update-song', args);
|
||||
};
|
||||
|
||||
const updatePosition = (timeSec: number) => {
|
||||
ipcRenderer.send('mpris-update-position', timeSec);
|
||||
};
|
||||
|
||||
const updateSeek = (timeSec: number) => {
|
||||
ipcRenderer.send('mpris-update-seek', timeSec);
|
||||
};
|
||||
|
||||
const updateVolume = (volume: number) => {
|
||||
ipcRenderer.send('mpris-update-volume', volume);
|
||||
};
|
||||
|
||||
const updateRepeat = (repeat: string) => {
|
||||
ipcRenderer.send('mpris-update-repeat', repeat);
|
||||
};
|
||||
|
||||
const updateShuffle = (shuffle: boolean) => {
|
||||
ipcRenderer.send('mpris-update-shuffle', shuffle);
|
||||
};
|
||||
|
||||
const toggleRepeat = () => {
|
||||
ipcRenderer.send('mpris-toggle-repeat');
|
||||
};
|
||||
|
||||
const toggleShuffle = () => {
|
||||
ipcRenderer.send('mpris-toggle-shuffle');
|
||||
};
|
||||
|
||||
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
|
||||
ipcRenderer.on('mpris-request-position', cb);
|
||||
};
|
||||
|
||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
||||
ipcRenderer.on('mpris-request-seek', cb);
|
||||
};
|
||||
|
||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
||||
ipcRenderer.on('mpris-request-volume', cb);
|
||||
};
|
||||
|
||||
const requestToggleRepeat = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('mpris-request-toggle-repeat', cb);
|
||||
};
|
||||
|
||||
const requestToggleShuffle = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
||||
};
|
||||
|
||||
export const mpris = {
|
||||
requestPosition,
|
||||
requestSeek,
|
||||
requestToggleRepeat,
|
||||
requestToggleShuffle,
|
||||
requestVolume,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
updatePosition,
|
||||
updateRepeat,
|
||||
updateSeek,
|
||||
updateShuffle,
|
||||
updateSong,
|
||||
updateVolume,
|
||||
};
|
||||
7
src/main/preload/utils.ts
Normal file
7
src/main/preload/utils.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { isMacOS, isWindows, isLinux } from '../utils';
|
||||
|
||||
export const utils = {
|
||||
isLinux,
|
||||
isMacOS,
|
||||
isWindows,
|
||||
};
|
||||
|
|
@ -24,7 +24,12 @@ const ActionRequiredRoute = () => {
|
|||
const getMpvPath = async () => {
|
||||
if (!isElectron()) return setIsMpvRequired(false);
|
||||
const mpvPath = await localSettings.get('mpv_path');
|
||||
return setIsMpvRequired(!mpvPath);
|
||||
|
||||
if (mpvPath) {
|
||||
return setIsMpvRequired(false);
|
||||
}
|
||||
|
||||
return setIsMpvRequired(true);
|
||||
};
|
||||
|
||||
getMpvPath();
|
||||
|
|
@ -48,6 +53,8 @@ const ActionRequiredRoute = () => {
|
|||
},
|
||||
];
|
||||
|
||||
console.log(checks);
|
||||
|
||||
const canReturnHome = checks.every((c) => c.valid);
|
||||
const displayedCheck = checks.find((c) => !c.valid);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
|||
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
|
||||
type QueueProps = {
|
||||
type: TableType;
|
||||
|
|
@ -68,6 +70,11 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
|||
|
||||
const handleDoubleClick = (e: CellDoubleClickedEvent) => {
|
||||
const playerData = setCurrentTrack(e.data.uniqueId);
|
||||
mpris?.updateSong({
|
||||
currentTime: 0,
|
||||
song: playerData.current.song,
|
||||
status: 'Playing',
|
||||
});
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueue(playerData);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import isElectron from 'is-electron';
|
||||
import styled from 'styled-components';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { PlaybackType } from '/@/renderer/types';
|
||||
|
|
@ -50,6 +51,9 @@ const CenterGridItem = styled.div`
|
|||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
|
||||
export const Playerbar = () => {
|
||||
const playersRef = useRef<any>();
|
||||
const settings = useSettingsStore((state) => state.player);
|
||||
|
|
@ -60,6 +64,14 @@ export const Playerbar = () => {
|
|||
const player = useCurrentPlayer();
|
||||
const { autoNext } = usePlayerControls();
|
||||
|
||||
const autoNextFn = useCallback(() => {
|
||||
const playerData = autoNext();
|
||||
mpris?.updateSong({
|
||||
currentTime: 0,
|
||||
song: playerData.current.song,
|
||||
});
|
||||
}, [autoNext]);
|
||||
|
||||
return (
|
||||
<PlayerbarContainer>
|
||||
<PlayerbarControlsGrid>
|
||||
|
|
@ -76,7 +88,7 @@ export const Playerbar = () => {
|
|||
{settings.type === PlaybackType.WEB && (
|
||||
<AudioPlayer
|
||||
ref={playersRef}
|
||||
autoNext={autoNext}
|
||||
autoNext={autoNextFn}
|
||||
crossfadeDuration={settings.crossfadeDuration}
|
||||
crossfadeStyle={settings.crossfadeStyle}
|
||||
currentPlayer={player}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ import {
|
|||
import { usePlayerType, useSettingsStore } 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';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
|
||||
export const useCenterControls = (args: { playersRef: any }) => {
|
||||
const { playersRef } = args;
|
||||
|
|
@ -65,7 +68,27 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
|
||||
const isMpvPlayer = isElectron() && settings.type === PlaybackType.LOCAL;
|
||||
|
||||
const mprisUpdateSong = (args?: {
|
||||
currentTime?: number;
|
||||
song?: QueueSong;
|
||||
status?: PlayerStatus;
|
||||
}) => {
|
||||
const { song, currentTime, status } = args || {};
|
||||
mpris?.updateSong({
|
||||
currentTime: currentTime || usePlayerStore.getState().current.time,
|
||||
repeat: usePlayerStore.getState().repeat,
|
||||
shuffle: usePlayerStore.getState().shuffle,
|
||||
song: song || usePlayerStore.getState().current.song,
|
||||
status:
|
||||
(status || usePlayerStore.getState().current.status) === PlayerStatus.PLAYING
|
||||
? 'Playing'
|
||||
: 'Paused',
|
||||
});
|
||||
};
|
||||
|
||||
const handlePlay = useCallback(() => {
|
||||
mprisUpdateSong({ status: PlayerStatus.PLAYING });
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.play();
|
||||
} else {
|
||||
|
|
@ -76,6 +99,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
}, [currentPlayerRef, isMpvPlayer, play]);
|
||||
|
||||
const handlePause = useCallback(() => {
|
||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.pause();
|
||||
}
|
||||
|
|
@ -84,6 +109,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
}, [isMpvPlayer, pause]);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.stop();
|
||||
} else {
|
||||
|
|
@ -97,24 +124,29 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleToggleShuffle = useCallback(() => {
|
||||
if (shuffleStatus === PlayerShuffle.NONE) {
|
||||
const playerData = setShuffle(PlayerShuffle.TRACK);
|
||||
mpris?.updateShuffle(true);
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
const playerData = setShuffle(PlayerShuffle.NONE);
|
||||
mpris?.updateShuffle(false);
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
}, [setShuffle, shuffleStatus]);
|
||||
|
||||
const handleToggleRepeat = useCallback(() => {
|
||||
if (repeatStatus === PlayerRepeat.NONE) {
|
||||
const playerData = setRepeat(PlayerRepeat.ALL);
|
||||
mpris?.updateRepeat('Playlist');
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
if (repeatStatus === PlayerRepeat.ALL) {
|
||||
const playerData = setRepeat(PlayerRepeat.ONE);
|
||||
mpris?.updateRepeat('Track');
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
mpris?.updateRepeat('None');
|
||||
return setRepeat(PlayerRepeat.NONE);
|
||||
}, [repeatStatus, setRepeat]);
|
||||
|
||||
|
|
@ -132,11 +164,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleRepeatAll = {
|
||||
local: () => {
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.autoNext(playerData);
|
||||
play();
|
||||
},
|
||||
web: () => {
|
||||
autoNext();
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -144,11 +178,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
if (isLastTrack) {
|
||||
const playerData = setCurrentIndex(0);
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.pause();
|
||||
pause();
|
||||
} else {
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.autoNext(playerData);
|
||||
play();
|
||||
}
|
||||
|
|
@ -156,9 +192,11 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
web: () => {
|
||||
if (isLastTrack) {
|
||||
resetPlayers();
|
||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
||||
pause();
|
||||
} else {
|
||||
autoNext();
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
}
|
||||
},
|
||||
|
|
@ -167,14 +205,17 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleRepeatOne = {
|
||||
local: () => {
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.autoNext(playerData);
|
||||
play();
|
||||
},
|
||||
web: () => {
|
||||
if (isLastTrack) {
|
||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
||||
resetPlayers();
|
||||
} else {
|
||||
autoNext();
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
}
|
||||
},
|
||||
|
|
@ -212,11 +253,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleRepeatAll = {
|
||||
local: () => {
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.next();
|
||||
},
|
||||
web: () => {
|
||||
next();
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -224,22 +267,26 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
if (isLastTrack) {
|
||||
const playerData = setCurrentIndex(0);
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.pause();
|
||||
pause();
|
||||
} else {
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.next();
|
||||
}
|
||||
},
|
||||
web: () => {
|
||||
if (isLastTrack) {
|
||||
setCurrentIndex(0);
|
||||
const playerData = setCurrentIndex(0);
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
pause();
|
||||
} else {
|
||||
next();
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
}
|
||||
},
|
||||
|
|
@ -248,12 +295,14 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleRepeatOne = {
|
||||
local: () => {
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.next();
|
||||
},
|
||||
web: () => {
|
||||
if (!isLastTrack) {
|
||||
next();
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -294,7 +343,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
if (currentTime >= 10) {
|
||||
setCurrentTime(0);
|
||||
handleScrobbleFromSongRestart(currentTime);
|
||||
|
||||
mpris?.updateSeek(0);
|
||||
if (isMpvPlayer) {
|
||||
return mpvPlayer.seekTo(0);
|
||||
}
|
||||
|
|
@ -307,20 +356,24 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
if (!isFirstTrack) {
|
||||
const playerData = previous();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
} else {
|
||||
const playerData = setCurrentIndex(queue.length - 1);
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
}
|
||||
},
|
||||
web: () => {
|
||||
if (isFirstTrack) {
|
||||
setCurrentIndex(queue.length - 1);
|
||||
const playerData = setCurrentIndex(queue.length - 1);
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
} else {
|
||||
previous();
|
||||
const playerData = previous();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
}
|
||||
},
|
||||
|
|
@ -329,15 +382,21 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleRepeatNone = {
|
||||
local: () => {
|
||||
const playerData = previous();
|
||||
mpris?.updateSong({
|
||||
currentTime: usePlayerStore.getState().current.time,
|
||||
song: playerData.current.song,
|
||||
});
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
},
|
||||
web: () => {
|
||||
if (isFirstTrack) {
|
||||
resetPlayers();
|
||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
||||
pause();
|
||||
} else {
|
||||
previous();
|
||||
const playerData = previous();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
}
|
||||
},
|
||||
|
|
@ -347,6 +406,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
if (!isFirstTrack) {
|
||||
const playerData = previous();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
} else {
|
||||
|
|
@ -354,7 +414,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
}
|
||||
},
|
||||
web: () => {
|
||||
previous();
|
||||
const playerData = previous();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
resetPlayers();
|
||||
},
|
||||
};
|
||||
|
|
@ -407,14 +468,15 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
? usePlayerStore.getState().current.time
|
||||
: currentPlayerRef.getCurrentTime();
|
||||
|
||||
const evaluatedTime = currentTime - seconds;
|
||||
const newTime = evaluatedTime < 0 ? 0 : evaluatedTime;
|
||||
setCurrentTime(newTime);
|
||||
mpris?.updateSeek(newTime);
|
||||
|
||||
if (isMpvPlayer) {
|
||||
const newTime = currentTime - seconds;
|
||||
mpvPlayer.seek(-seconds);
|
||||
setCurrentTime(newTime < 0 ? 0 : newTime);
|
||||
} else {
|
||||
const newTime = currentTime - seconds;
|
||||
resetNextPlayer();
|
||||
setCurrentTime(newTime);
|
||||
currentPlayerRef.seekTo(newTime);
|
||||
}
|
||||
};
|
||||
|
|
@ -427,12 +489,14 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
if (isMpvPlayer) {
|
||||
const newTime = currentTime + seconds;
|
||||
mpvPlayer.seek(seconds);
|
||||
mpris?.updateSeek(newTime);
|
||||
setCurrentTime(newTime);
|
||||
} else {
|
||||
const checkNewTime = currentTime + seconds;
|
||||
const songDuration = currentPlayerRef.player.player.duration;
|
||||
|
||||
const newTime = checkNewTime >= songDuration ? songDuration - 1 : checkNewTime;
|
||||
mpris?.updateSeek(newTime);
|
||||
|
||||
resetNextPlayer();
|
||||
setCurrentTime(newTime);
|
||||
|
|
@ -453,6 +517,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
setCurrentTime(e);
|
||||
handleScrobbleFromSeek(e);
|
||||
debouncedSeek(e);
|
||||
|
||||
mpris?.updateSeek(e);
|
||||
},
|
||||
[debouncedSeek, handleScrobbleFromSeek, setCurrentTime],
|
||||
);
|
||||
|
|
@ -529,6 +595,93 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
setCurrentTime,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (utils?.isLinux()) {
|
||||
const unsubCurrentTime = usePlayerStore.subscribe(
|
||||
(state) => state.current.time,
|
||||
(time) => {
|
||||
mpris?.updatePosition(time);
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubCurrentTime();
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (utils?.isLinux()) {
|
||||
mpris.requestPosition((_e: any, data: { position: number }) => {
|
||||
const newTime = data.position;
|
||||
handleSeekSlider(newTime);
|
||||
});
|
||||
|
||||
mpris.requestSeek((_e: any, data: { offset: number }) => {
|
||||
const currentTime = usePlayerStore.getState().current.time;
|
||||
const currentSongDuration = usePlayerStore.getState().current.song?.duration || 0;
|
||||
const resultingTime = currentTime + data.offset;
|
||||
|
||||
let newTime = resultingTime;
|
||||
if (resultingTime > currentSongDuration) {
|
||||
newTime = currentSongDuration - 1;
|
||||
}
|
||||
|
||||
if (resultingTime < 0) {
|
||||
newTime = 0;
|
||||
}
|
||||
|
||||
handleSeekSlider(newTime);
|
||||
});
|
||||
|
||||
mpris.requestVolume((_e: any, data: { volume: number }) => {
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
const resultingVolume = data.volume + currentVolume;
|
||||
|
||||
let newVolume = resultingVolume;
|
||||
if (newVolume > 100) {
|
||||
newVolume = 100;
|
||||
} else if (newVolume < 0) {
|
||||
newVolume = 0;
|
||||
}
|
||||
|
||||
usePlayerStore.getState().actions.setVolume(newVolume);
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.volume(newVolume);
|
||||
}
|
||||
});
|
||||
|
||||
mpris.requestToggleRepeat((_e: any, data: { repeat: string }) => {
|
||||
if (data.repeat === 'Playlist') {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ALL);
|
||||
} else if (data.repeat === 'Track') {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ONE);
|
||||
} else {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.NONE);
|
||||
}
|
||||
});
|
||||
|
||||
mpris.requestToggleShuffle((_e: any, data: { shuffle: boolean }) => {
|
||||
usePlayerStore
|
||||
.getState()
|
||||
.actions.setShuffle(data.shuffle ? PlayerShuffle.TRACK : PlayerShuffle.NONE);
|
||||
});
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('mpris-request-position');
|
||||
ipc?.removeAllListeners('mpris-request-seek');
|
||||
ipc?.removeAllListeners('mpris-request-volume');
|
||||
ipc?.removeAllListeners('mpris-request-toggle-repeat');
|
||||
ipc?.removeAllListeners('mpris-request-toggle-shuffle');
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [handleSeekSlider, isMpvPlayer]);
|
||||
|
||||
return {
|
||||
handleNextTrack,
|
||||
handlePlayPause,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import { nanoid } from 'nanoid/non-secure';
|
|||
import { LibraryItem, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
export const useHandlePlayQueueAdd = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const playerType = usePlayerType();
|
||||
|
|
@ -167,6 +168,13 @@ export const useHandlePlayQueueAdd = () => {
|
|||
if (!songs) return toast.warn({ message: 'No songs found' });
|
||||
|
||||
const playerData = usePlayerStore.getState().actions.addToQueue(songs, options.play);
|
||||
mpris?.updateSong({
|
||||
currentTime: usePlayerStore.getState().current.time,
|
||||
repeat: usePlayerStore.getState().repeat,
|
||||
shuffle: usePlayerStore.getState().shuffle,
|
||||
song: playerData.current.song,
|
||||
status: 'Playing',
|
||||
});
|
||||
|
||||
if (options.play === Play.NEXT || options.play === Play.LAST) {
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
|
|
|
|||
2
src/renderer/preload.d.ts
vendored
2
src/renderer/preload.d.ts
vendored
|
|
@ -40,8 +40,10 @@ declare global {
|
|||
windowUnmaximize(): void;
|
||||
};
|
||||
localSettings: any;
|
||||
mpris: any;
|
||||
mpvPlayer: any;
|
||||
mpvPlayerListener: any;
|
||||
utils: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@ export const AppOutlet = () => {
|
|||
const getMpvPath = async () => {
|
||||
if (!isElectron()) return setIsMpvRequired(false);
|
||||
const mpvPath = await localSettings.get('mpv_path');
|
||||
return setIsMpvRequired(!mpvPath);
|
||||
|
||||
if (mpvPath) {
|
||||
return setIsMpvRequired(false);
|
||||
}
|
||||
|
||||
return setIsMpvRequired(true);
|
||||
};
|
||||
|
||||
getMpvPath();
|
||||
|
|
|
|||
1
src/types/mpris-service.d.ts
vendored
Normal file
1
src/types/mpris-service.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
declare module 'mpris-service';
|
||||
Loading…
Add table
Add a link
Reference in a new issue