Migrated git root from W:/programming/Projects/ to W:/programming/Projects/Tubearr/. Previous history preserved in Tubearr-full-backup.bundle at parent directory. Completed milestones: M001 through M005 Active: M006/S02 (Add Channel UX)
207 lines
6.1 KiB
TypeScript
207 lines
6.1 KiB
TypeScript
import { type FastifyInstance } from 'fastify';
|
|
import {
|
|
createFormatProfile,
|
|
getAllFormatProfiles,
|
|
getFormatProfileById,
|
|
updateFormatProfile,
|
|
deleteFormatProfile,
|
|
} from '../../db/repositories/format-profile-repository';
|
|
|
|
// ── JSON Schemas for Fastify Validation ──
|
|
|
|
const createFormatProfileBodySchema = {
|
|
type: 'object' as const,
|
|
required: ['name'],
|
|
properties: {
|
|
name: { type: 'string' as const, minLength: 1 },
|
|
videoResolution: { type: 'string' as const, nullable: true },
|
|
audioCodec: { type: 'string' as const, nullable: true },
|
|
audioBitrate: { type: 'string' as const, nullable: true },
|
|
containerFormat: { type: 'string' as const, nullable: true },
|
|
isDefault: { type: 'boolean' as const },
|
|
subtitleLanguages: { type: 'string' as const, nullable: true },
|
|
embedSubtitles: { type: 'boolean' as const },
|
|
},
|
|
additionalProperties: false,
|
|
};
|
|
|
|
const updateFormatProfileBodySchema = {
|
|
type: 'object' as const,
|
|
properties: {
|
|
name: { type: 'string' as const, minLength: 1 },
|
|
videoResolution: { type: 'string' as const, nullable: true },
|
|
audioCodec: { type: 'string' as const, nullable: true },
|
|
audioBitrate: { type: 'string' as const, nullable: true },
|
|
containerFormat: { type: 'string' as const, nullable: true },
|
|
isDefault: { type: 'boolean' as const },
|
|
subtitleLanguages: { type: 'string' as const, nullable: true },
|
|
embedSubtitles: { type: 'boolean' as const },
|
|
},
|
|
additionalProperties: false,
|
|
};
|
|
|
|
// ── Route Plugin ──
|
|
|
|
/**
|
|
* Format profile CRUD route plugin.
|
|
*
|
|
* Registers:
|
|
* POST /api/v1/format-profile — create a new format profile
|
|
* GET /api/v1/format-profile — list all format profiles
|
|
* GET /api/v1/format-profile/:id — get a single format profile
|
|
* PUT /api/v1/format-profile/:id — update format profile fields
|
|
* DELETE /api/v1/format-profile/:id — delete a format profile
|
|
*/
|
|
export async function formatProfileRoutes(fastify: FastifyInstance): Promise<void> {
|
|
// ── POST /api/v1/format-profile ──
|
|
|
|
fastify.post<{
|
|
Body: {
|
|
name: string;
|
|
videoResolution?: string | null;
|
|
audioCodec?: string | null;
|
|
audioBitrate?: string | null;
|
|
containerFormat?: string | null;
|
|
isDefault?: boolean;
|
|
subtitleLanguages?: string | null;
|
|
embedSubtitles?: boolean;
|
|
};
|
|
}>(
|
|
'/api/v1/format-profile',
|
|
{
|
|
schema: { body: createFormatProfileBodySchema },
|
|
},
|
|
async (request, reply) => {
|
|
const profile = await createFormatProfile(fastify.db, request.body);
|
|
return reply.status(201).send(profile);
|
|
}
|
|
);
|
|
|
|
// ── GET /api/v1/format-profile ──
|
|
|
|
fastify.get('/api/v1/format-profile', async (_request, _reply) => {
|
|
return getAllFormatProfiles(fastify.db);
|
|
});
|
|
|
|
// ── GET /api/v1/format-profile/:id ──
|
|
|
|
fastify.get<{ Params: { id: string } }>(
|
|
'/api/v1/format-profile/:id',
|
|
async (request, reply) => {
|
|
const id = parseInt(request.params.id, 10);
|
|
if (isNaN(id)) {
|
|
return reply.status(400).send({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: 'Format profile ID must be a number',
|
|
});
|
|
}
|
|
|
|
const profile = await getFormatProfileById(fastify.db, id);
|
|
if (!profile) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Format profile with ID ${id} not found`,
|
|
});
|
|
}
|
|
|
|
return profile;
|
|
}
|
|
);
|
|
|
|
// ── PUT /api/v1/format-profile/:id ──
|
|
|
|
fastify.put<{
|
|
Params: { id: string };
|
|
Body: {
|
|
name?: string;
|
|
videoResolution?: string | null;
|
|
audioCodec?: string | null;
|
|
audioBitrate?: string | null;
|
|
containerFormat?: string | null;
|
|
isDefault?: boolean;
|
|
subtitleLanguages?: string | null;
|
|
embedSubtitles?: boolean;
|
|
};
|
|
}>(
|
|
'/api/v1/format-profile/:id',
|
|
{
|
|
schema: { body: updateFormatProfileBodySchema },
|
|
},
|
|
async (request, reply) => {
|
|
const id = parseInt(request.params.id, 10);
|
|
if (isNaN(id)) {
|
|
return reply.status(400).send({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: 'Format profile ID must be a number',
|
|
});
|
|
}
|
|
|
|
// Guard: prevent unsetting isDefault on the default profile
|
|
const existing = await getFormatProfileById(fastify.db, id);
|
|
if (!existing) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Format profile with ID ${id} not found`,
|
|
});
|
|
}
|
|
if (existing.isDefault && request.body.isDefault === false) {
|
|
return reply.status(400).send({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: 'Cannot unset isDefault on the default format profile',
|
|
});
|
|
}
|
|
|
|
const updated = await updateFormatProfile(fastify.db, id, request.body);
|
|
if (!updated) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Format profile with ID ${id} not found`,
|
|
});
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
);
|
|
|
|
// ── DELETE /api/v1/format-profile/:id ──
|
|
|
|
fastify.delete<{ Params: { id: string } }>(
|
|
'/api/v1/format-profile/:id',
|
|
async (request, reply) => {
|
|
const id = parseInt(request.params.id, 10);
|
|
if (isNaN(id)) {
|
|
return reply.status(400).send({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: 'Format profile ID must be a number',
|
|
});
|
|
}
|
|
|
|
// Guard: prevent deleting the default profile
|
|
const profile = await getFormatProfileById(fastify.db, id);
|
|
if (!profile) {
|
|
return reply.status(404).send({
|
|
statusCode: 404,
|
|
error: 'Not Found',
|
|
message: `Format profile with ID ${id} not found`,
|
|
});
|
|
}
|
|
if (profile.isDefault) {
|
|
return reply.status(403).send({
|
|
statusCode: 403,
|
|
error: 'Forbidden',
|
|
message: 'Cannot delete the default format profile',
|
|
});
|
|
}
|
|
|
|
await deleteFormatProfile(fastify.db, id);
|
|
return reply.status(204).send();
|
|
}
|
|
);
|
|
}
|