From 19f4957160c045b679a44f7f9669dc6591994303 Mon Sep 17 00:00:00 2001 From: mtvpls <247332661+mtvpls@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:42:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=85netdisk=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/source-detail/route.ts | 31 +++++----- src/components/EpisodeSelector.tsx | 90 +++++++++++++++++++++--------- 2 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/app/api/source-detail/route.ts b/src/app/api/source-detail/route.ts index 0e4a4c3..cc56165 100644 --- a/src/app/api/source-detail/route.ts +++ b/src/app/api/source-detail/route.ts @@ -142,30 +142,25 @@ function applyClientAdProxyToEpisodes( }); } +/** + * 网盘集标题:保留完整文件名(去掉常见视频扩展名), + * 供前端选集按钮长按/右键查看全名;按钮短标签仍由前端从文件名提取集数。 + * `parsed` 仅用于排序,不再覆盖为「第N集」。 + */ function formatNetdiskEpisodeTitle( - parsed: { + _parsed: { season?: number; episode?: number; }, fallback: string ) { - if (parsed.season && parsed.episode) { - const season = String(Math.trunc(parsed.season)).padStart(2, '0'); - const episodeValue = parsed.episode; - const episode = Number.isInteger(episodeValue) - ? String(Math.trunc(episodeValue)).padStart(2, '0') - : String(episodeValue); - return `S${season}E${episode}`; - } - - if (parsed.episode) { - const episodeValue = parsed.episode; - return Number.isInteger(episodeValue) - ? `第${Math.trunc(episodeValue)}集` - : `第${episodeValue}集`; - } - - return fallback; + const name = (fallback || '').trim(); + if (!name) return fallback; + // 去掉末尾视频扩展名,保留完整可读文件名 + return name.replace( + /\.(mp4|mkv|ts|m2ts|avi|mov|wmv|flv|webm|m4v|rmvb|iso|mpg|mpeg|m3u8)$/i, + '' + ); } /** diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx index 26286fc..46d0e29 100644 --- a/src/components/EpisodeSelector.tsx +++ b/src/components/EpisodeSelector.tsx @@ -23,7 +23,7 @@ import EpisodeFilterSettings from '@/components/EpisodeFilterSettings'; import ProxyImage from '@/components/ProxyImage'; import { useLongPress } from '@/hooks/useLongPress'; -/** 选集按钮上显示的短标签(数字等) */ +/** 选集按钮上显示的短标签(数字等);全名仍保留在 originalTitle 供长按查看 */ function getEpisodeDisplayLabel( title: string | undefined, episodeNumber: number @@ -31,19 +31,44 @@ function getEpisodeDisplayLabel( if (!title) { return String(episodeNumber); } - // 如果是 OVA 格式,直接返回完整标题 - if (title.match(/^OVA\s+\d+/i)) { - return title; + // OVA 单独展示 + const ovaMatch = title.match(/OVA\s*(\d+(?:\.\d+)?)/i); + if (ovaMatch) { + return `OVA ${ovaMatch[1]}`; } - // 如果匹配 S01E01 格式,只显示集数部分(去掉 SxxE) + // S01E05 / s01e05 → 5 const sxxexxMatch = title.match(/[Ss]\d+[Ee](\d{1,4}(?:\.\d+)?)/); if (sxxexxMatch) { return sxxexxMatch[1]; } - // 如果匹配"第X集"、"第X话"、"X集"、"X话"格式,提取中间的数字(支持小数) - const match = title.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/); - if (match) { - return match[1]; + // 第12集 / 12话 → 12 + const zhMatch = title.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/); + if (zhMatch) { + return zhMatch[1]; + } + // [01] / (01) → 1(网盘常见) + const bracketMatch = title.match(/[[(【](\d+(?:\.\d+)?)[\])】]/); + if (bracketMatch) { + return bracketMatch[1]; + } + // E01 / EP01 / ep.01 → 1 + const epMatch = title.match(/(?:^|[^a-zA-Z])(?:EP|E|ep|e)[.\s_-]*(\d+(?:\.\d+)?)/); + if (epMatch) { + return epMatch[1]; + } + // _01_ / -01- → 1 + const sepMatch = title.match(/[_-](\d+(?:\.\d+)?)[_-]/); + if (sepMatch) { + return sepMatch[1]; + } + // 纯数字开头:01.xxx / 01 xxx + const leadingNum = title.match(/^(\d+(?:\.\d+)?)[^\d.]/); + if (leadingNum) { + return leadingNum[1]; + } + // 整串就是数字 + if (/^\d+(?:\.\d+)?$/.test(title.trim())) { + return title.trim(); } return title; } @@ -61,24 +86,30 @@ interface EpisodeButtonProps { isWatched: boolean; originalTitle?: string; inactiveEpisodeClass: string; + /** 仅 netdisk 源启用长按/右键查看全名 */ + enableOriginalNamePopup?: boolean; onSelect: (zeroBasedIndex: number) => void; onShowOriginalName: (title: string, rect: DOMRect) => void; } -/** 单集按钮:点击选集;移动端长按 / 桌面右键显示原集名 popup */ +/** 单集按钮:点击选集;netdisk 时移动端长按 / 桌面右键显示原集名 popup */ const EpisodeButton: React.FC = ({ episodeNumber, isActive, isWatched, originalTitle, inactiveEpisodeClass, + enableOriginalNamePopup = false, onSelect, onShowOriginalName, }) => { const buttonRef = useRef(null); const displayLabel = getEpisodeDisplayLabel(originalTitle, episodeNumber); const canShowOriginalName = - !!originalTitle && originalTitle.trim() !== '' && originalTitle !== displayLabel; + enableOriginalNamePopup && + !!originalTitle && + originalTitle.trim() !== '' && + originalTitle !== displayLabel; const showOriginalName = useCallback(() => { if (!canShowOriginalName || !buttonRef.current) return; @@ -99,7 +130,8 @@ const EpisodeButton: React.FC = ({