diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 2a13d21..c624917 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -2,7 +2,7 @@ 'use client'; -import { Heart, Search, X, Cloud, Sparkles } from 'lucide-react'; +import { Heart, Search, X, Cloud, Sparkles, AlertCircle } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Suspense, useEffect, useRef, useState } from 'react'; @@ -51,6 +51,7 @@ import { import type { DanmakuAnime, DanmakuSelection, DanmakuSettings, DanmakuComment } from '@/lib/danmaku/types'; import { SearchResult, DanmakuFilterConfig, EpisodeFilterConfig } from '@/lib/types'; import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils'; +import { getTMDBImageUrl } from '@/lib/tmdb.search'; import EpisodeSelector from '@/components/EpisodeSelector'; import DownloadEpisodeSelector from '@/components/DownloadEpisodeSelector'; @@ -63,6 +64,7 @@ import AIChatPanel from '@/components/AIChatPanel'; import { useEnableComments } from '@/hooks/useEnableComments'; import PansouSearch from '@/components/PansouSearch'; import CustomHeatmap from '@/components/CustomHeatmap'; +import CorrectDialog from '@/components/CorrectDialog'; // 扩展 HTMLVideoElement 类型以支持 hls 属性 declare global { @@ -122,6 +124,9 @@ function PlayPageClient() { const [aiEnabled, setAiEnabled] = useState(false); const [aiDefaultMessageWithVideo, setAiDefaultMessageWithVideo] = useState(''); + // 纠错弹窗状态 + const [showCorrectDialog, setShowCorrectDialog] = useState(false); + // 检查AI功能是否启用 useEffect(() => { if (typeof window !== 'undefined') { @@ -2624,10 +2629,22 @@ function PlayPageClient() { // 直接使用 detailData.source(已经是完整格式) setCurrentSource(detailData.source); setCurrentId(detailData.id); + + // 如果是小雅源,检查并应用纠错信息 + if (detailData.source === 'xiaoya') { + const correction = getXiaoyaCorrection(detailData.source, detailData.id); + if (correction) { + console.log('发现小雅源纠错信息,正在应用...', correction); + detailData = applyCorrection(detailData, correction); + } + } + + // 更新所有相关状态(在应用纠错信息之后) setVideoYear(detailData.year); setVideoTitle(detailData.title || videoTitleRef.current); setVideoCover(detailData.poster); setVideoDoubanId(detailData.douban_id || 0); + setDetail(detailData); setSourceProxyMode(detailData.proxyMode || false); // 从 detail 数据中读取代理模式 if (currentEpisodeIndex >= detailData.episodes.length) { @@ -3902,6 +3919,40 @@ function PlayPageClient() { } }; + // 纠错成功后的回调 + const handleCorrectSuccess = () => { + if (!detail || detail.source !== 'xiaoya') return; + + // 从 localStorage 读取纠错信息 + const correction = getXiaoyaCorrection(detail.source, detail.id); + if (correction) { + console.log('应用纠错信息:', correction); + + // 只更新显示相关的状态,不更新 detail(避免触发播放器刷新) + if (correction.title) { + setVideoTitle(correction.title); + } + if (correction.posterPath) { + // 补全 TMDB 图片 URL + const fullPosterUrl = getTMDBImageUrl(correction.posterPath); + setVideoCover(fullPosterUrl); + } + if (correction.doubanId) { + const doubanIdNum = typeof correction.doubanId === 'string' + ? parseInt(correction.doubanId, 10) + : correction.doubanId; + setVideoDoubanId(doubanIdNum); + } + + // 更新 detailRef,这样其他地方使用 detailRef 时能获取到最新信息 + if (detailRef.current) { + detailRef.current = applyCorrection(detailRef.current, correction); + } + + console.log('已应用纠错信息(页面刷新后将完全生效)'); + } + }; + useEffect(() => { if ( !videoUrl || @@ -6866,6 +6917,19 @@ function PlayPageClient() { )} + {/* 纠错按钮 - 仅小雅源显示 */} + {detail && detail.source === 'xiaoya' && ( + + )} {/* 豆瓣评分显示 */} {doubanRating && doubanRating.value > 0 && (
@@ -7140,10 +7204,63 @@ function PlayPageClient() { welcomeMessage={aiDefaultMessageWithVideo ? aiDefaultMessageWithVideo.replace('{title}', detail.title || '') : `想了解《${detail.title}》的更多信息吗?我可以帮你查询剧情、演员、评价等。`} /> )} + + {/* 纠错弹窗 - 仅小雅源显示 */} + {detail && detail.source === 'xiaoya' && ( + setShowCorrectDialog(false)} + videoKey={`${detail.source}_${detail.id}`} + currentTitle={detail.title} + currentVideo={{ + tmdbId: detail.tmdb_id, + doubanId: detail.douban_id ? String(detail.douban_id) : undefined, + poster: detail.poster, + releaseDate: detail.year, + overview: detail.desc, + voteAverage: detail.rating, + mediaType: detail.type_name === '电影' ? 'movie' : 'tv', + }} + source="xiaoya" + onCorrect={() => { + // 纠错成功后的回调 + handleCorrectSuccess(); + }} + /> + )} ); } +// 从 localStorage 读取小雅源的纠错信息 +const getXiaoyaCorrection = (source: string, id: string) => { + try { + const storageKey = `xiaoya_correction_${source}_${id}`; + const correctionJson = localStorage.getItem(storageKey); + if (correctionJson) { + return JSON.parse(correctionJson); + } + } catch (error) { + console.error('读取纠错信息失败:', error); + } + return null; +}; + +// 应用纠错信息到 detail 对象 +const applyCorrection = (detail: SearchResult, correction: any): SearchResult => { + return { + ...detail, + title: correction.title || detail.title, + poster: correction.posterPath ? getTMDBImageUrl(correction.posterPath) : detail.poster, + year: correction.releaseDate || detail.year, + desc: correction.overview || detail.desc, + rating: correction.voteAverage || detail.rating, + tmdb_id: correction.tmdbId || detail.tmdb_id, + douban_id: correction.doubanId ? (typeof correction.doubanId === 'string' ? parseInt(correction.doubanId, 10) : correction.doubanId) : detail.douban_id, + type_name: correction.mediaType === 'movie' ? '电影' : (correction.mediaType === 'tv' ? '电视剧' : detail.type_name), + }; +}; + // FavoriteIcon 组件 const FavoriteIcon = ({ filled }: { filled: boolean }) => { if (filled) { diff --git a/src/components/CorrectDialog.tsx b/src/components/CorrectDialog.tsx index d302bde..6e2a85a 100644 --- a/src/components/CorrectDialog.tsx +++ b/src/components/CorrectDialog.tsx @@ -49,6 +49,7 @@ interface CorrectDialogProps { seasonName?: string; }; onCorrect: () => void; + source?: string; // 新增:视频源类型(xiaoya, openlist等) } export default function CorrectDialog({ @@ -58,6 +59,7 @@ export default function CorrectDialog({ currentTitle, currentVideo, onCorrect, + source = 'openlist', // 默认为 openlist }: CorrectDialogProps) { const [searchQuery, setSearchQuery] = useState(currentTitle); const [searching, setSearching] = useState(false); @@ -229,8 +231,7 @@ export default function CorrectDialog({ finalTitle = `${finalTitle} ${season.name}`; } - const body: any = { - key: videoKey, + const correctionData: any = { tmdbId: finalTmdbId, title: finalTitle, posterPath: season?.poster_path || result.poster_path, @@ -240,20 +241,38 @@ export default function CorrectDialog({ mediaType: result.media_type, }; - // 如果有季度信息,添加到请求中 + // 如果有季度信息,添加到数据中 if (season) { - body.seasonNumber = season.season_number; - body.seasonName = season.name; + correctionData.seasonNumber = season.season_number; + correctionData.seasonName = season.name; } - const response = await fetch('/api/openlist/correct', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); + // 根据源类型选择不同的存储方式 + if (source === 'xiaoya') { + // 小雅源:存储到 localStorage + const storageKey = `xiaoya_correction_${videoKey}`; + const correctionInfo = { + ...correctionData, + correctedAt: Date.now(), + }; + localStorage.setItem(storageKey, JSON.stringify(correctionInfo)); + console.log('小雅源纠错信息已存储到 localStorage:', storageKey, correctionInfo); + } else { + // openlist 等其他源:调用 API + const body: any = { + key: videoKey, + ...correctionData, + }; - if (!response.ok) { - throw new Error('纠错失败'); + const response = await fetch('/api/openlist/correct', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error('纠错失败'); + } } onCorrect(); @@ -313,8 +332,7 @@ export default function CorrectDialog({ setError(''); try { - const body: any = { - key: videoKey, + const correctionData: any = { title: manualData.title.trim(), posterPath: manualData.posterPath.trim() || null, releaseDate: manualData.releaseDate.trim() || '', @@ -325,28 +343,46 @@ export default function CorrectDialog({ // 添加 TMDB ID(如果提供) if (manualData.tmdbId.trim()) { - body.tmdbId = Number(manualData.tmdbId); + correctionData.tmdbId = Number(manualData.tmdbId); } // 添加豆瓣 ID(如果提供) if (manualData.doubanId.trim()) { - body.doubanId = manualData.doubanId.trim(); + correctionData.doubanId = manualData.doubanId.trim(); } // 如果是电视剧且有季度信息 if (manualData.mediaType === 'tv' && manualData.seasonNumber) { - body.seasonNumber = Number(manualData.seasonNumber); - body.seasonName = manualData.seasonName.trim() || `第 ${manualData.seasonNumber} 季`; + correctionData.seasonNumber = Number(manualData.seasonNumber); + correctionData.seasonName = manualData.seasonName.trim() || `第 ${manualData.seasonNumber} 季`; } - const response = await fetch('/api/openlist/correct', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); + // 根据源类型选择不同的存储方式 + if (source === 'xiaoya') { + // 小雅源:存储到 localStorage + const storageKey = `xiaoya_correction_${videoKey}`; + const correctionInfo = { + ...correctionData, + correctedAt: Date.now(), + }; + localStorage.setItem(storageKey, JSON.stringify(correctionInfo)); + console.log('小雅源纠错信息已存储到 localStorage:', storageKey, correctionInfo); + } else { + // openlist 等其他源:调用 API + const body: any = { + key: videoKey, + ...correctionData, + }; - if (!response.ok) { - throw new Error('纠错失败'); + const response = await fetch('/api/openlist/correct', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error('纠错失败'); + } } onCorrect(); diff --git a/src/lib/types.ts b/src/lib/types.ts index 19a4508..02802fa 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -137,6 +137,8 @@ export interface SearchResult { vod_total?: number; // 总集数 proxyMode?: boolean; // 代理模式:启用后由服务器代理m3u8和ts分片 subtitles?: Array>; // 字幕列表(按集数索引) + tmdb_id?: number; // TMDB ID + rating?: number; // 评分 } // 豆瓣数据结构