test: Built media server CRUD routes, connection test/sections endpoint…
- "src/server/routes/media-server.ts" - "src/__tests__/media-server-api.test.ts" - "src/index.ts" - "src/server/index.ts" GSD-Task: S04/T03
This commit is contained in:
parent
73c232a845
commit
9ef0323480
4 changed files with 593 additions and 0 deletions
332
src/__tests__/media-server-api.test.ts
Normal file
332
src/__tests__/media-server-api.test.ts
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { type FastifyInstance } from 'fastify';
|
||||
import { initDatabaseAsync, closeDatabase } from '../db/index';
|
||||
import { runMigrations } from '../db/migrate';
|
||||
import { buildServer } from '../server/index';
|
||||
import { systemConfig } from '../db/schema/index';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { type LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import type * as schema from '../db/schema/index';
|
||||
|
||||
/**
|
||||
* Integration tests for media-server CRUD + action API endpoints.
|
||||
* Uses Fastify inject — no real HTTP ports.
|
||||
*/
|
||||
|
||||
describe('Media Server API', () => {
|
||||
let server: FastifyInstance;
|
||||
let db: LibSQLDatabase<typeof schema>;
|
||||
let apiKey: string;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'tubearr-media-server-api-'));
|
||||
const dbPath = join(tmpDir, 'test.db');
|
||||
db = await initDatabaseAsync(dbPath);
|
||||
await runMigrations(dbPath);
|
||||
server = await buildServer({ db });
|
||||
await server.ready();
|
||||
|
||||
// Read API key from database (generated by auth plugin)
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(systemConfig)
|
||||
.where(eq(systemConfig.key, 'api_key'))
|
||||
.limit(1);
|
||||
apiKey = rows[0]?.value ?? '';
|
||||
expect(apiKey).toBeTruthy();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
closeDatabase();
|
||||
try {
|
||||
if (tmpDir && existsSync(tmpDir)) {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
});
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function authed(opts: Record<string, unknown>) {
|
||||
return {
|
||||
...opts,
|
||||
headers: { 'x-api-key': apiKey, ...(opts.headers as Record<string, string> | undefined) },
|
||||
};
|
||||
}
|
||||
|
||||
const plexBody = {
|
||||
name: 'My Plex',
|
||||
type: 'plex' as const,
|
||||
url: 'http://plex.local:32400',
|
||||
token: 'abc123secret',
|
||||
librarySection: '1',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const jellyfinBody = {
|
||||
name: 'My Jellyfin',
|
||||
type: 'jellyfin' as const,
|
||||
url: 'http://jellyfin.local:8096',
|
||||
token: 'jf-token-secret',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// ── CRUD ──
|
||||
|
||||
describe('CRUD', () => {
|
||||
it('POST /api/v1/media-servers creates a server and redacts token', async () => {
|
||||
const res = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: plexBody,
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
const body = res.json();
|
||||
expect(body.name).toBe('My Plex');
|
||||
expect(body.type).toBe('plex');
|
||||
expect(body.url).toBe('http://plex.local:32400');
|
||||
// Token should be redacted
|
||||
expect(body.token).not.toBe('abc123secret');
|
||||
expect(body.token).toContain('****');
|
||||
expect(body.id).toBeTypeOf('number');
|
||||
});
|
||||
|
||||
it('GET /api/v1/media-servers lists all servers', async () => {
|
||||
// Create a second server
|
||||
await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: jellyfinBody,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await server.inject(
|
||||
authed({ method: 'GET', url: '/api/v1/media-servers' })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(body.length).toBeGreaterThanOrEqual(2);
|
||||
// All tokens should be redacted
|
||||
for (const s of body) {
|
||||
expect(s.token).toContain('****');
|
||||
}
|
||||
});
|
||||
|
||||
it('GET /api/v1/media-servers/:id returns a single server', async () => {
|
||||
const createRes = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: { ...plexBody, name: 'Get-By-Id Test' },
|
||||
})
|
||||
);
|
||||
const created = createRes.json();
|
||||
|
||||
const res = await server.inject(
|
||||
authed({ method: 'GET', url: `/api/v1/media-servers/${created.id}` })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().name).toBe('Get-By-Id Test');
|
||||
});
|
||||
|
||||
it('GET /api/v1/media-servers/:id returns 404 for missing', async () => {
|
||||
const res = await server.inject(
|
||||
authed({ method: 'GET', url: '/api/v1/media-servers/99999' })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('PUT /api/v1/media-servers/:id updates fields', async () => {
|
||||
const createRes = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: plexBody,
|
||||
})
|
||||
);
|
||||
const id = createRes.json().id;
|
||||
|
||||
const res = await server.inject(
|
||||
authed({
|
||||
method: 'PUT',
|
||||
url: `/api/v1/media-servers/${id}`,
|
||||
payload: { name: 'Updated Plex', enabled: false },
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(body.name).toBe('Updated Plex');
|
||||
expect(body.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('PUT /api/v1/media-servers/:id returns 404 for missing', async () => {
|
||||
const res = await server.inject(
|
||||
authed({
|
||||
method: 'PUT',
|
||||
url: '/api/v1/media-servers/99999',
|
||||
payload: { name: 'Nope' },
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('DELETE /api/v1/media-servers/:id deletes and returns 204', async () => {
|
||||
const createRes = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: jellyfinBody,
|
||||
})
|
||||
);
|
||||
const id = createRes.json().id;
|
||||
|
||||
const res = await server.inject(
|
||||
authed({ method: 'DELETE', url: `/api/v1/media-servers/${id}` })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(204);
|
||||
|
||||
// Confirm gone
|
||||
const getRes = await server.inject(
|
||||
authed({ method: 'GET', url: `/api/v1/media-servers/${id}` })
|
||||
);
|
||||
expect(getRes.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('DELETE /api/v1/media-servers/:id returns 404 for missing', async () => {
|
||||
const res = await server.inject(
|
||||
authed({ method: 'DELETE', url: '/api/v1/media-servers/99999' })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Validation ──
|
||||
|
||||
describe('validation', () => {
|
||||
it('rejects POST with missing required fields', async () => {
|
||||
const res = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: { name: 'Missing fields' },
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects POST with invalid type', async () => {
|
||||
const res = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: { ...plexBody, type: 'emby' },
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects non-numeric ID param', async () => {
|
||||
const res = await server.inject(
|
||||
authed({ method: 'GET', url: '/api/v1/media-servers/abc' })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Action endpoints ──
|
||||
|
||||
describe('actions', () => {
|
||||
it('POST /api/v1/media-servers/:id/test returns 404 for missing server', async () => {
|
||||
const res = await server.inject(
|
||||
authed({ method: 'POST', url: '/api/v1/media-servers/99999/test' })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('GET /api/v1/media-servers/:id/sections returns 404 for missing server', async () => {
|
||||
const res = await server.inject(
|
||||
authed({ method: 'GET', url: '/api/v1/media-servers/99999/sections' })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('POST /api/v1/media-servers/:id/test calls testConnection on a real server record', async () => {
|
||||
// Create a server (the test will fail to actually connect, but verifies the route works)
|
||||
const createRes = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: { ...plexBody, name: 'Test-Connection Target', url: 'http://127.0.0.1:1' },
|
||||
})
|
||||
);
|
||||
const id = createRes.json().id;
|
||||
|
||||
const res = await server.inject(
|
||||
authed({ method: 'POST', url: `/api/v1/media-servers/${id}/test` })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
// Should return a structured result (will fail since no server is running)
|
||||
expect(body).toHaveProperty('success');
|
||||
expect(body).toHaveProperty('message');
|
||||
expect(body.success).toBe(false);
|
||||
});
|
||||
|
||||
it('GET /api/v1/media-servers/:id/sections returns sections array', async () => {
|
||||
// Create a jellyfin server (listLibrarySections returns [] for jellyfin)
|
||||
const createRes = await server.inject(
|
||||
authed({
|
||||
method: 'POST',
|
||||
url: '/api/v1/media-servers',
|
||||
payload: { ...jellyfinBody, name: 'Sections-Test' },
|
||||
})
|
||||
);
|
||||
const id = createRes.json().id;
|
||||
|
||||
const res = await server.inject(
|
||||
authed({ method: 'GET', url: `/api/v1/media-servers/${id}/sections` })
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Auth ──
|
||||
|
||||
describe('auth', () => {
|
||||
it('rejects requests without API key', async () => {
|
||||
const res = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/api/v1/media-servers',
|
||||
});
|
||||
|
||||
// Should be 401 or 403 depending on auth plugin behavior
|
||||
expect(res.statusCode).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
35
src/index.ts
35
src/index.ts
|
|
@ -19,6 +19,8 @@ import { DownloadEventBus } from './services/event-bus';
|
|||
import { QueueService } from './services/queue';
|
||||
import { NotificationService } from './services/notification';
|
||||
import { HealthService } from './services/health';
|
||||
import { MediaServerService } from './services/media-server';
|
||||
import { getEnabledMediaServers } from './db/repositories/media-server-repository';
|
||||
import { PlatformRegistry } from './sources/platform-source';
|
||||
import { YouTubeSource } from './sources/youtube';
|
||||
import { SoundCloudSource } from './sources/soundcloud';
|
||||
|
|
@ -165,6 +167,39 @@ async function main(): Promise<void> {
|
|||
);
|
||||
(server as { healthService: HealthService | null }).healthService = healthService;
|
||||
|
||||
// 5c. Wire automatic media-server scans on download completion
|
||||
const mediaServerService = new MediaServerService();
|
||||
eventBus.onDownload('download:complete', (payload) => {
|
||||
getEnabledMediaServers(db)
|
||||
.then(async (servers) => {
|
||||
if (servers.length === 0) return;
|
||||
console.log(
|
||||
`[media-server] download complete contentItemId=${payload.contentItemId} — triggering ${servers.length} server scan(s)`
|
||||
);
|
||||
const results = await Promise.allSettled(
|
||||
servers.map((s) => mediaServerService.triggerScan(s))
|
||||
);
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const r = results[i];
|
||||
const s = servers[i];
|
||||
if (r.status === 'fulfilled') {
|
||||
if (r.value.success) {
|
||||
console.log(`[media-server] scan ok server="${s.name}" msg="${r.value.message}"`);
|
||||
} else {
|
||||
console.log(`[media-server] scan failed server="${s.name}" msg="${r.value.message}"`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[media-server] scan error server="${s.name}" err="${r.reason}"`);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(
|
||||
`[media-server] failed to query enabled servers: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 6. Graceful shutdown handler
|
||||
const shutdown = async (signal: string) => {
|
||||
console.log(`[${APP_NAME}] ${signal} received — shutting down gracefully...`);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { scanRoutes } from './routes/scan';
|
|||
import { collectRoutes } from './routes/collect';
|
||||
import { playlistRoutes } from './routes/playlist';
|
||||
import { adhocDownloadRoutes } from './routes/adhoc-download';
|
||||
import { mediaServerRoutes } from './routes/media-server';
|
||||
import { websocketRoutes } from './routes/websocket';
|
||||
import type { SchedulerService } from '../services/scheduler';
|
||||
import type { DownloadService } from '../services/download';
|
||||
|
|
@ -111,6 +112,7 @@ export async function buildServer(opts: BuildServerOptions): Promise<FastifyInst
|
|||
await server.register(collectRoutes);
|
||||
await server.register(playlistRoutes);
|
||||
await server.register(adhocDownloadRoutes);
|
||||
await server.register(mediaServerRoutes);
|
||||
|
||||
// Register WebSocket route (before static file serving so /ws is handled)
|
||||
if (opts.eventBus) {
|
||||
|
|
|
|||
224
src/server/routes/media-server.ts
Normal file
224
src/server/routes/media-server.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue