Files
youtube-music/src/plugins/api-server/backend/routes/control.ts
2024-12-25 18:55:24 +09:00

717 lines
16 KiB
TypeScript

import { createRoute, z } from '@hono/zod-openapi';
import { ipcMain } from 'electron';
import getSongControls from '@/providers/song-controls';
import {
AddSongToQueueSchema,
AuthHeadersSchema,
GoBackSchema,
GoForwardScheme,
MoveSongInQueueSchema,
QueueParamsSchema,
SearchSchema,
SeekSchema,
SetFullscreenSchema,
SetQueueIndexSchema,
SetVolumeSchema,
SongInfoSchema,
SwitchRepeatSchema,
type ResponseSongInfo,
} from '../scheme';
import type { RepeatMode } from '@/types/datahost-get-state';
import type { SongInfo } from '@/providers/song-info';
import type { BackendContext } from '@/types/contexts';
import type { APIServerConfig } from '../../config';
import type { HonoApp } from '../types';
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
import type { Context } from 'hono';
const API_VERSION = 'v1';
const routes = {
previous: createRoute({
method: 'post',
path: `/api/${API_VERSION}/previous`,
summary: 'play previous song',
description: 'Plays the previous song in the queue',
responses: {
204: {
description: 'Success',
},
},
}),
next: createRoute({
method: 'post',
path: `/api/${API_VERSION}/next`,
summary: 'play next song',
description: 'Plays the next song in the queue',
responses: {
204: {
description: 'Success',
},
},
}),
play: createRoute({
method: 'post',
path: `/api/${API_VERSION}/play`,
summary: 'Play',
description: 'Change the state of the player to play',
responses: {
204: {
description: 'Success',
},
},
}),
pause: createRoute({
method: 'post',
path: `/api/${API_VERSION}/pause`,
summary: 'Pause',
description: 'Change the state of the player to pause',
responses: {
204: {
description: 'Success',
},
},
}),
togglePlay: createRoute({
method: 'post',
path: `/api/${API_VERSION}/toggle-play`,
summary: 'Toggle play/pause',
description:
'Change the state of the player to play if paused, or pause if playing',
responses: {
204: {
description: 'Success',
},
},
}),
like: createRoute({
method: 'post',
path: `/api/${API_VERSION}/like`,
summary: 'like song',
description: 'Set the current song as liked',
responses: {
204: {
description: 'Success',
},
},
}),
dislike: createRoute({
method: 'post',
path: `/api/${API_VERSION}/dislike`,
summary: 'dislike song',
description: 'Set the current song as disliked',
responses: {
204: {
description: 'Success',
},
},
}),
seekTo: createRoute({
method: 'post',
path: `/api/${API_VERSION}/seek-to`,
summary: 'seek',
description: 'Seek to a specific time in the current song',
request: {
headers: AuthHeadersSchema,
body: {
description: 'seconds to seek to',
content: {
'application/json': {
schema: SeekSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
goBack: createRoute({
method: 'post',
path: `/api/${API_VERSION}/go-back`,
summary: 'go back',
description: 'Move the current song back by a number of seconds',
request: {
headers: AuthHeadersSchema,
body: {
description: 'seconds to go back',
content: {
'application/json': {
schema: GoBackSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
goForward: createRoute({
method: 'post',
path: `/api/${API_VERSION}/go-forward`,
summary: 'go forward',
description: 'Move the current song forward by a number of seconds',
request: {
headers: AuthHeadersSchema,
body: {
description: 'seconds to go forward',
content: {
'application/json': {
schema: GoForwardScheme,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
shuffle: createRoute({
method: 'post',
path: `/api/${API_VERSION}/shuffle`,
summary: 'shuffle',
description: 'Shuffle the queue',
responses: {
204: {
description: 'Success',
},
},
}),
repeatMode: createRoute({
method: 'get',
path: `/api/${API_VERSION}/repeat-mode`,
summary: 'get current repeat mode',
description: 'Get the current repeat mode (NONE, ALL, ONE)',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({
mode: z.enum(['ONE', 'NONE', 'ALL']).nullable(),
}),
},
},
},
},
}),
switchRepeat: createRoute({
method: 'post',
path: `/api/${API_VERSION}/switch-repeat`,
summary: 'switch repeat',
description: 'Switch the repeat mode',
request: {
headers: AuthHeadersSchema,
body: {
description: 'number of times to click the repeat button',
content: {
'application/json': {
schema: SwitchRepeatSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
setVolume: createRoute({
method: 'post',
path: `/api/${API_VERSION}/volume`,
summary: 'set volume',
description: 'Set the volume of the player',
request: {
headers: AuthHeadersSchema,
body: {
description: 'volume to set',
content: {
'application/json': {
schema: SetVolumeSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
setFullscreen: createRoute({
method: 'post',
path: `/api/${API_VERSION}/fullscreen`,
summary: 'set fullscreen',
description: 'Set the fullscreen state of the player',
request: {
headers: AuthHeadersSchema,
body: {
description: 'fullscreen state',
content: {
'application/json': {
schema: SetFullscreenSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
toggleMute: createRoute({
method: 'post',
path: `/api/${API_VERSION}/toggle-mute`,
summary: 'toggle mute',
description: 'Toggle the mute state of the player',
responses: {
204: {
description: 'Success',
},
},
}),
getFullscreenState: createRoute({
method: 'get',
path: `/api/${API_VERSION}/fullscreen`,
summary: 'get fullscreen state',
description: 'Get the current fullscreen state',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({
state: z.boolean(),
}),
},
},
},
},
}),
oldQueueInfo: createRoute({
deprecated: true,
method: 'get',
path: `/api/${API_VERSION}/queue-info`,
summary: 'get current queue info',
description: 'Get the current queue info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({}),
},
},
},
204: {
description: 'No queue info',
},
},
}),
oldSongInfo: createRoute({
deprecated: true,
method: 'get',
path: `/api/${API_VERSION}/song-info`,
summary: 'get current song info',
description: 'Get the current song info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: SongInfoSchema,
},
},
},
204: {
description: 'No song info',
},
},
}),
songInfo: createRoute({
method: 'get',
path: `/api/${API_VERSION}/song`,
summary: 'get current song info',
description: 'Get the current song info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: SongInfoSchema,
},
},
},
204: {
description: 'No song info',
},
},
}),
queueInfo: createRoute({
method: 'get',
path: `/api/${API_VERSION}/queue`,
summary: 'get current queue info',
description: 'Get the current queue info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({}),
},
},
},
204: {
description: 'No queue info',
},
},
}),
addSongToQueue: createRoute({
method: 'post',
path: `/api/${API_VERSION}/queue`,
summary: 'add song to queue',
description: 'Add a song to the queue',
request: {
headers: AuthHeadersSchema,
body: {
description: 'video id of the song to add',
content: {
'application/json': {
schema: AddSongToQueueSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
moveSongInQueue: createRoute({
method: 'patch',
path: `/api/${API_VERSION}/queue/{index}`,
summary: 'move song in queue',
description: 'Move a song in the queue',
request: {
headers: AuthHeadersSchema,
params: QueueParamsSchema,
body: {
description: 'index to move the song to',
content: {
'application/json': {
schema: MoveSongInQueueSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
removeSongFromQueue: createRoute({
method: 'delete',
path: `/api/${API_VERSION}/queue/{index}`,
summary: 'remove song from queue',
description: 'Remove a song from the queue',
request: {
headers: AuthHeadersSchema,
params: QueueParamsSchema,
},
responses: {
204: {
description: 'Success',
},
},
}),
setQueueIndex: createRoute({
method: 'patch',
path: `/api/${API_VERSION}/queue`,
summary: 'set queue index',
description: 'Set the current index of the queue',
request: {
headers: AuthHeadersSchema,
body: {
description: 'index to move the song to',
content: {
'application/json': {
schema: SetQueueIndexSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
clearQueue: createRoute({
method: 'delete',
path: `/api/${API_VERSION}/queue`,
summary: 'clear queue',
description: 'Clear the queue',
responses: {
204: {
description: 'Success',
},
},
}),
search: createRoute({
method: 'post',
path: `/api/${API_VERSION}/search`,
summary: 'search for a song',
description: 'search for a song',
request: {
headers: AuthHeadersSchema,
body: {
description: 'search query',
content: {
'application/json': {
schema: SearchSchema,
},
},
},
},
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({}),
},
},
},
},
}),
};
export const register = (
app: HonoApp,
{ window }: BackendContext<APIServerConfig>,
songInfoGetter: () => SongInfo | undefined,
repeatModeGetter: () => RepeatMode | undefined,
) => {
const controller = getSongControls(window);
app.openapi(routes.previous, (ctx) => {
controller.previous();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.next, (ctx) => {
controller.next();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.play, (ctx) => {
controller.play();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.pause, (ctx) => {
controller.pause();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.togglePlay, (ctx) => {
controller.playPause();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.like, (ctx) => {
controller.like();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.dislike, (ctx) => {
controller.dislike();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.seekTo, (ctx) => {
const { seconds } = ctx.req.valid('json');
controller.seekTo(seconds);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.goBack, (ctx) => {
const { seconds } = ctx.req.valid('json');
controller.goBack(seconds);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.goForward, (ctx) => {
const { seconds } = ctx.req.valid('json');
controller.goForward(seconds);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.shuffle, (ctx) => {
controller.shuffle();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.repeatMode, (ctx) => {
ctx.status(200);
return ctx.json({ mode: repeatModeGetter() ?? null });
});
app.openapi(routes.switchRepeat, (ctx) => {
const { iteration } = ctx.req.valid('json');
controller.switchRepeat(iteration);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.setVolume, (ctx) => {
const { volume } = ctx.req.valid('json');
controller.setVolume(volume);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.setFullscreen, (ctx) => {
const { state } = ctx.req.valid('json');
controller.setFullscreen(state);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.toggleMute, (ctx) => {
controller.muteUnmute();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.getFullscreenState, async (ctx) => {
const stateResponsePromise = new Promise<boolean>((resolve) => {
ipcMain.once(
'ytmd:set-fullscreen',
(_, isFullscreen: boolean | undefined) => {
return resolve(!!isFullscreen);
},
);
controller.requestFullscreenInformation();
});
const fullscreen = await stateResponsePromise;
ctx.status(200);
return ctx.json({ state: fullscreen });
});
const songInfo = (ctx: Context) => {
const info = songInfoGetter();
if (!info) {
ctx.status(204);
return ctx.body(null);
}
const body = { ...info };
delete body.image;
ctx.status(200);
return ctx.json(body satisfies ResponseSongInfo);
};
app.openapi(routes.oldSongInfo, songInfo);
app.openapi(routes.songInfo, songInfo);
// Queue
const queueInfo = async (ctx: Context) => {
const queueResponsePromise = new Promise<QueueResponse>((resolve) => {
ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => {
return resolve(queue);
});
controller.requestQueueInformation();
});
const info = await queueResponsePromise;
if (!info) {
ctx.status(204);
return ctx.body(null);
}
ctx.status(200);
return ctx.json(info);
};
app.openapi(routes.oldQueueInfo, queueInfo);
app.openapi(routes.queueInfo, queueInfo);
app.openapi(routes.addSongToQueue, (ctx) => {
const { videoId } = ctx.req.valid('json');
controller.addSongToQueue(videoId);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.moveSongInQueue, (ctx) => {
const index = Number(ctx.req.param('index'));
const { toIndex } = ctx.req.valid('json');
controller.moveSongInQueue(index, toIndex);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.removeSongFromQueue, (ctx) => {
const index = Number(ctx.req.param('index'));
controller.removeSongFromQueue(index);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.setQueueIndex, (ctx) => {
const { index } = ctx.req.valid('json');
controller.setQueueIndex(index);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.clearQueue, (ctx) => {
controller.clearQueue();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.search, async (ctx) => {
const { query } = ctx.req.valid('json');
const response = await controller.search(query);
ctx.status(200);
return ctx.json(response as object);
});
};