Files
cinema-display/server/movie_posters/lib/poster.js
2025-10-09 22:53:49 +02:00

152 lines
6.4 KiB
JavaScript

import express from "express";
import { createPoster } from "./common.js";
import Joi from 'joi';
// Define default parameters for the legacy /poster endpoint
const DEFAULT_WIDTH = 480;
const DEFAULT_HEIGHT = 320;
const DEFAULT_COUNT = 4;
const DEFAULT_ORIENTATION = "horizontal";
const DEFAULT_SHUFFLE = true;
const DEFAULT_BACKGROUND_COLOR = "#000000"; // Consistent 6-digit hex
const DEFAULT_FORMAT = 'png'; // Default format consistent with POST /
const DEFAULT_LANGUAGE = 'de-DE'; // Default language consistent with POST /
const DEFAULT_OUTPUT = 'image'; // Default output consistent with POST /
const router = express.Router();
// --- Validation Schema for POST / ---
// Defines the expected structure and constraints for the request body.
const posterRequestSchema = Joi.object({
// Dimensions and Count
width: Joi.number().integer().min(1).max(4000).default(DEFAULT_WIDTH)
.messages({'number.base': "'width' must be a number", 'number.min': "'width' must be at least 1", 'number.max': "'width' cannot exceed 4000"}),
height: Joi.number().integer().min(1).max(4000).default(DEFAULT_HEIGHT)
.messages({'number.base': "'height' must be a number", 'number.min': "'height' must be at least 1", 'number.max': "'height' cannot exceed 4000"}),
count: Joi.number().integer().min(1).max(20).default(DEFAULT_COUNT)
.messages({'number.base': "'count' must be a number", 'number.min': "'count' must be at least 1", 'number.max': "'count' cannot exceed 20"}),
// Layout and Content
orientation: Joi.string().required().valid('horizontal', 'vertical').default(DEFAULT_ORIENTATION)
.messages({'any.required': "'orientation' is required", 'any.only': "'orientation' must be either 'horizontal' or 'vertical'"}),
shuffle: Joi.boolean().default(DEFAULT_SHUFFLE)
.messages({'boolean.base': "'shuffle' must be a boolean"}),
language: Joi.string().trim().default(DEFAULT_LANGUAGE) // Added trim()
.messages({'string.base': "'language' must be a string"}),
// Styling and Output
backgroundColor: Joi.string().trim() // Added trim()
.default(DEFAULT_BACKGROUND_COLOR) // Use 6-digit hex for consistency
.regex(/^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/) // Allow 3 or 6 hex digits
.message("The 'backgroundColor' must be a valid hexadecimal color code (e.g., #000 or #FF0000)"),
format: Joi.string().valid('png', 'jpeg').default(DEFAULT_FORMAT)
.messages({'any.only': "'format' must be either 'png' or 'jpeg'"}),
output: Joi.string().valid('image', 'lvgl', 'lvgl_binary').default(DEFAULT_OUTPUT)
.messages({'any.only': "'output' must be either 'image', 'lvgl' or 'lvgl_binary'"}),
});
// --- Validation Middleware ---
// Validates the request body against the posterRequestSchema.
const validatePosterRequest = (req, res, next) => {
const { error, value } = posterRequestSchema.validate(req.body, {
abortEarly: false, // Report all errors, not just the first one
stripUnknown: true // Remove unknown keys from the validated value
});
if (error) {
// Log the detailed validation error for debugging
console.warn('Validation failed:', error.details);
// Construct a user-friendly error message
const errorMessages = error.details.map(detail => detail.message).join('. ');
return res.status(400).send({ error: `Invalid request body: ${errorMessages}` });
}
// Replace req.body with the validated, defaulted, and potentially stripped value.
// This is a common pattern in Express middleware.
req.body = value;
next(); // Proceed to the main route handler
};
// --- Main POST Endpoint ---
// Handles requests to generate the poster image or LVGL data.
router.post("/", validatePosterRequest, async (req, res) => {
// Destructure the validated and defaulted body for clarity
const {
width,
height,
count,
orientation,
shuffle,
backgroundColor,
format,
language,
output
} = req.body;
try {
// Delegate the core poster creation logic to the common function.
// createPoster handles fetching data, drawing, and sending the response.
await createPoster(
req, // Pass req for API key access within createPoster
res,
width,
height,
count,
orientation,
shuffle,
backgroundColor,
format,
language,
output
);
} catch (error) {
// This catch block acts as a final safety net.
// Although createPoster has internal error handling, this catches
// potential unexpected issues during its execution.
console.error("Unhandled error during POST / handler:", error);
// Ensure response is sent only once
if (!res.headersSent) {
res.status(500).send({ error: "An unexpected error occurred while processing your request." });
}
}
});
/**
* @deprecated Use the POST /poster endpoint instead for more flexibility.
* GET /poster: Generates a movie poster collage using predefined default settings.
* This endpoint is maintained for backward compatibility.
*/
router.get("/", async (req, res) => {
// Call the common poster creation function with hardcoded legacy defaults.
// Note: This endpoint does not support customization via query parameters.
try {
await createPoster(
req, // Pass req for API key access within createPoster
res,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
DEFAULT_COUNT,
DEFAULT_ORIENTATION,
DEFAULT_SHUFFLE,
DEFAULT_BACKGROUND_COLOR,
DEFAULT_FORMAT,
DEFAULT_LANGUAGE,
DEFAULT_OUTPUT
);
// createPoster handles sending the response on success.
} catch (error) {
// Although createPoster has internal error handling and sends responses,
// catch potential unexpected errors during the await or within createPoster
// if they somehow bypass its internal handling.
console.error("Error in legacy /cinema endpoint:", error);
// Ensure a response is sent if headers haven't been sent yet.
if (!res.headersSent) {
res.status(500).send({ error: "Failed to generate cinema poster due to an internal error." });
}
}
});
// Export the router under the 'cinema' namespace
export const poster = {
router, // Use shorthand property name
};