simplify remote/media session (#632)

This commit is contained in:
Kendall Garner 2024-07-03 08:47:26 +00:00 committed by GitHub
parent d57b4b4b68
commit 110a1a63f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 236 additions and 230 deletions

View File

@ -8,9 +8,10 @@ import { app, ipcMain } from 'electron';
import { Server as WsServer, WebSocketServer, WebSocket } from 'ws'; import { Server as WsServer, WebSocketServer, WebSocket } from 'ws';
import manifest from './manifest.json'; import manifest from './manifest.json';
import { ClientEvent, ServerEvent } from '../../../../remote/types'; import { ClientEvent, ServerEvent } from '../../../../remote/types';
import { PlayerRepeat, SongUpdate } from '../../../../renderer/types'; import { PlayerRepeat, PlayerStatus, SongState } from '../../../../renderer/types';
import { getMainWindow } from '../../../main'; import { getMainWindow } from '../../../main';
import { isLinux } from '../../../utils'; import { isLinux } from '../../../utils';
import type { QueueSong } from '/@/renderer/api/types';
let mprisPlayer: any | undefined; let mprisPlayer: any | undefined;
@ -100,9 +101,7 @@ enum Encoding {
const GZIP_REGEX = /\bgzip\b/; const GZIP_REGEX = /\bgzip\b/;
const ZLIB_REGEX = /bdeflate\b/; const ZLIB_REGEX = /bdeflate\b/;
let currentSong: SongUpdate = { const currentState: SongState = {};
currentTime: 0,
};
const getEncoding = (encoding: string | string[]): Encoding => { const getEncoding = (encoding: string | string[]): Encoding => {
const encodingArray = Array.isArray(encoding) ? encoding : [encoding]; const encodingArray = Array.isArray(encoding) ? encoding : [encoding];
@ -388,7 +387,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
break; break;
} }
case 'proxy': { case 'proxy': {
const toFetch = currentSong.song?.imageUrl?.replaceAll( const toFetch = currentState.song?.imageUrl?.replaceAll(
/&(size|width|height=\d+)/g, /&(size|width|height=\d+)/g,
'', '',
); );
@ -438,9 +437,9 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
volume = 0; volume = 0;
} }
currentSong.volume = volume; currentState.volume = volume;
broadcast({ data: { volume }, event: 'song' }); broadcast({ data: volume, event: 'volume' });
getMainWindow()?.webContents.send('request-volume', { getMainWindow()?.webContents.send('request-volume', {
volume, volume,
}); });
@ -452,22 +451,22 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
} }
case 'favorite': { case 'favorite': {
const { favorite, id } = json; const { favorite, id } = json;
if (id && id === currentSong.song?.id) { if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-favorite', { getMainWindow()?.webContents.send('request-favorite', {
favorite, favorite,
id, id,
serverId: currentSong.song.serverId, serverId: currentState.song.serverId,
}); });
} }
break; break;
} }
case 'rating': { case 'rating': {
const { rating, id } = json; const { rating, id } = json;
if (id && id === currentSong.song?.id) { if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-rating', { getMainWindow()?.webContents.send('request-rating', {
id, id,
rating, rating,
serverId: currentSong.song.serverId, serverId: currentState.song.serverId,
}); });
} }
break; break;
@ -482,7 +481,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
ws.alive = true; ws.alive = true;
}); });
ws.send(JSON.stringify({ data: currentSong, event: 'song' })); ws.send(JSON.stringify({ data: currentState, event: 'state' }));
}); });
const heartBeat = setInterval(() => { const heartBeat = setInterval(() => {
@ -564,13 +563,13 @@ ipcMain.on('remote-username', (_event, username: string) => {
}); });
ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => { ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => {
if (currentSong.song?.serverId !== serverId) return; if (currentState.song?.serverId !== serverId) return;
const id = currentSong.song.id; const id = currentState.song.id;
for (const songId of ids) { for (const songId of ids) {
if (songId === id) { if (songId === id) {
currentSong.song.userFavorite = favorite; currentState.song.userFavorite = favorite;
broadcast({ data: { favorite, id: songId }, event: 'favorite' }); broadcast({ data: { favorite, id: songId }, event: 'favorite' });
return; return;
} }
@ -578,13 +577,13 @@ ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids:
}); });
ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => { ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => {
if (currentSong.song?.serverId !== serverId) return; if (currentState.song?.serverId !== serverId) return;
const id = currentSong.song.id; const id = currentState.song.id;
for (const songId of ids) { for (const songId of ids) {
if (songId === id) { if (songId === id) {
currentSong.song.userRating = rating; currentState.song.userRating = rating;
broadcast({ data: { id: songId, rating }, event: 'rating' }); broadcast({ data: { id: songId, rating }, event: 'rating' });
return; return;
} }
@ -592,42 +591,32 @@ ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: stri
}); });
ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => { ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => {
currentSong.repeat = repeat; currentState.repeat = repeat;
broadcast({ data: { repeat }, event: 'song' }); broadcast({ data: repeat, event: 'repeat' });
}); });
ipcMain.on('update-shuffle', (_event, shuffle: boolean) => { ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
currentSong.shuffle = shuffle; currentState.shuffle = shuffle;
broadcast({ data: { shuffle }, event: 'song' }); broadcast({ data: shuffle, event: 'shuffle' });
}); });
ipcMain.on('update-song', (_event, data: SongUpdate) => { ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
const { song, ...rest } = data; currentState.status = status;
const songChanged = song?.id !== currentSong.song?.id; broadcast({ data: status, event: 'playback' });
});
if (!song?.id) { ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
currentSong = { const songChanged = song?.id !== currentState.song?.id;
...currentSong, currentState.song = song;
...data,
song: undefined,
};
} else {
currentSong = {
...currentSong,
...data,
};
}
if (songChanged) { if (songChanged) {
broadcast({ data: { ...rest, song: song || null }, event: 'song' }); broadcast({ data: song || null, event: 'song' });
} else {
broadcast({ data: rest, event: 'song' });
} }
}); });
ipcMain.on('update-volume', (_event, volume: number) => { ipcMain.on('update-volume', (_event, volume: number) => {
currentSong.volume = volume; currentState.volume = volume;
broadcast({ data: { volume }, event: 'song' }); broadcast({ data: volume, event: 'volume' });
}); });
if (mprisPlayer) { if (mprisPlayer) {
@ -639,13 +628,13 @@ if (mprisPlayer) {
? PlayerRepeat.ONE ? PlayerRepeat.ONE
: PlayerRepeat.NONE; : PlayerRepeat.NONE;
currentSong.repeat = repeat; currentState.repeat = repeat;
broadcast({ data: { repeat }, event: 'song' }); broadcast({ data: repeat, event: 'repeat' });
}); });
mprisPlayer.on('shuffle', (shuffle: boolean) => { mprisPlayer.on('shuffle', (shuffle: boolean) => {
currentSong.shuffle = shuffle; currentState.shuffle = shuffle;
broadcast({ data: { shuffle }, event: 'song' }); broadcast({ data: shuffle, event: 'shuffle' });
}); });
mprisPlayer.on('volume', (vol: number) => { mprisPlayer.on('volume', (vol: number) => {
@ -656,7 +645,7 @@ if (mprisPlayer) {
} else if (volume < 0) { } else if (volume < 0) {
volume = 0; volume = 0;
} }
currentSong.volume = volume; currentState.volume = volume;
broadcast({ data: { volume }, event: 'song' }); broadcast({ data: volume, event: 'volume' });
}); });
} }

View File

@ -1,7 +1,8 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import Player from 'mpris-service'; import Player from 'mpris-service';
import { PlayerRepeat, PlayerStatus, SongUpdate } from '../../../renderer/types'; import { PlayerRepeat, PlayerStatus } from '../../../renderer/types';
import { getMainWindow } from '../../main'; import { getMainWindow } from '../../main';
import { QueueSong } from '/@/renderer/api/types';
const mprisPlayer = Player({ const mprisPlayer = Player({
identity: 'Feishin', identity: 'Feishin',
@ -117,6 +118,10 @@ ipcMain.on('update-volume', (_event, volume) => {
mprisPlayer.volume = Number(volume) / 100; mprisPlayer.volume = Number(volume) / 100;
}); });
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
mprisPlayer.playbackStatus = status === PlayerStatus.PLAYING ? 'Playing' : 'Paused';
});
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = { const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
[PlayerRepeat.ALL]: 'Playlist', [PlayerRepeat.ALL]: 'Playlist',
[PlayerRepeat.ONE]: 'Track', [PlayerRepeat.ONE]: 'Track',
@ -131,21 +136,9 @@ ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
mprisPlayer.shuffle = shuffle; mprisPlayer.shuffle = shuffle;
}); });
ipcMain.on('update-song', (_event, args: SongUpdate) => { ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
const { song, status, repeat, shuffle } = args || {};
try { try {
mprisPlayer.playbackStatus = status === PlayerStatus.PLAYING ? 'Playing' : 'Paused'; if (!song?.id) {
if (repeat) {
mprisPlayer.loopStatus = REPEAT_TO_MPRIS[repeat];
}
if (shuffle) {
mprisPlayer.shuffle = shuffle;
}
if (!song) {
mprisPlayer.metadata = {}; mprisPlayer.metadata = {};
return; return;
} }

View File

@ -1,5 +1,6 @@
import { IpcRendererEvent, ipcRenderer } from 'electron'; import { IpcRendererEvent, ipcRenderer } from 'electron';
import { SongUpdate } from '/@/renderer/types'; import { QueueSong } from '/@/renderer/api/types';
import { PlayerStatus } from '/@/renderer/types';
const requestFavorite = ( const requestFavorite = (
cb: ( cb: (
@ -46,6 +47,10 @@ const updatePassword = (password: string) => {
ipcRenderer.send('remote-password', password); ipcRenderer.send('remote-password', password);
}; };
const updatePlayback = (playback: PlayerStatus) => {
ipcRenderer.send('update-playback', playback);
};
const updateSetting = ( const updateSetting = (
enabled: boolean, enabled: boolean,
port: number, port: number,
@ -67,7 +72,7 @@ const updateShuffle = (shuffle: boolean) => {
ipcRenderer.send('update-shuffle', shuffle); ipcRenderer.send('update-shuffle', shuffle);
}; };
const updateSong = (args: SongUpdate) => { const updateSong = (args: QueueSong | undefined) => {
ipcRenderer.send('update-song', args); ipcRenderer.send('update-song', args);
}; };
@ -89,6 +94,7 @@ export const remote = {
setRemotePort, setRemotePort,
updateFavorite, updateFavorite,
updatePassword, updatePassword,
updatePlayback,
updateRating, updateRating,
updateRepeat, updateRepeat,
updateSetting, updateSetting,

View File

@ -18,7 +18,7 @@ import {
import { PlayerRepeat, PlayerStatus } from '/@/renderer/types'; import { PlayerRepeat, PlayerStatus } from '/@/renderer/types';
import { WrapperSlider } from '/@/remote/components/wrapped-slider'; import { WrapperSlider } from '/@/remote/components/wrapped-slider';
import { Tooltip } from '/@/renderer/components/tooltip'; import { Tooltip } from '/@/renderer/components/tooltip';
import { Rating } from '/@/renderer/components'; import { Rating } from '/@/renderer/components/rating';
export const RemoteContainer = () => { export const RemoteContainer = () => {
const { repeat, shuffle, song, status, volume } = useInfo(); const { repeat, shuffle, song, status, volume } = useInfo();
@ -38,7 +38,7 @@ export const RemoteContainer = () => {
return ( return (
<> <>
{song && ( {id && (
<> <>
<Title order={1}>{song.name}</Title> <Title order={1}>{song.name}</Title>
<Group align="flex-end"> <Group align="flex-end">
@ -61,7 +61,7 @@ export const RemoteContainer = () => {
spacing={0} spacing={0}
> >
<RemoteButton <RemoteButton
disabled={!song} disabled={!id}
tooltip="Previous track" tooltip="Previous track"
variant="default" variant="default"
onClick={() => send({ event: 'previous' })} onClick={() => send({ event: 'previous' })}
@ -69,8 +69,8 @@ export const RemoteContainer = () => {
<RiSkipBackFill size={25} /> <RiSkipBackFill size={25} />
</RemoteButton> </RemoteButton>
<RemoteButton <RemoteButton
disabled={!song} disabled={!id}
tooltip={song && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'} tooltip={id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
variant="default" variant="default"
onClick={() => { onClick={() => {
if (status === PlayerStatus.PLAYING) { if (status === PlayerStatus.PLAYING) {
@ -80,14 +80,14 @@ export const RemoteContainer = () => {
} }
}} }}
> >
{song && status === PlayerStatus.PLAYING ? ( {id && status === PlayerStatus.PLAYING ? (
<RiPauseFill size={25} /> <RiPauseFill size={25} />
) : ( ) : (
<RiPlayFill size={25} /> <RiPlayFill size={25} />
)} )}
</RemoteButton> </RemoteButton>
<RemoteButton <RemoteButton
disabled={!song} disabled={!id}
tooltip="Next track" tooltip="Next track"
variant="default" variant="default"
onClick={() => send({ event: 'next' })} onClick={() => send({ event: 'next' })}
@ -127,7 +127,7 @@ export const RemoteContainer = () => {
</RemoteButton> </RemoteButton>
<RemoteButton <RemoteButton
$active={song?.userFavorite} $active={song?.userFavorite}
disabled={!song} disabled={!id}
tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'} tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'}
variant="default" variant="default"
onClick={() => { onClick={() => {

View File

@ -4,7 +4,7 @@ import merge from 'lodash/merge';
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/remote/types'; import type { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/remote/types';
interface StatefulWebSocket extends WebSocket { interface StatefulWebSocket extends WebSocket {
natural: boolean; natural: boolean;
@ -133,6 +133,12 @@ export const useRemoteStore = create<SettingsSlice>()(
}); });
break; break;
} }
case 'playback': {
set((state) => {
state.info.status = data;
});
break;
}
case 'proxy': { case 'proxy': {
set((state) => { set((state) => {
if (state.info.song) { if (state.info.song) {
@ -149,9 +155,34 @@ export const useRemoteStore = create<SettingsSlice>()(
}); });
break; break;
} }
case 'repeat': {
set((state) => {
state.info.repeat = data;
});
break;
}
case 'shuffle': {
set((state) => {
state.info.shuffle = data;
});
break;
}
case 'song': { case 'song': {
set((nested) => { set((state) => {
nested.info = { ...nested.info, ...data }; console.log(data);
state.info.song = data;
});
break;
}
case 'state': {
set((state) => {
state.info = data;
});
break;
}
case 'volume': {
set((state) => {
state.info.volume = data;
}); });
} }
} }
@ -212,11 +243,9 @@ export const useRemoteStore = create<SettingsSlice>()(
{ name: 'store_settings' }, { name: 'store_settings' },
), ),
{ {
merge: (persistedState, currentState) => { merge: (persistedState, currentState) => merge(currentState, persistedState),
return merge(currentState, persistedState);
},
name: 'store_settings', name: 'store_settings',
version: 6, version: 7,
}, },
), ),
); );

View File

@ -1,7 +1,7 @@
import type { QueueSong } from '/@/renderer/api/types'; import type { QueueSong } from '/@/renderer/api/types';
import type { SongUpdate } from '/@/renderer/types'; import type { PlayerRepeat, PlayerStatus, SongState } from '/@/renderer/types';
export interface SongUpdateSocket extends Omit<SongUpdate, 'song'> { export interface SongUpdateSocket extends Omit<SongState, 'song'> {
song?: QueueSong | null; song?: QueueSong | null;
} }
@ -15,6 +15,11 @@ export interface ServerFavorite {
event: 'favorite'; event: 'favorite';
} }
export interface ServerPlayStatus {
data: PlayerStatus;
event: 'playback';
}
export interface ServerProxy { export interface ServerProxy {
data: string; data: string;
event: 'proxy'; event: 'proxy';
@ -25,12 +30,42 @@ export interface ServerRating {
event: 'rating'; event: 'rating';
} }
export interface ServerRepeat {
data: PlayerRepeat;
event: 'repeat';
}
export interface ServerShuffle {
data: boolean;
event: 'shuffle';
}
export interface ServerSong { export interface ServerSong {
data: SongUpdateSocket; data: QueueSong | null;
event: 'song'; event: 'song';
} }
export type ServerEvent = ServerError | ServerFavorite | ServerRating | ServerSong | ServerProxy; export interface ServerState {
data: SongState;
event: 'state';
}
export interface ServerVolume {
data: number;
event: 'volume';
}
export type ServerEvent =
| ServerError
| ServerFavorite
| ServerPlayStatus
| ServerRating
| ServerRepeat
| ServerShuffle
| ServerSong
| ServerState
| ServerProxy
| ServerVolume;
export interface ClientSimpleEvent { export interface ClientSimpleEvent {
event: 'next' | 'pause' | 'play' | 'previous' | 'proxy' | 'repeat' | 'shuffle'; event: 'next' | 'pause' | 'play' | 'previous' | 'proxy' | 'repeat' | 'shuffle';

View File

@ -40,13 +40,13 @@ type WebAudio = {
gain: GainNode; gain: GainNode;
}; };
// Credits: http://stackoverflow.com/questions/12150729/ddg // Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
// This is used so that the player will always have an <audio> element. This means that // This is used so that the player will always have an <audio> element. This means that
// player1Source and player2Source are connected BEFORE the user presses play for // player1Source and player2Source are connected BEFORE the user presses play for
// the first time. This workaround is important for Safari, which seems to require the // the first time. This workaround is important for Safari, which seems to require the
// source to be connected PRIOR to resuming audio context // source to be connected PRIOR to resuming audio context
const EMPTY_SOURCE = const EMPTY_SOURCE =
'data:audio/wav;base64,UklGRjIAAABXQVZFZm10IBIAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA=='; 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
export const AudioPlayer = forwardRef( export const AudioPlayer = forwardRef(
( (

View File

@ -60,6 +60,7 @@ import {
import { usePlaybackType } from '/@/renderer/store/settings.store'; import { usePlaybackType } from '/@/renderer/store/settings.store';
import { Play, PlaybackType } from '/@/renderer/types'; import { Play, PlaybackType } from '/@/renderer/types';
import { ItemDetailsModal } from '/@/renderer/features/item-details/components/item-details-modal'; import { ItemDetailsModal } from '/@/renderer/features/item-details/components/item-details-modal';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { controller } from '/@/renderer/api/controller'; import { controller } from '/@/renderer/api/controller';
type ContextMenuContextProps = { type ContextMenuContextProps = {
@ -89,7 +90,6 @@ const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating', 'shareI
// const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = []; // const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const remote = isElectron() ? window.electron.remote : null;
export interface ContextMenuProviderProps { export interface ContextMenuProviderProps {
children: ReactNode; children: ReactNode;
@ -643,7 +643,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
ctx.tableApi?.redrawRows(); ctx.tableApi?.redrawRows();
if (isCurrentSongRemoved) { if (isCurrentSongRemoved) {
remote?.updateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
} }
}, [ctx.dataNodes, ctx.tableApi, playbackType, removeFromQueue]); }, [ctx.dataNodes, ctx.tableApi, playbackType, removeFromQueue]);

View File

@ -14,13 +14,13 @@ import {
} from 'react-icons/ri'; } from 'react-icons/ri';
import { Song } from '/@/renderer/api/types'; import { Song } from '/@/renderer/api/types';
import { usePlayerControls, useQueueControls } from '/@/renderer/store'; import { usePlayerControls, useQueueControls } from '/@/renderer/store';
import { PlaybackType, PlayerStatus, TableType } from '/@/renderer/types'; import { PlaybackType, TableType } from '/@/renderer/types';
import { usePlaybackType } from '/@/renderer/store/settings.store'; import { usePlaybackType } from '/@/renderer/store/settings.store';
import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store'; import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store';
import { TableConfigDropdown } from '/@/renderer/components/virtual-table'; import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const remote = isElectron() ? window.electron.remote : null;
interface PlayQueueListOptionsProps { interface PlayQueueListOptionsProps {
tableRef: MutableRefObject<{ grid: AgGridReactType<Song> } | null>; tableRef: MutableRefObject<{ grid: AgGridReactType<Song> } | null>;
@ -79,7 +79,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
} }
if (isCurrentSongRemoved) { if (isCurrentSongRemoved) {
remote?.updateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
} }
}; };
@ -91,7 +91,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
mpvPlayer!.pause(); mpvPlayer!.pause();
} }
remote?.updateSong({ song: undefined, status: PlayerStatus.PAUSED }); updateSong(undefined);
setCurrentTime(0); setCurrentTime(0);
pause(); pause();

View File

@ -30,16 +30,16 @@ import debounce from 'lodash/debounce';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table'; import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
import { ErrorFallback } from '/@/renderer/features/action-required'; import { ErrorFallback } from '/@/renderer/features/action-required';
import { PlaybackType, PlayerStatus, TableType } from '/@/renderer/types'; import { PlaybackType, TableType } from '/@/renderer/types';
import { LibraryItem, QueueSong } from '/@/renderer/api/types'; import { LibraryItem, QueueSong } from '/@/renderer/api/types';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { useAppFocus } from '/@/renderer/hooks'; import { useAppFocus } from '/@/renderer/hooks';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref'; import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const remote = isElectron() ? window.electron.remote : null;
type QueueProps = { type QueueProps = {
type: TableType; type: TableType;
@ -82,11 +82,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
const handleDoubleClick = (e: CellDoubleClickedEvent) => { const handleDoubleClick = (e: CellDoubleClickedEvent) => {
const playerData = setCurrentTrack(e.data.uniqueId); const playerData = setCurrentTrack(e.data.uniqueId);
remote?.updateSong({ updateSong(playerData.current.song);
currentTime: 0,
song: playerData.current.song,
status: PlayerStatus.PLAYING,
});
if (playbackType === PlaybackType.LOCAL) { if (playbackType === PlaybackType.LOCAL) {
mpvPlayer!.volume(volume); mpvPlayer!.volume(volume);

View File

@ -1,5 +1,4 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import isElectron from 'is-electron';
import styled from 'styled-components'; import styled from 'styled-components';
import { usePlaybackType, useSettingsStore } from '/@/renderer/store/settings.store'; import { usePlaybackType, useSettingsStore } from '/@/renderer/store/settings.store';
import { PlaybackType } from '/@/renderer/types'; import { PlaybackType } from '/@/renderer/types';
@ -17,6 +16,7 @@ import { CenterControls } from './center-controls';
import { LeftControls } from './left-controls'; import { LeftControls } from './left-controls';
import { RightControls } from './right-controls'; import { RightControls } from './right-controls';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref'; import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
const PlayerbarContainer = styled.div` const PlayerbarContainer = styled.div`
width: 100vw; width: 100vw;
@ -59,8 +59,6 @@ const CenterGridItem = styled.div`
overflow: hidden; overflow: hidden;
`; `;
const remote = isElectron() ? window.electron.remote : null;
export const Playerbar = () => { export const Playerbar = () => {
const playersRef = PlayersRef; const playersRef = PlayersRef;
const settings = useSettingsStore((state) => state.playback); const settings = useSettingsStore((state) => state.playback);
@ -75,13 +73,7 @@ export const Playerbar = () => {
const autoNextFn = useCallback(() => { const autoNextFn = useCallback(() => {
const playerData = autoNext(); const playerData = autoNext();
updateSong(playerData.current.song);
if (remote) {
remote.updateSong({
currentTime: 0,
song: playerData.current.song,
});
}
}, [autoNext]); }, [autoNext]);
return ( return (

View File

@ -14,9 +14,9 @@ import {
import { usePlaybackType } from '/@/renderer/store/settings.store'; import { usePlaybackType } from '/@/renderer/store/settings.store';
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble'; import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { QueueSong } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components'; import { toast } from '/@/renderer/components';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null; const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
@ -24,7 +24,7 @@ const ipc = isElectron() ? window.electron.ipc : null;
const utils = isElectron() ? window.electron.utils : null; const utils = isElectron() ? window.electron.utils : null;
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null; const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
const remote = isElectron() ? window.electron.remote : null; const remote = isElectron() ? window.electron.remote : null;
const mediaSession = !isElectron() || !utils?.isLinux() ? navigator.mediaSession : null; const mediaSession = navigator.mediaSession;
export const useCenterControls = (args: { playersRef: any }) => { export const useCenterControls = (args: { playersRef: any }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -46,6 +46,23 @@ export const useCenterControls = (args: { playersRef: any }) => {
const { handleScrobbleFromSongRestart, handleScrobbleFromSeek } = useScrobble(); const { handleScrobbleFromSongRestart, handleScrobbleFromSeek } = useScrobble();
useEffect(() => {
if (mediaSession) {
mediaSession.playbackState =
playerStatus === PlayerStatus.PLAYING ? 'playing' : 'paused';
}
remote?.updatePlayback(playerStatus);
}, [playerStatus]);
useEffect(() => {
remote?.updateRepeat(repeatStatus);
}, [repeatStatus]);
useEffect(() => {
remote?.updateShuffle(shuffleStatus !== PlayerShuffle.NONE);
}, [shuffleStatus]);
const resetPlayers = useCallback(() => { const resetPlayers = useCallback(() => {
if (player1Ref.getInternalPlayer()) { if (player1Ref.getInternalPlayer()) {
player1Ref.getInternalPlayer().currentTime = 0; player1Ref.getInternalPlayer().currentTime = 0;
@ -76,61 +93,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
const isMpvPlayer = isElectron() && playbackType === PlaybackType.LOCAL; const isMpvPlayer = isElectron() && playbackType === PlaybackType.LOCAL;
const mprisUpdateSong = (args?: {
currentTime?: number;
song?: QueueSong;
status?: PlayerStatus;
}) => {
const { song, currentTime, status } = args || {};
const time = currentTime || usePlayerStore.getState().current.time;
const playStatus = status || usePlayerStore.getState().current.status;
const track = song || usePlayerStore.getState().current.song;
remote?.updateSong({
currentTime: time,
repeat: usePlayerStore.getState().repeat,
shuffle: usePlayerStore.getState().shuffle !== PlayerShuffle.NONE,
song: track,
status: playStatus,
});
if (mediaSession) {
mediaSession.playbackState = playStatus === PlayerStatus.PLAYING ? 'playing' : 'paused';
let metadata: MediaMetadata;
if (track) {
let artwork: MediaImage[];
if (track.imageUrl) {
const image300 = track.imageUrl
?.replace(/&size=\d+/, '&size=300')
.replace(/\?width=\d+/, '?width=300')
.replace(/&height=\d+/, '&height=300');
artwork = [{ sizes: '300x300', src: image300, type: 'image/png' }];
} else {
artwork = [];
}
metadata = new MediaMetadata({
album: track.album ?? '',
artist: track.artistName,
artwork,
title: track.name,
});
} else {
metadata = new MediaMetadata();
}
mediaSession.metadata = metadata;
}
};
const handlePlay = useCallback(() => { const handlePlay = useCallback(() => {
mprisUpdateSong({ status: PlayerStatus.PLAYING });
if (isMpvPlayer) { if (isMpvPlayer) {
mpvPlayer?.volume(usePlayerStore.getState().volume); mpvPlayer?.volume(usePlayerStore.getState().volume);
mpvPlayer!.play(); mpvPlayer!.play();
@ -145,8 +108,6 @@ export const useCenterControls = (args: { playersRef: any }) => {
}, [currentPlayerRef, isMpvPlayer, play]); }, [currentPlayerRef, isMpvPlayer, play]);
const handlePause = useCallback(() => { const handlePause = useCallback(() => {
mprisUpdateSong({ status: PlayerStatus.PAUSED });
if (isMpvPlayer) { if (isMpvPlayer) {
mpvPlayer!.pause(); mpvPlayer!.pause();
} }
@ -155,8 +116,6 @@ export const useCenterControls = (args: { playersRef: any }) => {
}, [isMpvPlayer, pause]); }, [isMpvPlayer, pause]);
const handleStop = useCallback(() => { const handleStop = useCallback(() => {
mprisUpdateSong({ status: PlayerStatus.PAUSED });
if (isMpvPlayer) { if (isMpvPlayer) {
mpvPlayer!.pause(); mpvPlayer!.pause();
mpvPlayer!.seekTo(0); mpvPlayer!.seekTo(0);
@ -212,13 +171,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
const handleRepeatAll = { const handleRepeatAll = {
local: () => { local: () => {
const playerData = autoNext(); const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING }); updateSong(playerData.current.song);
mpvPlayer!.autoNext(playerData); mpvPlayer!.autoNext(playerData);
play(); play();
}, },
web: () => { web: () => {
const playerData = autoNext(); const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING }); updateSong(playerData.current.song);
}, },
}; };
@ -226,15 +185,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
local: () => { local: () => {
if (isLastTrack) { if (isLastTrack) {
const playerData = setCurrentIndex(0); const playerData = setCurrentIndex(0);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData, true); mpvPlayer!.setQueue(playerData, true);
pause(); pause();
} else { } else {
const playerData = autoNext(); const playerData = autoNext();
mprisUpdateSong({ updateSong(playerData.current.song);
song: playerData.current.song,
status: PlayerStatus.PLAYING,
});
mpvPlayer!.autoNext(playerData); mpvPlayer!.autoNext(playerData);
play(); play();
} }
@ -242,14 +198,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
web: () => { web: () => {
if (isLastTrack) { if (isLastTrack) {
resetPlayers(); resetPlayers();
mprisUpdateSong({ status: PlayerStatus.PAUSED });
pause(); pause();
} else { } else {
const playerData = autoNext(); const playerData = autoNext();
mprisUpdateSong({ updateSong(playerData.current.song);
song: playerData.current.song,
status: PlayerStatus.PLAYING,
});
resetPlayers(); resetPlayers();
} }
}, },
@ -258,20 +210,15 @@ export const useCenterControls = (args: { playersRef: any }) => {
const handleRepeatOne = { const handleRepeatOne = {
local: () => { local: () => {
const playerData = autoNext(); const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING }); updateSong(playerData.current.song);
mpvPlayer!.autoNext(playerData); mpvPlayer!.autoNext(playerData);
play(); play();
}, },
web: () => { web: () => {
if (isLastTrack) { if (isLastTrack) {
mprisUpdateSong({ status: PlayerStatus.PAUSED });
resetPlayers(); resetPlayers();
} else { } else {
const playerData = autoNext(); autoNext();
mprisUpdateSong({
song: playerData.current.song,
status: PlayerStatus.PLAYING,
});
resetPlayers(); resetPlayers();
} }
}, },
@ -309,12 +256,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
const handleRepeatAll = { const handleRepeatAll = {
local: () => { local: () => {
const playerData = next(); const playerData = next();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData); mpvPlayer!.setQueue(playerData);
}, },
web: () => { web: () => {
const playerData = next(); const playerData = next();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
}, },
}; };
@ -322,27 +269,24 @@ export const useCenterControls = (args: { playersRef: any }) => {
local: () => { local: () => {
if (isLastTrack) { if (isLastTrack) {
const playerData = setCurrentIndex(0); const playerData = setCurrentIndex(0);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData, true); mpvPlayer!.setQueue(playerData, true);
pause(); pause();
} else { } else {
const playerData = next(); const playerData = next();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData); mpvPlayer!.setQueue(playerData);
} }
}, },
web: () => { web: () => {
if (isLastTrack) { if (isLastTrack) {
const playerData = setCurrentIndex(0); const playerData = setCurrentIndex(0);
mprisUpdateSong({ updateSong(playerData.current.song);
song: playerData.current.song,
status: PlayerStatus.PAUSED,
});
resetPlayers(); resetPlayers();
pause(); pause();
} else { } else {
const playerData = next(); const playerData = next();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
resetPlayers(); resetPlayers();
} }
}, },
@ -352,14 +296,14 @@ export const useCenterControls = (args: { playersRef: any }) => {
local: () => { local: () => {
if (!isLastTrack) { if (!isLastTrack) {
const playerData = next(); const playerData = next();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData); mpvPlayer!.setQueue(playerData);
} }
}, },
web: () => { web: () => {
if (!isLastTrack) { if (!isLastTrack) {
const playerData = next(); const playerData = next();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
} }
}, },
}; };
@ -413,22 +357,22 @@ export const useCenterControls = (args: { playersRef: any }) => {
local: () => { local: () => {
if (!isFirstTrack) { if (!isFirstTrack) {
const playerData = previous(); const playerData = previous();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData); mpvPlayer!.setQueue(playerData);
} else { } else {
const playerData = setCurrentIndex(queue.length - 1); const playerData = setCurrentIndex(queue.length - 1);
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData); mpvPlayer!.setQueue(playerData);
} }
}, },
web: () => { web: () => {
if (isFirstTrack) { if (isFirstTrack) {
const playerData = setCurrentIndex(queue.length - 1); const playerData = setCurrentIndex(queue.length - 1);
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
resetPlayers(); resetPlayers();
} else { } else {
const playerData = previous(); const playerData = previous();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
resetPlayers(); resetPlayers();
} }
}, },
@ -438,26 +382,22 @@ export const useCenterControls = (args: { playersRef: any }) => {
local: () => { local: () => {
if (isFirstTrack) { if (isFirstTrack) {
const playerData = setCurrentIndex(0); const playerData = setCurrentIndex(0);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData, true); mpvPlayer!.setQueue(playerData, true);
pause(); pause();
} else { } else {
const playerData = previous(); const playerData = previous();
mprisUpdateSong({ updateSong(playerData.current.song);
currentTime: usePlayerStore.getState().current.time,
song: playerData.current.song,
});
mpvPlayer!.setQueue(playerData); mpvPlayer!.setQueue(playerData);
} }
}, },
web: () => { web: () => {
if (isFirstTrack) { if (isFirstTrack) {
resetPlayers(); resetPlayers();
mprisUpdateSong({ status: PlayerStatus.PAUSED });
pause(); pause();
} else { } else {
const playerData = previous(); const playerData = previous();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
resetPlayers(); resetPlayers();
} }
}, },
@ -466,12 +406,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
const handleRepeatOne = { const handleRepeatOne = {
local: () => { local: () => {
const playerData = previous(); const playerData = previous();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
mpvPlayer!.setQueue(playerData); mpvPlayer!.setQueue(playerData);
}, },
web: () => { web: () => {
const playerData = previous(); const playerData = previous();
mprisUpdateSong({ song: playerData.current.song }); updateSong(playerData.current.song);
resetPlayers(); resetPlayers();
}, },
}; };

View File

@ -2,13 +2,7 @@ import { useCallback, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store'; import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
import { usePlaybackType } from '/@/renderer/store/settings.store'; import { usePlaybackType } from '/@/renderer/store/settings.store';
import { import { PlayQueueAddOptions, Play, PlaybackType } from '/@/renderer/types';
PlayQueueAddOptions,
Play,
PlaybackType,
PlayerStatus,
PlayerShuffle,
} from '/@/renderer/types';
import { toast } from '/@/renderer/components/toast/index'; import { toast } from '/@/renderer/components/toast/index';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { nanoid } from 'nanoid/non-secure'; import { nanoid } from 'nanoid/non-secure';
@ -30,6 +24,7 @@ import {
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref'; import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => { const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
let queryKey; let queryKey;
@ -59,7 +54,6 @@ const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
}; };
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const remote = isElectron() ? window.electron.remote : null;
const addToQueue = usePlayerStore.getState().actions.addToQueue; const addToQueue = usePlayerStore.getState().actions.addToQueue;
@ -171,6 +165,8 @@ export const useHandlePlayQueueAdd = () => {
const hadSong = usePlayerStore.getState().queue.default.length > 0; const hadSong = usePlayerStore.getState().queue.default.length > 0;
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs }); const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
updateSong(playerData.current.song);
if (playbackType === PlaybackType.LOCAL) { if (playbackType === PlaybackType.LOCAL) {
mpvPlayer!.volume(usePlayerStore.getState().volume); mpvPlayer!.volume(usePlayerStore.getState().volume);
@ -197,14 +193,6 @@ export const useHandlePlayQueueAdd = () => {
play(); play();
} }
remote?.updateSong({
currentTime: usePlayerStore.getState().current.time,
repeat: usePlayerStore.getState().repeat,
shuffle: usePlayerStore.getState().shuffle !== PlayerShuffle.NONE,
song: playerData.current.song,
status: PlayerStatus.PLAYING,
});
return null; return null;
}, },
[play, playbackType, queryClient, server, t], [play, playbackType, queryClient, server, t],

View File

@ -0,0 +1,39 @@
import isElectron from 'is-electron';
import { QueueSong } from '/@/renderer/api/types';
const remote = isElectron() ? window.electron.remote : null;
const mediaSession = navigator.mediaSession;
export const updateSong = (song: QueueSong | undefined) => {
if (mediaSession) {
let metadata: MediaMetadata;
if (song?.id) {
let artwork: MediaImage[];
if (song.imageUrl) {
const image300 = song.imageUrl
?.replace(/&size=\d+/, '&size=300')
.replace(/\?width=\d+/, '?width=300')
.replace(/&height=\d+/, '&height=300');
artwork = [{ sizes: '300x300', src: image300, type: 'image/png' }];
} else {
artwork = [];
}
metadata = new MediaMetadata({
album: song.album ?? '',
artist: song.artistName,
artwork,
title: song.name,
});
} else {
metadata = new MediaMetadata();
}
mediaSession.metadata = metadata;
}
remote?.updateSong(song);
};

View File

@ -211,8 +211,7 @@ export type GridCardData = {
route: CardRoute; route: CardRoute;
}; };
export type SongUpdate = { export type SongState = {
currentTime?: number;
repeat?: PlayerRepeat; repeat?: PlayerRepeat;
shuffle?: boolean; shuffle?: boolean;
song?: QueueSong; song?: QueueSong;