- "src/server/routes/media-server.ts" - "src/__tests__/media-server-api.test.ts" - "src/index.ts" - "src/server/index.ts" GSD-Task: S04/T03
224 lines
6.8 KiB
TypeScript
224 lines
6.8 KiB
TypeScript
import { type FastifyInstance } from 'fastify';
|
|
import { parseIdParam } from './helpers';
|
|
import {
|
|
createMediaServer,
|
|
getAllMediaServers,
|
|
getMediaServerById,
|
|
updateMediaServer,
|
|
deleteMediaServer,
|
|
} from '../../db/repositories/media-server-repository';
|
|
import { MediaServerService } from '../../services/media-server';
|
|
|
|
// ── JSON Schemas for Fastify Validation ──
|
|
|
|
const createMediaServerBodySchema = {
|
|
type: 'object' as const,
|
|
required: ['name', 'type', 'url', 'token'],
|
|
properties: {
|
|
name: { type: 'string' as const, minLength: 1 },
|
|
type: { type: 'string' as const, enum: ['plex', 'jellyfin'] },
|
|
url: { type: 'string' as const, minLength: 1 },
|
|
token: { type: 'string' as const, minLength: 1 },
|
|
librarySection: { type: 'string' as const, nullable: true },
|
|
enabled: { type: 'boolean' as const },
|
|
},
|
|
additionalProperties: false,
|
|
};
|
|
|
|
const updateMediaServerBodySchema = {
|
|
type: 'object' as const,
|
|
properties: {
|
|
name: { type: 'string' as const, minLength: 1 },
|
|
type: { type: 'string' as const, enum: ['plex', 'jellyfin'] },
|
|
url: { type: 'string' as const, minLength: 1 },
|
|
token: { type: 'string' as const, minLength: 1 },
|
|
librarySection: { type: 'string' as const, nullable: true },
|
|
enabled: { type: 'boolean' as const },
|
|
},
|
|
additionalProperties: false,
|
|
};
|
|
|
|
// ── Helpers ──
|
|
|
|
/** Redact the token for API responses — show first 4 chars + '…'. */
|
|
function redactToken(token: string): string {
|
|
if (token.length <= 4) return '****';
|
|
return token.slice(0, 4) + '****';
|
|
}
|
|
|
|
/** Return a copy of the media server with the token redacted. */
|
|
function redactServer<T extends { token: string }>(server: T): T {
|
|
return { ...server, token: redactToken(server.token) };
|
|
}
|
|
|
|
// ── Route Plugin ──
|
|
|
|
/**
|
|
* Media server CRUD + action route plugin.
|
|
*
|
|
* Registers:
|
|
* POST /api/v1/media-servers — create a media server
|
|
* GET /api/v1/media-servers — list all media servers (redacted tokens)
|
|
* GET /api/v1/media-servers/:id — get a single server (redacted token)
|
|
* PUT /api/v1/media-servers/:id — update server fields
|
|
* DELETE /api/v1/media-servers/:id — delete a server
|
|
* POST /api/v1/media-servers/:id/test — test connection
|
|
* GET /api/v1/media-servers/:id/sections — list library sections
|
|
*/
|
|
export async function mediaServerRoutes(fastify: FastifyInstance): Promise<void> {
|
|
const service = new MediaServerService();
|
|
|
|
// ── POST /api/v1/media-servers ──
|
|
|
|
fastify.post<{
|
|
Body: {
|
|
name: string;
|
|
type: 'plex' | 'jellyfin';
|
|
url: string;
|
|
token: string;
|
|
librarySection?: string | null;
|
|
enabled?: boolean;
|
|
};
|
|
}>(
|
|
'/api/v1/media-servers',
|
|
{ schema: { body: createMediaServerBodySchema } },
|
|
async (request, reply) => {
|
|
const server = await createMediaServer(fastify.db, request.body);
|
|
request.log.info(
|
|
{ serverId: server.id, name: server.name, type: server.type },
|
|
`[media-server] created server="${server.name}" type=${server.type}`
|
|
);
|
|
return reply.status(201).send(redactServer(server));
|
|
}
|
|
);
|
|
|
|
// ── GET /api/v1/media-servers ──
|
|
|
|
fastify.get('/api/v1/media-servers', async () => {
|
|
const servers = await getAllMediaServers(fastify.db);
|
|
return servers.map(redactServer);
|
|
});
|
|
|
|
// ── GET /api/v1/media-servers/:id ──
|
|
|
|
fastify.get<{ Params: { id: string } }>(
|
|
'/api/v1/media-servers/:id',
|
|
async (request, reply) => {
|
|
const id = parseIdParam(request.params.id, reply, 'Media server ID');
|
|
if (id === null) return;
|
|
|
|
const server = await getMediaServerById(fastify.db, id);
|
|
if (!server) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Media server with ID ${id} not found`,
|
|
});
|
|
}
|
|
|
|
return redactServer(server);
|
|
}
|
|
);
|
|
|
|
// ── PUT /api/v1/media-servers/:id ──
|
|
|
|
fastify.put<{
|
|
Params: { id: string };
|
|
Body: {
|
|
name?: string;
|
|
type?: 'plex' | 'jellyfin';
|
|
url?: string;
|
|
token?: string;
|
|
librarySection?: string | null;
|
|
enabled?: boolean;
|
|
};
|
|
}>(
|
|
'/api/v1/media-servers/:id',
|
|
{ schema: { body: updateMediaServerBodySchema } },
|
|
async (request, reply) => {
|
|
const id = parseIdParam(request.params.id, reply, 'Media server ID');
|
|
if (id === null) return;
|
|
|
|
const updated = await updateMediaServer(fastify.db, id, request.body);
|
|
if (!updated) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Media server with ID ${id} not found`,
|
|
});
|
|
}
|
|
|
|
request.log.info(
|
|
{ serverId: id, fields: Object.keys(request.body) },
|
|
`[media-server] updated server=${id}`
|
|
);
|
|
return redactServer(updated);
|
|
}
|
|
);
|
|
|
|
// ── DELETE /api/v1/media-servers/:id ──
|
|
|
|
fastify.delete<{ Params: { id: string } }>(
|
|
'/api/v1/media-servers/:id',
|
|
async (request, reply) => {
|
|
const id = parseIdParam(request.params.id, reply, 'Media server ID');
|
|
if (id === null) return;
|
|
|
|
const deleted = await deleteMediaServer(fastify.db, id);
|
|
if (!deleted) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Media server with ID ${id} not found`,
|
|
});
|
|
}
|
|
|
|
request.log.info({ serverId: id }, `[media-server] deleted server=${id}`);
|
|
return reply.status(204).send();
|
|
}
|
|
);
|
|
|
|
// ── POST /api/v1/media-servers/:id/test ──
|
|
|
|
fastify.post<{ Params: { id: string } }>(
|
|
'/api/v1/media-servers/:id/test',
|
|
async (request, reply) => {
|
|
const id = parseIdParam(request.params.id, reply, 'Media server ID');
|
|
if (id === null) return;
|
|
|
|
const server = await getMediaServerById(fastify.db, id);
|
|
if (!server) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Media server with ID ${id} not found`,
|
|
});
|
|
}
|
|
|
|
const result = await service.testConnection(server);
|
|
return reply.send(result);
|
|
}
|
|
);
|
|
|
|
// ── GET /api/v1/media-servers/:id/sections ──
|
|
|
|
fastify.get<{ Params: { id: string } }>(
|
|
'/api/v1/media-servers/:id/sections',
|
|
async (request, reply) => {
|
|
const id = parseIdParam(request.params.id, reply, 'Media server ID');
|
|
if (id === null) return;
|
|
|
|
const server = await getMediaServerById(fastify.db, id);
|
|
if (!server) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Media server with ID ${id} not found`,
|
|
});
|
|
}
|
|
|
|
const sections = await service.listLibrarySections(server);
|
|
return reply.send(sections);
|
|
}
|
|
);
|
|
}
|