From 76b3349aa2994c2ff979b2464895e116147f3f72 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 25 Dec 2025 23:34:54 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=81=E4=BA=BA=E5=BD=B1=E5=BA=93=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E5=A2=9E=E5=8A=A0=E5=AD=A3=E5=BA=A6=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG | 2 +- src/app/admin/page.tsx | 12 ++ src/app/api/openlist/correct/route.ts | 15 +- src/app/api/openlist/list/route.ts | 36 +++-- src/app/api/openlist/refresh/route.ts | 62 ++++++- src/app/api/tmdb/seasons/route.ts | 65 ++++++++ src/components/CorrectDialog.tsx | 222 ++++++++++++++++++++++++-- src/components/VideoCard.tsx | 23 +++ src/lib/openlist-cache.ts | 2 + src/lib/season-parser.ts | 171 ++++++++++++++++++++ src/lib/tmdb.search.ts | 123 ++++++++++++++ 11 files changed, 699 insertions(+), 34 deletions(-) create mode 100644 src/app/api/tmdb/seasons/route.ts create mode 100644 src/lib/season-parser.ts diff --git a/CHANGELOG b/CHANGELOG index 3a89e19..92df6df 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,7 @@ ## [204.0.0] - 2025-12-25 ### Added -- ⚠️⚠️⚠️更新此版本前前务必进行备份!!!⚠️⚠️⚠️ +- ⚠️⚠️⚠️更新此版本前务必进行备份!!!⚠️⚠️⚠️ - 新增私人影视库功能(实验性) - 增加弹幕热力图 - 增加盘搜搜索资源 diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 0cde3f3..a8e08fc 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -3226,6 +3226,9 @@ const OpenListConfigComponent = ({ 类型 + + 季度 + 年份 @@ -3257,6 +3260,15 @@ const OpenListConfigComponent = ({ {video.mediaType === 'movie' ? '电影' : '剧集'} + + {video.seasonNumber ? ( + + S{video.seasonNumber} + + ) : ( + '-' + )} + {video.releaseDate ? video.releaseDate.split('-')[0] : '-'} diff --git a/src/app/api/openlist/correct/route.ts b/src/app/api/openlist/correct/route.ts index beb6e95..4982377 100644 --- a/src/app/api/openlist/correct/route.ts +++ b/src/app/api/openlist/correct/route.ts @@ -27,7 +27,18 @@ export async function POST(request: NextRequest) { } const body = await request.json(); - const { folder, tmdbId, title, posterPath, releaseDate, overview, voteAverage, mediaType } = body; + const { + folder, + tmdbId, + title, + posterPath, + releaseDate, + overview, + voteAverage, + mediaType, + seasonNumber, + seasonName, + } = body; if (!folder || !tmdbId) { return NextResponse.json( @@ -97,6 +108,8 @@ export async function POST(request: NextRequest) { media_type: mediaType, last_updated: Date.now(), failed: false, // 纠错后标记为成功 + season_number: seasonNumber, // 季度编号(可选) + season_name: seasonName, // 季度名称(可选) }; // 保存 metainfo 到数据库 diff --git a/src/app/api/openlist/list/route.ts b/src/app/api/openlist/list/route.ts index ae32a3a..b064df4 100644 --- a/src/app/api/openlist/list/route.ts +++ b/src/app/api/openlist/list/route.ts @@ -125,19 +125,29 @@ export async function GET(request: NextRequest) { const allVideos = Object.entries(metaInfo.folders) .filter(([, info]) => includeFailed || !info.failed) // 根据参数过滤失败的视频 .map( - ([folderName, info]) => ({ - id: folderName, - folder: folderName, - tmdbId: info.tmdb_id, - title: info.title, - poster: getTMDBImageUrl(info.poster_path), - releaseDate: info.release_date, - overview: info.overview, - voteAverage: info.vote_average, - mediaType: info.media_type, - lastUpdated: info.last_updated, - failed: info.failed || false, - }) + ([folderName, info]) => { + // 构建 id:如果是第二季及以后,id 也要包含季度信息 + let videoId = folderName; + if (info.season_number && info.season_number > 1 && info.season_name) { + videoId = `${folderName} ${info.season_name}`; + } + + return { + id: videoId, + folder: folderName, + tmdbId: info.tmdb_id, + title: info.title, + poster: getTMDBImageUrl(info.poster_path), + releaseDate: info.release_date, + overview: info.overview, + voteAverage: info.vote_average, + mediaType: info.media_type, + lastUpdated: info.last_updated, + failed: info.failed || false, + seasonNumber: info.season_number, + seasonName: info.season_name, + }; + } ); // 按更新时间倒序排序 diff --git a/src/app/api/openlist/refresh/route.ts b/src/app/api/openlist/refresh/route.ts index d815371..c939b1d 100644 --- a/src/app/api/openlist/refresh/route.ts +++ b/src/app/api/openlist/refresh/route.ts @@ -19,7 +19,8 @@ import { failScanTask, updateScanTaskProgress, } from '@/lib/scan-task'; -import { searchTMDB } from '@/lib/tmdb.search'; +import { parseSeasonFromTitle } from '@/lib/season-parser'; +import { searchTMDB, getTVSeasonDetails } from '@/lib/tmdb.search'; export const runtime = 'nodejs'; @@ -200,17 +201,25 @@ async function performScan( } try { - // 搜索 TMDB + // 解析文件夹名称,提取季度信息 + const seasonInfo = parseSeasonFromTitle(folder.name); + const searchQuery = seasonInfo.cleanTitle || folder.name; + + console.log(`[OpenList Refresh] 处理文件夹: ${folder.name}`); + console.log(`[OpenList Refresh] 清理后标题: ${searchQuery}, 季度: ${seasonInfo.seasonNumber}`); + + // 搜索 TMDB(使用清理后的标题) const searchResult = await searchTMDB( tmdbApiKey, - folder.name, + searchQuery, tmdbProxy ); if (searchResult.code === 200 && searchResult.result) { const result = searchResult.result; - metaInfo.folders[folder.name] = { + // 基础信息 + const folderInfo: any = { tmdb_id: result.id, title: result.title || result.name || folder.name, poster_path: result.poster_path, @@ -222,6 +231,51 @@ async function performScan( failed: false, }; + // 如果是电视剧且识别到季度编号,获取该季度的详细信息 + if (result.media_type === 'tv' && seasonInfo.seasonNumber) { + try { + const seasonDetails = await getTVSeasonDetails( + tmdbApiKey, + result.id, + seasonInfo.seasonNumber, + tmdbProxy + ); + + if (seasonDetails.code === 200 && seasonDetails.season) { + folderInfo.season_number = seasonDetails.season.season_number; + folderInfo.season_name = seasonDetails.season.name; + + // 如果是第二季及以后,替换标题和ID + if (seasonDetails.season.season_number > 1) { + folderInfo.title = `${folderInfo.title} ${seasonDetails.season.name}`; + folderInfo.tmdb_id = seasonDetails.season.id; // 使用季度的ID + } + + // 使用季度的海报(如果有) + if (seasonDetails.season.poster_path) { + folderInfo.poster_path = seasonDetails.season.poster_path; + } + // 使用季度的简介(如果有) + if (seasonDetails.season.overview) { + folderInfo.overview = seasonDetails.season.overview; + } + // 使用季度的首播日期(如果有) + if (seasonDetails.season.air_date) { + folderInfo.release_date = seasonDetails.season.air_date; + } + } else { + console.warn(`[OpenList Refresh] 获取季度 ${seasonInfo.seasonNumber} 详情失败`); + // 即使获取季度详情失败,也保存季度编号 + folderInfo.season_number = seasonInfo.seasonNumber; + } + } catch (error) { + console.error(`[OpenList Refresh] 获取季度详情异常:`, error); + // 即使出错,也保存季度编号 + folderInfo.season_number = seasonInfo.seasonNumber; + } + } + + metaInfo.folders[folder.name] = folderInfo; newCount++; } else { // 记录失败的文件夹 diff --git a/src/app/api/tmdb/seasons/route.ts b/src/app/api/tmdb/seasons/route.ts new file mode 100644 index 0000000..c2026c7 --- /dev/null +++ b/src/app/api/tmdb/seasons/route.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ + +import { NextRequest, NextResponse } from 'next/server'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { getConfig } from '@/lib/config'; +import { getTVSeasons } from '@/lib/tmdb.search'; + +export const runtime = 'nodejs'; + +/** + * GET /api/tmdb/seasons?tvId=xxx + * 获取电视剧的季度列表 + */ +export async function GET(request: NextRequest) { + try { + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: '未授权' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const tvIdStr = searchParams.get('tvId'); + + if (!tvIdStr) { + return NextResponse.json({ error: '缺少 tvId 参数' }, { status: 400 }); + } + + const tvId = parseInt(tvIdStr, 10); + if (isNaN(tvId)) { + return NextResponse.json({ error: 'tvId 必须是数字' }, { status: 400 }); + } + + const config = await getConfig(); + const tmdbApiKey = config.SiteConfig.TMDBApiKey; + const tmdbProxy = config.SiteConfig.TMDBProxy; + + if (!tmdbApiKey) { + return NextResponse.json( + { error: 'TMDB API Key 未配置' }, + { status: 400 } + ); + } + + const result = await getTVSeasons(tmdbApiKey, tvId, tmdbProxy); + + if (result.code === 200 && result.seasons) { + return NextResponse.json({ + success: true, + seasons: result.seasons, + }); + } else { + return NextResponse.json( + { error: '获取季度列表失败', code: result.code }, + { status: result.code } + ); + } + } catch (error) { + console.error('获取季度列表失败:', error); + return NextResponse.json( + { error: '获取失败', details: (error as Error).message }, + { status: 500 } + ); + } +} diff --git a/src/components/CorrectDialog.tsx b/src/components/CorrectDialog.tsx index 0c64e6a..dca470e 100644 --- a/src/components/CorrectDialog.tsx +++ b/src/components/CorrectDialog.tsx @@ -22,6 +22,16 @@ interface TMDBResult { media_type: 'movie' | 'tv'; } +interface TMDBSeason { + id: number; + name: string; + season_number: number; + episode_count: number; + air_date: string | null; + poster_path: string | null; + overview: string; +} + interface CorrectDialogProps { isOpen: boolean; onClose: () => void; @@ -43,11 +53,20 @@ export default function CorrectDialog({ const [error, setError] = useState(''); const [correcting, setCorrecting] = useState(false); + // 季度选择相关状态 + const [selectedResult, setSelectedResult] = useState(null); + const [seasons, setSeasons] = useState([]); + const [loadingSeasons, setLoadingSeasons] = useState(false); + const [showSeasonSelection, setShowSeasonSelection] = useState(false); + useEffect(() => { if (isOpen) { setSearchQuery(currentTitle); setResults([]); setError(''); + setSelectedResult(null); + setSeasons([]); + setShowSeasonSelection(false); } }, [isOpen, currentTitle]); @@ -60,6 +79,8 @@ export default function CorrectDialog({ setSearching(true); setError(''); setResults([]); + setShowSeasonSelection(false); + setSelectedResult(null); try { const response = await fetch( @@ -88,22 +109,99 @@ export default function CorrectDialog({ } }; - const handleCorrect = async (result: TMDBResult) => { + // 获取电视剧的季度列表 + const fetchSeasons = async (tvId: number) => { + setLoadingSeasons(true); + setError(''); + try { + const response = await fetch(`/api/tmdb/seasons?tvId=${tvId}`); + + if (!response.ok) { + throw new Error('获取季度列表失败'); + } + + const data = await response.json(); + + if (data.success && data.seasons) { + return data.seasons as TMDBSeason[]; + } else { + setError('获取季度列表失败'); + return []; + } + } catch (err) { + console.error('获取季度列表失败:', err); + setError('获取季度列表失败,请重试'); + return []; + } finally { + setLoadingSeasons(false); + } + }; + + // 处理选择结果(电影直接纠错,电视剧显示季度选择) + const handleSelectResult = async (result: TMDBResult) => { + if (result.media_type === 'tv') { + // 电视剧:先获取季度列表 + setSelectedResult(result); + const seasonsList = await fetchSeasons(result.id); + + if (seasonsList.length === 1) { + // 只有一季,直接使用该季度进行纠错 + await handleCorrect(result, seasonsList[0]); + } else if (seasonsList.length > 1) { + // 多季,显示选择界面 + setSeasons(seasonsList); + setShowSeasonSelection(true); + } else { + // 没有季度信息,直接使用剧集信息 + await handleCorrect(result); + } + } else { + // 电影:直接纠错 + await handleCorrect(result); + } + }; + + // 处理选择季度 + const handleSelectSeason = async (season: TMDBSeason) => { + if (!selectedResult) return; + + await handleCorrect(selectedResult, season); + }; + + // 执行纠错 + const handleCorrect = async (result: TMDBResult, season?: TMDBSeason) => { setCorrecting(true); try { + // 构建标题和ID:如果是第二季及以后,在标题后加上季度名称,并使用季度ID + let finalTitle = result.title || result.name; + let finalTmdbId = result.id; + + if (season && season.season_number > 1) { + finalTitle = `${finalTitle} ${season.name}`; + finalTmdbId = season.id; // 使用季度的ID + } + + const body: any = { + folder, + tmdbId: finalTmdbId, + title: finalTitle, + posterPath: season?.poster_path || result.poster_path, + releaseDate: season?.air_date || result.release_date || result.first_air_date, + overview: season?.overview || result.overview, + voteAverage: result.vote_average, + mediaType: result.media_type, + }; + + // 如果有季度信息,添加到请求中 + if (season) { + body.seasonNumber = season.season_number; + body.seasonName = season.name; + } + const response = await fetch('/api/openlist/correct', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - folder, - tmdbId: result.id, - title: result.title || result.name, - posterPath: result.poster_path, - releaseDate: result.release_date || result.first_air_date, - overview: result.overview, - voteAverage: result.vote_average, - mediaType: result.media_type, - }), + body: JSON.stringify(body), }); if (!response.ok) { @@ -120,6 +218,13 @@ export default function CorrectDialog({ } }; + // 返回搜索结果列表 + const handleBackToResults = () => { + setShowSeasonSelection(false); + setSelectedResult(null); + setSeasons([]); + }; + if (!isOpen) return null; return createPortal( @@ -169,11 +274,98 @@ export default function CorrectDialog({ {/* 结果列表 */}
- {results.length === 0 ? ( + {showSeasonSelection ? ( + // 季度选择界面 +
+
+ +
+ + {selectedResult && ( +
+

+ {selectedResult.title || selectedResult.name} +

+

+ 请选择季度: +

+
+ )} + + {loadingSeasons ? ( +
+ 加载季度列表中... +
+ ) : seasons.length === 0 ? ( +
+ 未找到季度信息 +
+ ) : ( +
+ {seasons.map((season) => ( +
+ {/* 海报 */} +
+ {season.poster_path ? ( + {season.name} + ) : ( +
+ 无海报 +
+ )} +
+ + {/* 信息 */} +
+

+ {season.name} +

+

+ {season.episode_count} 集 + {season.air_date && ` • ${season.air_date.split('-')[0]}`} +

+

+ {season.overview || '暂无简介'} +

+
+ + {/* 选择按钮 */} +
+ +
+
+ ))} +
+ )} +
+ ) : results.length === 0 ? ( + // 空状态
{searching ? '搜索中...' : '请输入关键词搜索'}
) : ( + // 搜索结果列表
{results.map((result) => (
diff --git a/src/components/VideoCard.tsx b/src/components/VideoCard.tsx index a7cbb76..9a1460d 100644 --- a/src/components/VideoCard.tsx +++ b/src/components/VideoCard.tsx @@ -49,6 +49,8 @@ export interface VideoCardProps { origin?: 'vod' | 'live'; releaseDate?: string; // 上映日期,格式:YYYY-MM-DD isUpcoming?: boolean; // 是否为即将上映 + seasonNumber?: number; // 季度编号 + seasonName?: string; // 季度名称 } export type VideoCardHandle = { @@ -80,6 +82,8 @@ const VideoCard = forwardRef(function VideoCard origin = 'vod', releaseDate, isUpcoming = false, + seasonNumber, + seasonName, }: VideoCardProps, ref ) { @@ -775,6 +779,25 @@ const VideoCard = forwardRef(function VideoCard
)} + {/* 季度徽章 */} + {seasonNumber && ( +
{ + e.preventDefault(); + return false; + }} + title={seasonName || `第${seasonNumber}季`} + > + S{seasonNumber} +
+ )} + {/* 徽章 */} {config.showRating && rate && (
{ + const seasonMatch = match[1].match(/[Ss](\d{1,2})/); + return seasonMatch ? parseInt(seasonMatch[1], 10) : null; + }, + }, + // S01, S1, s01, s1 格式 + { + regex: /\b[Ss](\d{1,2})\b/, + extract: (match: RegExpMatchArray) => parseInt(match[1], 10), + }, + // [Season 1], [Season 01] 格式(方括号包裹) + { + regex: /\[Season\s+(\d{1,2})\]/i, + extract: (match: RegExpMatchArray) => parseInt(match[1], 10), + }, + // Season 1, Season 01 格式 + { + regex: /\bSeason\s+(\d{1,2})\b/i, + extract: (match: RegExpMatchArray) => parseInt(match[1], 10), + }, + // [第一季], [第1季], [第01季] 格式(方括号包裹) + { + regex: /\[第([一二三四五六七八九十\d]{1,2})季\]/, + extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]), + }, + // 第一季, 第1季, 第01季 格式 + { + regex: /第([一二三四五六七八九十\d]{1,2})季/, + extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]), + }, + // [第一部], [第1部] 格式(方括号包裹) + { + regex: /\[第([一二三四五六七八九十\d]{1,2})部\]/, + extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]), + }, + // 第一部, 第1部, 第01部 格式 + { + regex: /第([一二三四五六七八九十\d]{1,2})部/, + extract: (match: RegExpMatchArray) => chineseNumberToInt(match[1]), + }, + ]; + + // 尝试匹配每个模式 + for (const pattern of patterns) { + const match = title.match(pattern.regex); + if (match) { + const extracted = pattern.extract(match); + if (extracted !== null) { + seasonNumber = extracted; + // 移除匹配到的季度标识 + cleanTitle = title.replace(pattern.regex, '').trim(); + break; + } + } + } + + // 清理标题:移除空的方括号和多余的空格 + cleanTitle = cleanTitle + .replace(/\[\s*\]/g, '') // 移除空方括号 + .replace(/\s+/g, ' ') // 合并多个空格 + .replace(/[·\-_\s]+$/, '') // 移除末尾的特殊字符 + .trim(); + + return { + cleanTitle, + seasonNumber, + originalTitle, + }; +} + +/** + * 将中文数字转换为阿拉伯数字 + */ +function chineseNumberToInt(str: string): number { + // 如果已经是数字,直接返回 + if (/^\d+$/.test(str)) { + return parseInt(str, 10); + } + + const chineseNumbers: Record = { + '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, + '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, + }; + + // 处理"十"的特殊情况 + if (str === '十') { + return 10; + } + + // 处理"十X"的情况(如"十一") + if (str.startsWith('十')) { + const unit = str.substring(1); + return 10 + (chineseNumbers[unit] || 0); + } + + // 处理"X十"的情况(如"二十") + if (str.endsWith('十')) { + const tens = str.substring(0, str.length - 1); + return (chineseNumbers[tens] || 0) * 10; + } + + // 处理"X十Y"的情况(如"二十一") + const tenIndex = str.indexOf('十'); + if (tenIndex !== -1) { + const tens = str.substring(0, tenIndex); + const units = str.substring(tenIndex + 1); + return (chineseNumbers[tens] || 0) * 10 + (chineseNumbers[units] || 0); + } + + // 单个中文数字 + return chineseNumbers[str] || parseInt(str, 10) || 1; +} + +/** + * 测试示例 + */ +export function testSeasonParser() { + const testCases = [ + '权力的游戏 第一季', + 'Breaking Bad S01', + 'Game of Thrones Season 1', + '绝命毒师 第1季', + '权力的游戏 S1', + '权力的游戏', + '绝命毒师 第二部', + 'Stranger Things S03', + ]; + + console.log('Season Parser Test Results:'); + testCases.forEach((title) => { + const result = parseSeasonFromTitle(title); + console.log(`Input: "${title}"`); + console.log(` Clean Title: "${result.cleanTitle}"`); + console.log(` Season: ${result.seasonNumber}`); + console.log(''); + }); +} diff --git a/src/lib/tmdb.search.ts b/src/lib/tmdb.search.ts index b35ec27..e28c1ae 100644 --- a/src/lib/tmdb.search.ts +++ b/src/lib/tmdb.search.ts @@ -79,6 +79,129 @@ export async function searchTMDB( } } +/** + * TMDB 季度信息 + */ +export interface TMDBSeasonInfo { + id: number; + name: string; + season_number: number; + episode_count: number; + air_date: string | null; + poster_path: string | null; + overview: string; +} + +/** + * TMDB 电视剧详情(包含季度列表) + */ +interface TMDBTVDetails { + id: number; + name: string; + seasons: TMDBSeasonInfo[]; + number_of_seasons: number; + poster_path: string | null; + first_air_date: string; + overview: string; + vote_average: number; +} + +/** + * 获取电视剧的季度列表 + */ +export async function getTVSeasons( + apiKey: string, + tvId: number, + proxy?: string +): Promise<{ code: number; seasons: TMDBSeasonInfo[] | null }> { + try { + if (!apiKey) { + return { code: 400, seasons: null }; + } + + const url = `https://api.themoviedb.org/3/tv/${tvId}?api_key=${apiKey}&language=zh-CN`; + + const fetchOptions: any = proxy + ? { + agent: new HttpsProxyAgent(proxy, { + timeout: 30000, + keepAlive: false, + }), + signal: AbortSignal.timeout(30000), + } + : { + signal: AbortSignal.timeout(15000), + }; + + const response = await nodeFetch(url, fetchOptions); + + if (!response.ok) { + console.error('TMDB 获取电视剧详情失败:', response.status, response.statusText); + return { code: response.status, seasons: null }; + } + + const data: TMDBTVDetails = await response.json() as TMDBTVDetails; + + // 过滤掉特殊季度(如 Season 0 通常是特别篇) + const validSeasons = data.seasons.filter((season) => season.season_number > 0); + + return { + code: 200, + seasons: validSeasons, + }; + } catch (error) { + console.error('TMDB 获取季度列表异常:', error); + return { code: 500, seasons: null }; + } +} + +/** + * 获取电视剧特定季度的详细信息 + */ +export async function getTVSeasonDetails( + apiKey: string, + tvId: number, + seasonNumber: number, + proxy?: string +): Promise<{ code: number; season: TMDBSeasonInfo | null }> { + try { + if (!apiKey) { + return { code: 400, season: null }; + } + + const url = `https://api.themoviedb.org/3/tv/${tvId}/season/${seasonNumber}?api_key=${apiKey}&language=zh-CN`; + + const fetchOptions: any = proxy + ? { + agent: new HttpsProxyAgent(proxy, { + timeout: 30000, + keepAlive: false, + }), + signal: AbortSignal.timeout(30000), + } + : { + signal: AbortSignal.timeout(15000), + }; + + const response = await nodeFetch(url, fetchOptions); + + if (!response.ok) { + console.error('TMDB 获取季度详情失败:', response.status, response.statusText); + return { code: response.status, season: null }; + } + + const data: TMDBSeasonInfo = await response.json() as TMDBSeasonInfo; + + return { + code: 200, + season: data, + }; + } catch (error) { + console.error('TMDB 获取季度详情异常:', error); + return { code: 500, season: null }; + } +} + /** * 获取 TMDB 图片完整 URL */