Files
ratebubble/src/routes/tmdb.routes.ts
2026-02-28 02:44:41 +03:00

223 lines
5.8 KiB
TypeScript

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: <api_key>
*
* Response: { success: boolean, data?: TmdbSearchResponse, error?: ApiError }
*/
router.post(
'/search',
authMiddleware,
scrapeRateLimiter,
async (
req: Request,
res: Response<ApiResponse<TmdbSearchResponse>>
) => {
// 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<TmdbSearchResponse> = {
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<TmdbSearchResponse> = {
success: true,
data: searchResult,
};
res.json(response);
} catch (error) {
const response: ApiResponse<TmdbSearchResponse> = {
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<ApiResponse<TmdbSearchResponse>>
) => {
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<TmdbSearchResponse> = {
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<TmdbSearchResponse> = {
success: true,
data: searchResult,
};
res.json(response);
} catch (error) {
const response: ApiResponse<TmdbSearchResponse> = {
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<ApiResponse<TmdbSearchResponse>>
) => {
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<TmdbSearchResponse> = {
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<TmdbSearchResponse> = {
success: true,
data: searchResult,
};
res.json(response);
} catch (error) {
const response: ApiResponse<TmdbSearchResponse> = {
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;