diff --git a/src/plugins/api-server/backend/routes/control.ts b/src/plugins/api-server/backend/routes/control.ts index 235d0d0dc1..fccf12dcab 100644 --- a/src/plugins/api-server/backend/routes/control.ts +++ b/src/plugins/api-server/backend/routes/control.ts @@ -173,7 +173,24 @@ const routes = { }, }, }), - + getShuffleState: createRoute({ + method: 'get', + path: `/api/${API_VERSION}/shuffle`, + summary: 'get shuffle state', + description: 'Get the current shuffle state', + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { + schema: z.object({ + state: z.boolean().nullable(), + }), + }, + }, + }, + }, + }), shuffle: createRoute({ method: 'post', path: `/api/${API_VERSION}/shuffle`, @@ -581,6 +598,25 @@ export const register = ( ctx.status(204); return ctx.body(null); }); + + app.openapi(routes.getShuffleState, async (ctx) => { + const stateResponsePromise = new Promise((resolve) => { + ipcMain.once( + 'ytmd:get-shuffle-response', + (_, isShuffled: boolean | undefined) => { + return resolve(!!isShuffled); + }, + ); + + controller.requestShuffleInformation(); + }); + + const isShuffled = await stateResponsePromise; + + ctx.status(200); + return ctx.json({ state: isShuffled }); + }); + app.openapi(routes.shuffle, (ctx) => { controller.shuffle(); diff --git a/src/plugins/shortcuts/mpris-service.d.ts b/src/plugins/shortcuts/mpris-service.d.ts index ee0457d5be..d598026a41 100644 --- a/src/plugins/shortcuts/mpris-service.d.ts +++ b/src/plugins/shortcuts/mpris-service.d.ts @@ -86,7 +86,7 @@ declare module '@jellybrick/mpris-service' { supportedMimeTypes: string[]; canQuit: boolean; canRaise: boolean; - canSetFullscreen?: boolean; + canUsePlayerControls?: boolean; desktopEntry?: string; hasTrackList: boolean; diff --git a/src/plugins/shortcuts/mpris.ts b/src/plugins/shortcuts/mpris.ts index 928e2c326c..93cb40f94a 100644 --- a/src/plugins/shortcuts/mpris.ts +++ b/src/plugins/shortcuts/mpris.ts @@ -77,7 +77,7 @@ function setupMPRIS() { instance.canRaise = true; instance.canQuit = false; - instance.canSetFullscreen = true; + instance.canUsePlayerControls = true; instance.supportedUriSchemes = ['http', 'https']; instance.desktopEntry = 'youtube-music'; return instance; @@ -93,6 +93,7 @@ function registerMPRIS(win: BrowserWindow) { shuffle, switchRepeat, setFullscreen, + requestShuffleInformation, requestFullscreenInformation, requestQueueInformation, } = songControls; @@ -126,8 +127,10 @@ function registerMPRIS(win: BrowserWindow) { win.webContents.send('ytmd:setup-time-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-repeat-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-volume-changed-listener', 'mpris'); + win.webContents.send('ytmd:setup-shuffle-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-fullscreen-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-autoplay-changed-listener', 'mpris'); + requestShuffleInformation(); requestFullscreenInformation(); requestQueueInformation(); }); @@ -156,8 +159,16 @@ function registerMPRIS(win: BrowserWindow) { requestQueueInformation(); }); + ipcMain.on('ytmd:shuffle-changed', (_, shuffleEnabled: boolean) => { + if (player.shuffle === undefined || !player.canUsePlayerControls) { + return; + } + + player.shuffle = shuffleEnabled ?? !player.shuffle; + }); + ipcMain.on('ytmd:fullscreen-changed', (_, changedTo: boolean) => { - if (player.fullscreen === undefined || !player.canSetFullscreen) { + if (player.fullscreen === undefined || !player.canUsePlayerControls) { return; } @@ -168,7 +179,7 @@ function registerMPRIS(win: BrowserWindow) { ipcMain.on( 'ytmd:set-fullscreen', (_, isFullscreen: boolean | undefined) => { - if (!player.canSetFullscreen || isFullscreen === undefined) { + if (!player.canUsePlayerControls || isFullscreen === undefined) { return; } @@ -179,7 +190,7 @@ function registerMPRIS(win: BrowserWindow) { ipcMain.on( 'ytmd:fullscreen-changed-supported', (_, isFullscreenSupported: boolean) => { - player.canSetFullscreen = isFullscreenSupported; + player.canUsePlayerControls = isFullscreenSupported; }, ); ipcMain.on('ytmd:autoplay-changed', (_) => { @@ -272,6 +283,12 @@ function registerMPRIS(win: BrowserWindow) { player.on('position', seekTo); player.on('shuffle', (enableShuffle) => { + if (!player.canUsePlayerControls || enableShuffle === undefined) { + return; + } + + player.shuffle = enableShuffle; + if (enableShuffle) { shuffle(); requestQueueInformation(); diff --git a/src/providers/song-controls.ts b/src/providers/song-controls.ts index c693132a06..e03aa44a40 100644 --- a/src/providers/song-controls.ts +++ b/src/providers/song-controls.ts @@ -62,6 +62,9 @@ export default (win: BrowserWindow) => { win.webContents.send('ytmd:seek-by', seconds); } }, + requestShuffleInformation: () => { + win.webContents.send('ytmd:get-shuffle'); + }, shuffle: () => win.webContents.send('ytmd:shuffle'), switchRepeat: (n: ArgsType = 1) => { const repeat = parseNumberFromArgsType(n); diff --git a/src/providers/song-info-front.ts b/src/providers/song-info-front.ts index 8f1715cdc3..1bd89fe695 100644 --- a/src/providers/song-info-front.ts +++ b/src/providers/song-info-front.ts @@ -87,6 +87,28 @@ export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => { window.ipcRenderer.send('ytmd:volume-changed', api.getVolume()); }); +export const setupShuffleChangedListener = singleton(() => { + const playerBar = document.querySelector('ytmusic-player-bar'); + + if (!playerBar) { + window.ipcRenderer.send('ytmd:shuffle-changed-supported', false); + return; + } + + const observer = new MutationObserver(() => { + window.ipcRenderer.send( + 'ytmd:shuffle-changed', + (playerBar?.attributes.getNamedItem('shuffle-on') ?? null) !== null, + ); + }); + + observer.observe(playerBar, { + attributes: true, + childList: false, + subtree: false, + }); +}); + export const setupFullScreenChangedListener = singleton(() => { const playerBar = document.querySelector('ytmusic-player-bar'); @@ -139,6 +161,10 @@ export default (api: YoutubePlayer) => { setupVolumeChangedListener(api); }); + window.ipcRenderer.on('ytmd:setup-shuffle-changed-listener', () => { + setupShuffleChangedListener(); + }); + window.ipcRenderer.on('ytmd:setup-fullscreen-changed-listener', () => { setupFullScreenChangedListener(); }); diff --git a/src/renderer.ts b/src/renderer.ts index d2a06ae820..e7be8bcf74 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -80,6 +80,20 @@ async function onApiLoaded() { >('ytmusic-player-bar') ?.queue.shuffle(); }); + + const isShuffled = () => { + const isShuffled = + document + .querySelector('ytmusic-player-bar') + ?.attributes.getNamedItem('shuffle-on') ?? null; + + return isShuffled !== null; + }; + + window.ipcRenderer.on('ytmd:get-shuffle', () => { + window.ipcRenderer.send('ytmd:get-shuffle-response', isShuffled()); + }); + window.ipcRenderer.on( 'ytmd:update-like', (_, status: 'LIKE' | 'DISLIKE' = 'LIKE') => {