([]);
+ 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.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
*/