223 lines
5.8 KiB
TypeScript
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;
|