server implementation for poster generation
This commit is contained in:
283
server/movie_posters/lib/common.js
Normal file
283
server/movie_posters/lib/common.js
Normal file
@@ -0,0 +1,283 @@
|
||||
import axios from "axios";
|
||||
import shuffleArray from "shuffle-array";
|
||||
import {createCanvas, loadImage} from "canvas";
|
||||
|
||||
const TMDB_API_BASE_URL = "https://api.themoviedb.org/3";
|
||||
const TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/original";
|
||||
|
||||
/**
|
||||
* Fetches trending movie poster paths from TMDB.
|
||||
* @param {string} apiKey - The TMDB API key.
|
||||
* @param {string} language - The language for the movie results (e.g., 'de-DE').
|
||||
* @returns {Promise<string[]>} A promise that resolves to an array of poster image URLs.
|
||||
* @throws {Error} If the API request fails or no results are found.
|
||||
*/
|
||||
async function fetchTrendingMoviePosters(apiKey, language) {
|
||||
const url = `${TMDB_API_BASE_URL}/trending/movie/week?language=${language}`;
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Movie Posters Service (v1.0.0)'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.data || !response.data.results || response.data.results.length === 0) {
|
||||
throw new Error("No trending movies found from TMDB.");
|
||||
}
|
||||
|
||||
const posterUrls = response.data.results
|
||||
.filter(item => item.poster_path)
|
||||
.map(item => `${TMDB_IMAGE_BASE_URL}${item.poster_path}`);
|
||||
|
||||
if (posterUrls.length === 0) {
|
||||
throw new Error("No valid poster paths found in the trending movies results.");
|
||||
}
|
||||
|
||||
return posterUrls;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching data from TMDB:", error.message);
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
console.error("TMDB API Response Status:", error.response.status);
|
||||
console.error("TMDB API Response Data:", error.response.data);
|
||||
throw new Error(`Failed to fetch trending movies from TMDB: ${error.response.statusText || error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen RGBA8888-Pixelbuffer in einen RGB565 Little-Endian Buffer.
|
||||
* @param {Uint8ClampedArray} rgbaBuffer - Der Eingabebuffer (R, G, B, A, R, G, B, A, ...)
|
||||
* @param {number} width - Bildbreite
|
||||
* @param {number} height - Bildhöhe
|
||||
* @returns {Buffer} Ein Buffer mit RGB565-Daten (2 Bytes pro Pixel, Little Endian).
|
||||
*/
|
||||
function convertRgbaToRgb565(rgbaBuffer, width, height) {
|
||||
const pixelCount = width * height;
|
||||
const rgb565Buffer = Buffer.alloc(pixelCount * 2);
|
||||
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
const r = rgbaBuffer[i * 4 + 0];
|
||||
const g = rgbaBuffer[i * 4 + 1];
|
||||
const b = rgbaBuffer[i * 4 + 2];
|
||||
|
||||
const r5 = (r >> 3) & 0x1F;
|
||||
const g6 = (g >> 2) & 0x3F;
|
||||
const b5 = (b >> 3) & 0x1F;
|
||||
|
||||
const rgb565Value = (r5 << 11) | (g6 << 5) | b5;
|
||||
|
||||
rgb565Buffer.writeUInt16LE(rgb565Value, i * 2);
|
||||
}
|
||||
|
||||
return rgb565Buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws images horizontally onto the canvas, scaling and centering them.
|
||||
* @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
|
||||
* @param {string[]} imageUrls - Array of image URLs to draw.
|
||||
* @param {number} count - The number of images to draw.
|
||||
* @param {number} canvasWidth - The width of the canvas.
|
||||
* @param {number} canvasHeight - The height of the canvas.
|
||||
*/
|
||||
async function drawHorizontalPosters(ctx, imageUrls, count, canvasWidth, canvasHeight) {
|
||||
const imageWidth = canvasWidth / count;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!imageUrls[i]) continue;
|
||||
|
||||
try {
|
||||
const image = await loadImage(imageUrls[i]);
|
||||
const scaledHeight = image.height * (imageWidth / image.width);
|
||||
const yPos = (canvasHeight - scaledHeight) / 2;
|
||||
ctx.drawImage(image, imageWidth * i, yPos, imageWidth, scaledHeight);
|
||||
} catch (error) {
|
||||
console.error(`Error loading or drawing image ${i} (${imageUrls[i]}):`, error.message);
|
||||
ctx.fillStyle = 'grey';
|
||||
ctx.fillRect(imageWidth * i, 0, imageWidth, canvasHeight);
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('Error', imageWidth * i + imageWidth / 2, canvasHeight / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws images vertically onto the canvas, scaling and centering them.
|
||||
* @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
|
||||
* @param {string[]} imageUrls - Array of image URLs to draw.
|
||||
* @param {number} count - The number of images to draw.
|
||||
* @param {number} canvasWidth - The width of the canvas.
|
||||
* @param {number} canvasHeight - The height of the canvas.
|
||||
*/
|
||||
async function drawVerticalPosters(ctx, imageUrls, count, canvasWidth, canvasHeight) {
|
||||
const imageHeight = canvasHeight / count;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (!imageUrls[i]) continue;
|
||||
|
||||
try {
|
||||
const image = await loadImage(imageUrls[i]);
|
||||
const scaledWidth = image.width * (imageHeight / image.height);
|
||||
const xPos = (canvasWidth - scaledWidth) / 2;
|
||||
ctx.drawImage(image, xPos, imageHeight * i, scaledWidth, imageHeight);
|
||||
} catch (error) {
|
||||
console.error(`Error loading or drawing image ${i} (${imageUrls[i]}):`, error.message);
|
||||
ctx.fillStyle = 'grey';
|
||||
ctx.fillRect(0, imageHeight * i, canvasWidth, imageHeight);
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('Error', canvasWidth / 2, imageHeight * i + imageHeight / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the final output buffer or JSON based on requested format.
|
||||
* @param {Canvas} canvas - The canvas object.
|
||||
* @param {CanvasRenderingContext2D} ctx - The canvas rendering context.
|
||||
* @param {'png' | 'jpeg'} imageFormat - The desired image format ('png' or 'jpeg').
|
||||
* @param {'image' | 'lvgl' | 'lvgl_binary'} outputType - The desired output type ('image' or 'lvgl').
|
||||
* @returns {{contentType: string, data: Buffer | string}} An object containing the content type and the output data (Buffer for image, string for LVGL JSON).
|
||||
* @throws {Error} If an invalid format or output type is provided.
|
||||
*/
|
||||
function generateOutput(canvas, ctx, imageFormat, outputType) {
|
||||
switch (outputType) {
|
||||
case 'lvgl_binary': {
|
||||
const rawRgbaData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const rgb565Buffer = convertRgbaToRgb565(rawRgbaData, canvas.width, canvas.height);
|
||||
|
||||
return {
|
||||
contentType: 'application/octet-stream',
|
||||
contentDisposition: 'attachment; filename="image.bin"',
|
||||
data: rgb565Buffer
|
||||
}
|
||||
}
|
||||
|
||||
case 'lvgl': {
|
||||
const rawRgbaData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const rgb565Buffer = convertRgbaToRgb565(rawRgbaData, canvas.width, canvas.height);
|
||||
|
||||
const lvglColorFormat = 'LV_COLOR_FORMAT_RGB565';
|
||||
const bytesPerPixel = 2;
|
||||
|
||||
const dataSize = canvas.width * canvas.height * bytesPerPixel;
|
||||
let cArrayString = `const uint8_t image_data_map[${dataSize}] = {`;
|
||||
|
||||
for (let i = 0; i < rgb565Buffer.length; i++) {
|
||||
cArrayString += '0x' + rgb565Buffer[i].toString(16).padStart(2, '0').toUpperCase();
|
||||
|
||||
if (i < rgb565Buffer.length - 1) {
|
||||
cArrayString += ', ';
|
||||
}
|
||||
}
|
||||
cArrayString += '};';
|
||||
|
||||
const result = {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
colorFormat: lvglColorFormat,
|
||||
pixelFormat: 'RGB565',
|
||||
data_size: dataSize,
|
||||
c_array: cArrayString
|
||||
};
|
||||
|
||||
return {
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(result, null, 2)
|
||||
};
|
||||
}
|
||||
|
||||
case 'image': {
|
||||
let buffer;
|
||||
let contentType;
|
||||
switch (imageFormat) {
|
||||
case 'jpeg':
|
||||
buffer = canvas.toBuffer('image/jpeg');
|
||||
contentType = 'image/jpeg';
|
||||
break;
|
||||
case 'png':
|
||||
buffer = canvas.toBuffer('image/png');
|
||||
contentType = 'image/png';
|
||||
break;
|
||||
default:
|
||||
console.warn(`Invalid image format requested: ${imageFormat}. Defaulting to PNG.`);
|
||||
buffer = canvas.toBuffer('image/png');
|
||||
contentType = 'image/png';
|
||||
}
|
||||
return {contentType, data: buffer};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid output type specified: ${outputType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a composite poster image from trending movie posters.
|
||||
*
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {number} width - Desired width of the final poster.
|
||||
* @param {number} height - Desired height of the final poster.
|
||||
* @param {number} count - Number of movie posters to include.
|
||||
* @param {'horizontal' | 'vertical'} orientation - Layout orientation of the posters.
|
||||
* @param {boolean} shuffleImages - Whether to shuffle the fetched posters.
|
||||
* @param {string} backgroundColor - Background color for the canvas (hex code).
|
||||
* @param {'png' | 'jpeg'} format - Output image format (if output is 'image').
|
||||
* @param {string} language - Language for fetching movie posters (e.g., 'de-DE').
|
||||
* @param {'image' | 'lvgl'} output - Desired output type.
|
||||
*/
|
||||
export async function createPoster(req, res, width, height, count, orientation, shuffleImages, backgroundColor, format, language, output) {
|
||||
const apiKey = process.env.TMDB_API_KEY || req.header("x-api-key");
|
||||
if (!apiKey) {
|
||||
return res.status(401).send({error: "API key is missing. Provide it via x-api-key header or TMDB_API_KEY environment variable."});
|
||||
}
|
||||
|
||||
try {
|
||||
let imageUrls = await fetchTrendingMoviePosters(apiKey, language);
|
||||
|
||||
if (imageUrls.length < count) {
|
||||
console.warn(`Warning: Fetched ${imageUrls.length} posters, but ${count} were requested. Repeating posters.`);
|
||||
const originalUrls = [...imageUrls];
|
||||
while (imageUrls.length < count) {
|
||||
imageUrls.push(...originalUrls.slice(0, count - imageUrls.length));
|
||||
}
|
||||
}
|
||||
|
||||
if (shuffleImages) {
|
||||
shuffleArray(imageUrls);
|
||||
}
|
||||
|
||||
const selectedImageUrls = imageUrls.slice(0, count);
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d', {pixelFormat: 'RGB24'});
|
||||
|
||||
ctx.fillStyle = backgroundColor;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (orientation === 'horizontal') {
|
||||
await drawHorizontalPosters(ctx, selectedImageUrls, count, canvas.width, canvas.height);
|
||||
} else if (orientation === 'vertical') {
|
||||
await drawVerticalPosters(ctx, selectedImageUrls, count, canvas.width, canvas.height);
|
||||
} else {
|
||||
throw new Error(`Invalid orientation specified: ${orientation}`);
|
||||
}
|
||||
|
||||
const {contentType, data, contentDisposition} = generateOutput(canvas, ctx, format, output);
|
||||
if (contentDisposition) {
|
||||
res.setHeader('Content-Disposition', contentDisposition);
|
||||
}
|
||||
res.status(200).type(contentType).end(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating poster:", error.message);
|
||||
res.status(500).send({error: `Failed to create poster: ${error.message}`});
|
||||
}
|
||||
}
|
82
server/movie_posters/lib/index.js
Normal file
82
server/movie_posters/lib/index.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { poster } from './poster.js';
|
||||
import { start as startProfiler } from '@google-cloud/profiler'; // Renamed import for clarity
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
|
||||
// --- Configuration ---
|
||||
const PORT = process.env.PORT || 3000;
|
||||
// Enable profiler, e.g., only in production environments
|
||||
const PROFILER_ENABLED = process.env.ENABLE_PROFILER === 'true' || process.env.NODE_ENV === 'production';
|
||||
const SERVICE_NAME = 'movie_posters';
|
||||
const SERVICE_VERSION = '1.0.0'; // Consider reading from package.json
|
||||
|
||||
// --- Application Setup ---
|
||||
const app = express();
|
||||
|
||||
// --- Google Cloud Profiler Setup ---
|
||||
if (PROFILER_ENABLED) {
|
||||
startProfiler({
|
||||
projectId: process.env.PROJECT_ID, // Ensure PROJECT_ID is set in the environment
|
||||
serviceContext: {
|
||||
service: SERVICE_NAME,
|
||||
version: SERVICE_VERSION,
|
||||
},
|
||||
}).then(() => {
|
||||
console.log(`Google Cloud Profiler started for service ${SERVICE_NAME} v${SERVICE_VERSION}.`);
|
||||
}).catch(err => {
|
||||
// Log the error but don't prevent the app from starting
|
||||
console.error('Failed to start Google Cloud Profiler:', err);
|
||||
});
|
||||
} else {
|
||||
console.log('Google Cloud Profiler is disabled.');
|
||||
}
|
||||
|
||||
// --- Core Middleware ---
|
||||
app.use(cors()); // Enable Cross-Origin Resource Sharing for all origins
|
||||
app.use(express.json()); // Parse incoming JSON request bodies
|
||||
|
||||
// --- Specific Error Handling Middleware ---
|
||||
// Handles JSON parsing errors specifically, providing a clearer error message.
|
||||
app.use((err, req, res, next) => {
|
||||
// Check if the error is a syntax error thrown by express.json()
|
||||
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
|
||||
console.error('Bad JSON format received:', err.message);
|
||||
return res.status(400).send({ error: 'Invalid JSON format in request body.' });
|
||||
}
|
||||
// Pass other errors down the middleware chain
|
||||
next(err);
|
||||
});
|
||||
|
||||
app.use('/poster', poster.router);
|
||||
|
||||
// --- Routes ---
|
||||
|
||||
// Root GET endpoint: Provides basic API status information.
|
||||
app.get("/", (req, res) => {
|
||||
res.status(200).send({
|
||||
status: 'API is running.',
|
||||
message: 'Use the POST /poster endpoint with a JSON body to generate posters.',
|
||||
service: SERVICE_NAME,
|
||||
version: SERVICE_VERSION
|
||||
});
|
||||
});
|
||||
|
||||
// --- Generic Error Handler ---
|
||||
// Catches any errors passed via `next(err)` from preceding middleware or routes.
|
||||
// This should be the last middleware added.
|
||||
app.use((err, req, res, next) => {
|
||||
console.error("Unhandled application error:", err);
|
||||
// Ensure response is sent only once
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send({ error: 'An internal server error occurred.' });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Server Start ---
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server listening on port ${PORT}`);
|
||||
console.log(`Access the API at http://localhost:${PORT}`);
|
||||
if (PROFILER_ENABLED) {
|
||||
console.log(`Google Cloud Profiler is active for project: ${process.env.PROJECT_ID || 'Not Specified'}`);
|
||||
}
|
||||
});
|
152
server/movie_posters/lib/poster.js
Normal file
152
server/movie_posters/lib/poster.js
Normal file
@@ -0,0 +1,152 @@
|
||||
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
|
||||
};
|
Reference in New Issue
Block a user