import { Router, Request, Response } from 'express'; import { z } from 'zod'; import { authMiddleware } from '../middleware/auth.middleware.js'; import { scrapeRateLimiter } from '../middleware/rateLimit.middleware.js'; import { TmdbService } from '../services/tmdb.service.js'; import type { ApiResponse, TmdbSearchResponse, } from '../types/index.js'; const router = Router(); /** * Validation schema for TMDB search */ const tmdbSearchSchema = z.object({ query: z.string().trim().min(1, 'Query must be at least 1 character').max(200, 'Query must be at most 200 characters'), year: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), type: z.enum(['movie', 'tv', 'multi']).optional(), seasonYear: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), seasonNumber: z.coerce.number().int().min(1).max(100).optional(), }); /** * POST /api/tmdb/search * Search for movies and TV shows using TMDB API * * Request body: { query: string, year?: number, type?: 'movie' | 'tv' | 'multi' } * Headers: X-API-Key: * * Response: { success: boolean, data?: TmdbSearchResponse, error?: ApiError } */ router.post( '/search', authMiddleware, scrapeRateLimiter, async ( req: Request, res: Response> ) => { // Validate request body const result = tmdbSearchSchema.safeParse(req.body); if (!result.success) { const errors = result.error.issues.map((issue) => ({ field: issue.path.join('.'), message: issue.message, })); const response: ApiResponse = { success: false, error: { code: 'VALIDATION_ERROR', message: 'Invalid request parameters', details: { errors }, }, }; res.status(400).json(response); return; } const { query, year, type, seasonYear, seasonNumber } = result.data; try { const searchResult = await TmdbService.search({ query, year, type: type || 'multi', seasonYear, seasonNumber, }); const response: ApiResponse = { success: true, data: searchResult, }; res.json(response); } catch (error) { const response: ApiResponse = { success: false, error: { code: 'TMDB_ERROR', message: error instanceof Error ? error.message : 'Failed to search TMDB', }, }; res.status(500).json(response); } } ); /** * POST /api/tmdb/search/movie * Search for movies only */ router.post( '/search/movie', authMiddleware, scrapeRateLimiter, async ( req: Request, res: Response> ) => { const movieSearchSchema = z.object({ query: z.string().trim().min(1).max(200), year: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), }); const result = movieSearchSchema.safeParse(req.body); if (!result.success) { const errors = result.error.issues.map((issue) => ({ field: issue.path.join('.'), message: issue.message, })); const response: ApiResponse = { success: false, error: { code: 'VALIDATION_ERROR', message: 'Invalid request parameters', details: { errors }, }, }; res.status(400).json(response); return; } const { query, year } = result.data; try { const searchResult = await TmdbService.searchMovies(query, year); const response: ApiResponse = { success: true, data: searchResult, }; res.json(response); } catch (error) { const response: ApiResponse = { success: false, error: { code: 'TMDB_ERROR', message: error instanceof Error ? error.message : 'Failed to search movies', }, }; res.status(500).json(response); } } ); /** * POST /api/tmdb/search/tv * Search for TV shows only */ router.post( '/search/tv', authMiddleware, scrapeRateLimiter, async ( req: Request, res: Response> ) => { const tvSearchSchema = z.object({ query: z.string().trim().min(1).max(200), year: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), seasonYear: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), seasonNumber: z.coerce.number().int().min(1).max(100).optional(), }); const result = tvSearchSchema.safeParse(req.body); if (!result.success) { const errors = result.error.issues.map((issue) => ({ field: issue.path.join('.'), message: issue.message, })); const response: ApiResponse = { success: false, error: { code: 'VALIDATION_ERROR', message: 'Invalid request parameters', details: { errors }, }, }; res.status(400).json(response); return; } const { query, year, seasonYear, seasonNumber } = result.data; try { const searchResult = await TmdbService.searchTv(query, year, seasonNumber, seasonYear); const response: ApiResponse = { success: true, data: searchResult, }; res.json(response); } catch (error) { const response: ApiResponse = { success: false, error: { code: 'TMDB_ERROR', message: error instanceof Error ? error.message : 'Failed to search TV shows', }, }; res.status(500).json(response); } } ); export default router;