From 4ff7c1caf3a553a91a5e455ad37fba973a0bb424 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 11 Dec 2025 22:03:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=80=89=E9=9B=86=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/play/page.tsx | 127 ++++++--- src/components/DownloadEpisodeSelector.tsx | 297 +++++++++++++++++++++ 2 files changed, 384 insertions(+), 40 deletions(-) create mode 100644 src/components/DownloadEpisodeSelector.tsx diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 850294f..3754903 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -40,6 +40,7 @@ import { SearchResult, DanmakuFilterConfig } from '@/lib/types'; import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils'; import EpisodeSelector from '@/components/EpisodeSelector'; +import DownloadEpisodeSelector from '@/components/DownloadEpisodeSelector'; import PageLayout from '@/components/PageLayout'; import DoubanComments from '@/components/DoubanComments'; import DanmakuFilterSettings from '@/components/DanmakuFilterSettings'; @@ -500,6 +501,9 @@ function PlayPageClient() { const [isEpisodeSelectorCollapsed, setIsEpisodeSelectorCollapsed] = useState(false); + // 下载选集面板显示状态 + const [showDownloadSelector, setShowDownloadSelector] = useState(false); + // 换源加载状态 const [isVideoLoading, setIsVideoLoading] = useState(true); const [videoLoadingStage, setVideoLoadingStage] = useState< @@ -754,6 +758,76 @@ function PlayPageClient() { } }; + // 处理下载指定集数(支持批量下载) + const handleDownloadEpisode = async (episodeIndexes: number[]) => { + if (!detail || !detail.episodes || episodeIndexes.length === 0) { + if (artPlayerRef.current) { + artPlayerRef.current.notice.show = '无法获取视频地址'; + } + return; + } + + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; + const origin = `${window.location.protocol}//${window.location.host}`; + + let successCount = 0; + let failCount = 0; + + // 批量处理下载 + for (const episodeIndex of episodeIndexes) { + if (episodeIndex >= detail.episodes.length) { + failCount++; + continue; + } + + const episodeUrl = detail.episodes[episodeIndex]; + const proxyUrl = externalPlayerAdBlock + ? `${origin}/api/proxy-m3u8?url=${encodeURIComponent(episodeUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` + : episodeUrl; + const isM3u8 = episodeUrl.toLowerCase().includes('.m3u8') || episodeUrl.toLowerCase().includes('/m3u8/'); + + if (isM3u8) { + // M3U8格式 - 使用新的下载器,TS 格式 + try { + const downloadTitle = `${videoTitle}_第${episodeIndex + 1}集`; + await addDownloadTask(proxyUrl, downloadTitle, 'TS'); + successCount++; + } catch (error) { + console.error(`添加下载任务失败 (第${episodeIndex + 1}集):`, error); + failCount++; + } + } else { + // 普通视频格式 - 直接下载 + try { + const a = document.createElement('a'); + a.href = proxyUrl; + a.download = `${videoTitle}_第${episodeIndex + 1}集.mp4`; + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + successCount++; + // 添加延迟避免浏览器阻止多个下载 + await new Promise(resolve => setTimeout(resolve, 300)); + } catch (error) { + console.error(`下载失败 (第${episodeIndex + 1}集):`, error); + failCount++; + } + } + } + + // 显示结果通知 + if (artPlayerRef.current) { + if (failCount === 0) { + artPlayerRef.current.notice.show = `已添加 ${successCount} 个下载任务!`; + } else if (successCount === 0) { + artPlayerRef.current.notice.show = '下载失败,请重试'; + } else { + artPlayerRef.current.notice.show = `成功 ${successCount} 个,失败 ${failCount} 个`; + } + } + }; + const ensureVideoSource = (video: HTMLVideoElement | null, url: string) => { if (!video || !url) return; const sources = Array.from(video.getElementsByTagName('source')); @@ -3646,47 +3720,9 @@ function PlayPageClient() {
{/* 下载按钮 */} + + +
+ + + {/* 分页标签 */} + {pageCount > 1 && ( +
+
+
+ {categories.map((label, idx) => { + const isActive = idx === displayPage; + return ( + + ); + })} +
+
+ {/* 向上/向下按钮 */} + +
+ )} + + {/* 集数网格 */} +
+
+ {(() => { + const len = currentEnd - currentStart + 1; + const episodes = Array.from({ length: len }, (_, i) => + descending ? currentEnd - i : currentStart + i + ); + return episodes; + })().map((episodeIndex) => { + const isSelected = selectedEpisodes.has(episodeIndex); + const isCurrent = episodeIndex === currentEpisodeIndex; + const episodeNumber = episodeIndex + 1; + return ( + + ); + })} +
+
+ + {/* 底部操作栏 */} +
+
+ 已选择: + {selectedEpisodes.size === 0 ? ( + 未选择任何集数 + ) : selectedEpisodes.size === 1 ? ( + <> + 第 {Array.from(selectedEpisodes)[0] + 1} 集 + {Array.from(selectedEpisodes)[0] === currentEpisodeIndex && ( + (当前播放) + )} + + ) : ( + + {selectedEpisodes.size} 集 + + )} +
+
+ + +
+
+ + + ); +}; + +export default DownloadEpisodeSelector;