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(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 { 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); } ); }