feat(api-server): add endpoint to get shuffle state (#2792)

This commit is contained in:
Franz DC
2025-01-18 13:23:17 +08:00
committed by GitHub
parent 3339f997e3
commit 368b251e3f
6 changed files with 102 additions and 6 deletions

View File

@ -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({ shuffle: createRoute({
method: 'post', method: 'post',
path: `/api/${API_VERSION}/shuffle`, path: `/api/${API_VERSION}/shuffle`,
@ -581,6 +598,25 @@ export const register = (
ctx.status(204); ctx.status(204);
return ctx.body(null); return ctx.body(null);
}); });
app.openapi(routes.getShuffleState, async (ctx) => {
const stateResponsePromise = new Promise<boolean>((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) => { app.openapi(routes.shuffle, (ctx) => {
controller.shuffle(); controller.shuffle();

View File

@ -86,7 +86,7 @@ declare module '@jellybrick/mpris-service' {
supportedMimeTypes: string[]; supportedMimeTypes: string[];
canQuit: boolean; canQuit: boolean;
canRaise: boolean; canRaise: boolean;
canSetFullscreen?: boolean; canUsePlayerControls?: boolean;
desktopEntry?: string; desktopEntry?: string;
hasTrackList: boolean; hasTrackList: boolean;

View File

@ -77,7 +77,7 @@ function setupMPRIS() {
instance.canRaise = true; instance.canRaise = true;
instance.canQuit = false; instance.canQuit = false;
instance.canSetFullscreen = true; instance.canUsePlayerControls = true;
instance.supportedUriSchemes = ['http', 'https']; instance.supportedUriSchemes = ['http', 'https'];
instance.desktopEntry = 'youtube-music'; instance.desktopEntry = 'youtube-music';
return instance; return instance;
@ -93,6 +93,7 @@ function registerMPRIS(win: BrowserWindow) {
shuffle, shuffle,
switchRepeat, switchRepeat,
setFullscreen, setFullscreen,
requestShuffleInformation,
requestFullscreenInformation, requestFullscreenInformation,
requestQueueInformation, requestQueueInformation,
} = songControls; } = songControls;
@ -126,8 +127,10 @@ function registerMPRIS(win: BrowserWindow) {
win.webContents.send('ytmd:setup-time-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-time-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-repeat-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-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-fullscreen-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-autoplay-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-autoplay-changed-listener', 'mpris');
requestShuffleInformation();
requestFullscreenInformation(); requestFullscreenInformation();
requestQueueInformation(); requestQueueInformation();
}); });
@ -156,8 +159,16 @@ function registerMPRIS(win: BrowserWindow) {
requestQueueInformation(); 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) => { ipcMain.on('ytmd:fullscreen-changed', (_, changedTo: boolean) => {
if (player.fullscreen === undefined || !player.canSetFullscreen) { if (player.fullscreen === undefined || !player.canUsePlayerControls) {
return; return;
} }
@ -168,7 +179,7 @@ function registerMPRIS(win: BrowserWindow) {
ipcMain.on( ipcMain.on(
'ytmd:set-fullscreen', 'ytmd:set-fullscreen',
(_, isFullscreen: boolean | undefined) => { (_, isFullscreen: boolean | undefined) => {
if (!player.canSetFullscreen || isFullscreen === undefined) { if (!player.canUsePlayerControls || isFullscreen === undefined) {
return; return;
} }
@ -179,7 +190,7 @@ function registerMPRIS(win: BrowserWindow) {
ipcMain.on( ipcMain.on(
'ytmd:fullscreen-changed-supported', 'ytmd:fullscreen-changed-supported',
(_, isFullscreenSupported: boolean) => { (_, isFullscreenSupported: boolean) => {
player.canSetFullscreen = isFullscreenSupported; player.canUsePlayerControls = isFullscreenSupported;
}, },
); );
ipcMain.on('ytmd:autoplay-changed', (_) => { ipcMain.on('ytmd:autoplay-changed', (_) => {
@ -272,6 +283,12 @@ function registerMPRIS(win: BrowserWindow) {
player.on('position', seekTo); player.on('position', seekTo);
player.on('shuffle', (enableShuffle) => { player.on('shuffle', (enableShuffle) => {
if (!player.canUsePlayerControls || enableShuffle === undefined) {
return;
}
player.shuffle = enableShuffle;
if (enableShuffle) { if (enableShuffle) {
shuffle(); shuffle();
requestQueueInformation(); requestQueueInformation();

View File

@ -62,6 +62,9 @@ export default (win: BrowserWindow) => {
win.webContents.send('ytmd:seek-by', seconds); win.webContents.send('ytmd:seek-by', seconds);
} }
}, },
requestShuffleInformation: () => {
win.webContents.send('ytmd:get-shuffle');
},
shuffle: () => win.webContents.send('ytmd:shuffle'), shuffle: () => win.webContents.send('ytmd:shuffle'),
switchRepeat: (n: ArgsType<number> = 1) => { switchRepeat: (n: ArgsType<number> = 1) => {
const repeat = parseNumberFromArgsType(n); const repeat = parseNumberFromArgsType(n);

View File

@ -87,6 +87,28 @@ export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
window.ipcRenderer.send('ytmd:volume-changed', api.getVolume()); 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(() => { export const setupFullScreenChangedListener = singleton(() => {
const playerBar = document.querySelector('ytmusic-player-bar'); const playerBar = document.querySelector('ytmusic-player-bar');
@ -139,6 +161,10 @@ export default (api: YoutubePlayer) => {
setupVolumeChangedListener(api); setupVolumeChangedListener(api);
}); });
window.ipcRenderer.on('ytmd:setup-shuffle-changed-listener', () => {
setupShuffleChangedListener();
});
window.ipcRenderer.on('ytmd:setup-fullscreen-changed-listener', () => { window.ipcRenderer.on('ytmd:setup-fullscreen-changed-listener', () => {
setupFullScreenChangedListener(); setupFullScreenChangedListener();
}); });

View File

@ -80,6 +80,20 @@ async function onApiLoaded() {
>('ytmusic-player-bar') >('ytmusic-player-bar')
?.queue.shuffle(); ?.queue.shuffle();
}); });
const isShuffled = () => {
const isShuffled =
document
.querySelector<HTMLElement>('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( window.ipcRenderer.on(
'ytmd:update-like', 'ytmd:update-like',
(_, status: 'LIKE' | 'DISLIKE' = 'LIKE') => { (_, status: 'LIKE' | 'DISLIKE' = 'LIKE') => {