import { Link } from 'react-router-dom'
import { Play } from 'lucide-react'
import { imageURL } from '../api/client'
import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
import type { Media } from '../types'
import { seriesTitleFromPath } from '../utils/groupSeries'
import { formatSize } from './libraryPageModel'
type SeasonGroup = {
season: number
episodes: Media[]
}
type LibrarySeriesEpisodesProps = {
loading: boolean
selectedEpisodes: SeasonGroup[]
selectedSeason: number | null
visibleEpisodes: Media[]
playbackFrom: string
onSeasonChange: (season: number) => void
}
export function LibrarySeriesEpisodes({
loading,
selectedEpisodes,
selectedSeason,
visibleEpisodes,
playbackFrom,
onSeasonChange,
}: LibrarySeriesEpisodesProps) {
if (loading) {
return (
正在加载剧集…
)
}
const displaySeason = selectedSeason ?? selectedEpisodes[0]?.season ?? 1
return (
<>
{selectedEpisodes.map(({ season, episodes }) => (
))}
{displaySeason === 0 ? '特别篇' : `第 ${displaySeason} 季`}
{visibleEpisodes.map((ep) => {
const displayTitle = episodeDisplayTitle(ep, visibleEpisodes)
return (
{ep.backdrop_url || ep.poster_url ? (

) : (
{ep.episode_num || '—'}
)}
{ep.episode_num > 0 && (
{ep.episode_num}
)}
{displayTitle}
{ep.duration_sec > 0
? `${Math.floor(ep.duration_sec / 60)} 分钟`
: formatSize(ep.size_bytes)}
)
})}
>
)
}
function episodeDisplayTitle(ep: Media, siblings: Media[]): string {
let mainTitle = ''
const epTitle = ep.episode_title?.trim()
if (epTitle && !looksLikeSeriesTitle(ep, epTitle, siblings)) {
mainTitle = epTitle
} else {
const mediaTitle = ep.title?.trim()
if (mediaTitle && !looksLikeSeriesTitle(ep, mediaTitle, siblings)) {
mainTitle = mediaTitle
}
}
if (ep.episode_num > 0) {
if (!mainTitle) {
return `第 ${ep.episode_num} 集`
}
const prefixRegex = new RegExp(`^(第\\s*0*${ep.episode_num}\\s*集|ep?\\.?\\s*0*${ep.episode_num}\\b)`, 'i')
if (prefixRegex.test(mainTitle)) {
return mainTitle
}
return `第 ${ep.episode_num} 集 · ${mainTitle}`
}
return mainTitle || '未命名'
}
function looksLikeSeriesTitle(ep: Media, title: string, siblings: Media[]): boolean {
const normalized = normalizeEpisodeTitle(title)
if (!normalized) return true
if (ep.original_name && normalizeEpisodeTitle(ep.original_name) === normalized) return true
const pathTitle = seriesTitleFromPath(ep.path)
if (pathTitle && normalizeEpisodeTitle(pathTitle) === normalized) return true
const siblingTitles = new Set(
siblings
.map((item) => normalizeEpisodeTitle(item.title))
.filter(Boolean),
)
return siblingTitles.size === 1 && siblingTitles.has(normalized) && siblings.length > 1
}
function normalizeEpisodeTitle(value?: string): string {
return (value ?? '')
.toLowerCase()
.replace(/\s*\((?:19|20)\d{2}\)\s*/g, ' ')
.replace(/\s*\{(?:tmdb|tmdbid|douban|bangumi|bgm|thetvdb|tvdb)[\s:=#-]*[a-z0-9_-]+\}\s*/g, ' ')
.replace(/[\s._-]+/g, ' ')
.trim()
}