Files
youtube-music/src/plugins/api-server/backend/main.ts
2025-07-10 14:14:43 +09:00

153 lines
3.7 KiB
TypeScript

import { jwt } from 'hono/jwt';
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
import { cors } from 'hono/cors';
import { swaggerUI } from '@hono/swagger-ui';
import { serve } from '@hono/node-server';
import registerCallback from '@/providers/song-info';
import { createBackend } from '@/utils';
import { JWTPayloadSchema } from './scheme';
import { registerAuth, registerControl } from './routes';
import { type APIServerConfig, AuthStrategy } from '../config';
import type { BackendType } from './types';
import type { RepeatMode } from '@/types/datahost-get-state';
export const backend = createBackend<BackendType, APIServerConfig>({
async start(ctx) {
const config = await ctx.getConfig();
await this.init(ctx);
registerCallback((songInfo) => {
this.songInfo = songInfo;
});
ctx.ipc.on('ytmd:player-api-loaded', () => {
ctx.ipc.send('ytmd:setup-time-changed-listener');
ctx.ipc.send('ytmd:setup-repeat-changed-listener');
ctx.ipc.send('ytmd:setup-volume-changed-listener');
});
ctx.ipc.on(
'ytmd:repeat-changed',
(mode: RepeatMode) => (this.currentRepeatMode = mode),
);
ctx.ipc.on(
'ytmd:volume-changed',
(newVolume: number) => (this.volume = newVolume),
);
this.run(config.hostname, config.port);
},
stop() {
this.end();
},
onConfigChange(config) {
if (
this.oldConfig?.hostname === config.hostname &&
this.oldConfig?.port === config.port
) {
this.oldConfig = config;
return;
}
this.end();
this.run(config.hostname, config.port);
this.oldConfig = config;
},
// Custom
async init(ctx) {
const config = await ctx.getConfig();
this.app = new Hono();
this.app.use('*', cors());
// for web remote control
this.app.use('*', async (ctx, next) => {
ctx.header('Access-Control-Request-Private-Network', 'true');
await next();
});
// middlewares
this.app.use('/api/*', async (ctx, next) => {
if (config.authStrategy !== AuthStrategy.NONE) {
return await jwt({
secret: config.secret,
})(ctx, next);
}
await next();
});
this.app.use('/api/*', async (ctx, next) => {
const result = await JWTPayloadSchema.spa(await ctx.get('jwtPayload'));
const isAuthorized =
config.authStrategy === AuthStrategy.NONE ||
(result.success && config.authorizedClients.includes(result.data.id));
if (!isAuthorized) {
ctx.status(401);
return ctx.body('Unauthorized');
}
return await next();
});
// routes
registerControl(
this.app,
ctx,
() => this.songInfo,
() => this.currentRepeatMode,
() => this.volume,
);
registerAuth(this.app, ctx);
// swagger
this.app.openAPIRegistry.registerComponent(
'securitySchemes',
'bearerAuth',
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
);
this.app.doc('/doc', {
openapi: '3.1.0',
info: {
version: '1.0.0',
title: 'Youtube Music API Server',
description:
'Note: You need to get an access token using the `/auth/{id}` endpoint first to call any API endpoints under `/api`.',
},
security: [
{
bearerAuth: [],
},
],
});
this.app.get('/swagger', swaggerUI({ url: '/doc' }));
},
run(hostname, port) {
if (!this.app) return;
try {
this.server = serve({
fetch: this.app.fetch.bind(this.app),
port,
hostname,
});
} catch (err) {
console.error(err);
}
},
end() {
this.server?.close();
this.server = undefined;
},
});