Refactor ShowDetail component to utilize parseEpisodeName for episode parsing: replace legacy parsing logic with a new function that supports additional episode naming formats. Update sorting and display logic to reflect parsed episode data, enhancing the overall user experience.

This commit is contained in:
Leonid Pershin
2026-07-25 08:23:59 +03:00
parent 2dd8bf9724
commit de7e27c80f
2 changed files with 28 additions and 37 deletions
@@ -10,21 +10,16 @@ import { Button } from '@/shared/ui/button'
import { Input } from '@/shared/ui/input'
import { toast } from '@/shared/ui/toast-store'
import { listMedia } from '@/features/admin/media/api'
import {
type ParsedEpisode,
compareParsed,
formatSeasonEpisode,
parseEpisodeName,
} from '@/features/admin/media/episode-parse'
import { formatDuration } from '@/features/admin/media/MediaPanel'
import { addEpisode, getShow, removeEpisode } from './api'
const pad2 = (n: number) => String(n).padStart(2, '0')
/** Достаёт сезон/серию из имени файла: SxxEyy либо NxNN. Нужно для сортировки в правильный порядок. */
function parseEpisode(name: string): { season: number; episode: number } | null {
const m1 = name.match(/[Ss](\d{1,2})[ ._-]*[Ee](\d{1,3})/)
if (m1) return { season: Number(m1[1]), episode: Number(m1[2]) }
const m2 = name.match(/(?:^|[^\d])(\d{1,2})x(\d{1,3})(?:[^\d]|$)/i)
if (m2) return { season: Number(m2[1]), episode: Number(m2[2]) }
return null
}
type Candidate = { asset: MediaAssetDto; se: { season: number; episode: number } | null }
type Candidate = { asset: MediaAssetDto; parsed: ParsedEpisode }
export function ShowDetail({ showId }: { showId: string }) {
const { t } = useTranslation()
@@ -61,14 +56,13 @@ export function ShowDetail({ showId }: { showId: string }) {
return (ready?.items ?? [])
.filter((a) => !existing.has(a.id))
.filter((a) => !term || a.originalFileName.toLowerCase().includes(term))
.map((asset) => ({ asset, se: parseEpisode(asset.originalFileName) }))
.sort((a, b) => {
if (a.se && b.se)
return a.se.season - b.se.season || a.se.episode - b.se.episode
if (a.se) return -1
if (b.se) return 1
return a.asset.originalFileName.localeCompare(b.asset.originalFileName)
})
.map((asset) => ({ asset, parsed: parseEpisodeName(asset.originalFileName) }))
.sort((a, b) =>
compareParsed(
{ name: a.asset.originalFileName, parsed: a.parsed },
{ name: b.asset.originalFileName, parsed: b.parsed },
),
)
}, [show, ready, filter])
const selected = candidates.filter((c) => !deselected.has(c.asset.id))
@@ -78,8 +72,8 @@ export function ShowDetail({ showId }: { showId: string }) {
if (!show) return [] as number[]
const set = new Set<number>()
for (const e of show.episodes) {
const se = parseEpisode(e.assetName ?? '')
if (se) set.add(se.season)
const parsed = parseEpisodeName(e.assetName ?? '')
if (parsed.episode != null && parsed.season != null) set.add(parsed.season)
}
return [...set].sort((a, b) => a - b)
}, [show])
@@ -177,7 +171,9 @@ export function ShowDetail({ showId }: { showId: string }) {
<p className="px-4 py-3 text-sm text-muted-foreground">{t('admin.shows.noMatches')}</p>
) : (
<ul className="divide-y divide-border text-sm">
{candidates.map(({ asset, se }) => (
{candidates.map(({ asset, parsed }) => {
const label = formatSeasonEpisode(parsed)
return (
<li key={asset.id}>
<label className="flex cursor-pointer items-center gap-3 px-4 py-2 hover:bg-muted">
<input
@@ -185,17 +181,12 @@ export function ShowDetail({ showId }: { showId: string }) {
checked={!deselected.has(asset.id)}
onChange={() => toggle(asset.id)}
/>
{se ? (
<Badge>
S{pad2(se.season)}E{pad2(se.episode)}
</Badge>
) : (
<Badge variant="muted"></Badge>
)}
{label ? <Badge>{label}</Badge> : <Badge variant="muted"></Badge>}
<span className="truncate">{asset.originalFileName}</span>
</label>
</li>
))}
)
})}
</ul>
)}
</div>
@@ -215,17 +206,13 @@ export function ShowDetail({ showId }: { showId: string }) {
</thead>
<tbody>
{show.episodes.map((episode, index) => {
const se = parseEpisode(episode.assetName ?? '')
const label = formatSeasonEpisode(parseEpisodeName(episode.assetName ?? ''))
return (
<tr key={episode.id} className="border-b border-border last:border-0">
<td className="px-4 py-2 text-muted-foreground">{index + 1}</td>
<td className="px-4 py-2">
<div className="flex items-center gap-2">
{se && (
<Badge>
S{pad2(se.season)}E{pad2(se.episode)}
</Badge>
)}
{label && <Badge>{label}</Badge>}
<span>{episode.assetName ?? '—'}</span>
</div>
</td>