Files
MoonTVPlus/src/app/api/tmdb-recommendations/route.ts
2026-01-17 18:04:58 +08:00

197 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from 'next/server';
import {
searchTMDBMulti,
getTMDBMovieRecommendations,
getTMDBTVRecommendations,
getTMDBImageUrl,
} from '@/lib/tmdb.client';
import { getConfig } from '@/lib/config';
// 服务器端缓存1天
const searchCache = new Map<string, { data: any; timestamp: number }>();
const CACHE_TTL = 24 * 60 * 60 * 1000; // 1天
// 移除季度信息的辅助函数
function removeSeasonInfo(title: string): string {
// 移除 "第一季"、"第1季"、"第一1季" 等格式
return title
.replace(/第[一二三四五六七八九十\d]+[(]\d+[)][季部]/g, '')
.replace(/第[一二三四五六七八九十\d]+[季部]/g, '')
.replace(/[(]\d+[)]/g, '')
.replace(/\s+season\s+\d+/gi, '')
.replace(/\s+S\d+/gi, '')
.trim();
}
// 精确匹配标题
function findExactMatch(results: any[], originalTitle: string): any | null {
if (!results || results.length === 0) return null;
// 如果只有一个结果,直接返回
if (results.length === 1) return results[0];
const cleanedTitle = removeSeasonInfo(originalTitle).toLowerCase();
// 寻找完全匹配的结果
for (const result of results) {
const resultTitle = (result.title || result.name || '').toLowerCase();
const resultOriginalTitle = (result.original_title || result.original_name || '').toLowerCase();
if (resultTitle === cleanedTitle || resultOriginalTitle === cleanedTitle) {
return result;
}
}
// 如果没有完全匹配,返回第一个
return results[0];
}
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const title = searchParams.get('title');
const cachedId = searchParams.get('cachedId'); // 浏览器缓存的ID
if (!title && !cachedId) {
return NextResponse.json(
{ error: '缺少必要参数' },
{ status: 400 }
);
}
const config = await getConfig();
const tmdbApiKey = config.SiteConfig.TMDBApiKey;
const tmdbProxy = config.SiteConfig.TMDBProxy;
const tmdbReverseProxy = config.SiteConfig.TMDBReverseProxy;
if (!tmdbApiKey) {
return NextResponse.json(
{ error: 'TMDB API Key 未配置' },
{ status: 500 }
);
}
let tmdbId: number;
let mediaType: 'movie' | 'tv';
// 如果有缓存的ID直接使用
if (cachedId) {
const [type, id] = cachedId.split(':');
mediaType = type as 'movie' | 'tv';
tmdbId = parseInt(id);
} else {
// 否则搜索
const cleanedTitle = removeSeasonInfo(title!);
const cacheKey = `search:${cleanedTitle}`;
// 检查服务器缓存
const cached = searchCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
tmdbId = cached.data.tmdbId;
mediaType = cached.data.mediaType;
} else {
// 搜索TMDB
const searchResult = await searchTMDBMulti(tmdbApiKey, cleanedTitle, tmdbProxy, tmdbReverseProxy);
if (searchResult.code !== 200 || !searchResult.results.length) {
return NextResponse.json(
{ recommendations: [], tmdbId: null, mediaType: null },
{
status: 200,
headers: {
'Cache-Control': 'public, max-age=86400', // 浏览器缓存1天
},
}
);
}
// 过滤出电影和电视剧
const validResults = searchResult.results.filter(
(r: any) => r.media_type === 'movie' || r.media_type === 'tv'
);
// 精确匹配
const matched = findExactMatch(validResults, title!);
if (!matched) {
return NextResponse.json(
{ recommendations: [], tmdbId: null, mediaType: null },
{
status: 200,
headers: {
'Cache-Control': 'public, max-age=86400',
},
}
);
}
tmdbId = matched.id;
mediaType = matched.media_type;
// 保存到服务器缓存
searchCache.set(cacheKey, {
data: { tmdbId, mediaType },
timestamp: Date.now(),
});
// 清理过期缓存
Array.from(searchCache.entries()).forEach(([key, value]) => {
if (Date.now() - value.timestamp > CACHE_TTL) {
searchCache.delete(key);
}
});
}
}
// 获取推荐
const recommendationsResult =
mediaType === 'movie'
? await getTMDBMovieRecommendations(tmdbApiKey, tmdbId, tmdbProxy, tmdbReverseProxy)
: await getTMDBTVRecommendations(tmdbApiKey, tmdbId, tmdbProxy, tmdbReverseProxy);
if (recommendationsResult.code !== 200) {
return NextResponse.json(
{ recommendations: [], tmdbId: `${mediaType}:${tmdbId}`, mediaType },
{
status: 200,
headers: {
'Cache-Control': 'public, max-age=86400',
},
}
);
}
// 转换为统一格式
const recommendations = (recommendationsResult.results as any[])
.filter((r: any) => r.poster_path) // 只保留有海报的
.slice(0, 20) // 最多20个
.map((r: any) => ({
tmdbId: r.id,
title: r.title || r.name,
poster: getTMDBImageUrl(r.poster_path, 'w342'),
rating: r.vote_average ? r.vote_average.toFixed(1) : '',
mediaType,
}));
return NextResponse.json(
{
recommendations,
tmdbId: `${mediaType}:${tmdbId}`, // 返回给浏览器用于缓存
mediaType,
},
{
status: 200,
headers: {
'Cache-Control': 'public, max-age=86400', // 浏览器缓存1天
},
}
);
} catch (error) {
console.error('获取 TMDB 推荐失败:', error);
return NextResponse.json(
{ error: '获取推荐失败' },
{ status: 500 }
);
}
}