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
@@ -25,6 +25,10 @@ export function parseEpisodeName(name: string, opts?: ParseOptions): ParsedEpiso
if (nx) { if (nx) {
season = Number(nx[1]) season = Number(nx[1])
episode = Number(nx[2]) episode = Number(nx[2])
} else {
// Ведущий номер серии: «01. Название», «02 - Название», «03_Название», «4) Название».
const lead = name.match(/^\s*(\d{1,3})[\s._)\]-]/)
if (lead) episode = Number(lead[1])
} }
} }
@@ -10,21 +10,16 @@ import { Button } from '@/shared/ui/button'
import { Input } from '@/shared/ui/input' import { Input } from '@/shared/ui/input'
import { toast } from '@/shared/ui/toast-store' import { toast } from '@/shared/ui/toast-store'
import { listMedia } from '@/features/admin/media/api' 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 { formatDuration } from '@/features/admin/media/MediaPanel'
import { addEpisode, getShow, removeEpisode } from './api' import { addEpisode, getShow, removeEpisode } from './api'
const pad2 = (n: number) => String(n).padStart(2, '0') type Candidate = { asset: MediaAssetDto; parsed: ParsedEpisode }
/** Достаёт сезон/серию из имени файла: 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 }
export function ShowDetail({ showId }: { showId: string }) { export function ShowDetail({ showId }: { showId: string }) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -61,14 +56,13 @@ export function ShowDetail({ showId }: { showId: string }) {
return (ready?.items ?? []) return (ready?.items ?? [])
.filter((a) => !existing.has(a.id)) .filter((a) => !existing.has(a.id))
.filter((a) => !term || a.originalFileName.toLowerCase().includes(term)) .filter((a) => !term || a.originalFileName.toLowerCase().includes(term))
.map((asset) => ({ asset, se: parseEpisode(asset.originalFileName) })) .map((asset) => ({ asset, parsed: parseEpisodeName(asset.originalFileName) }))
.sort((a, b) => { .sort((a, b) =>
if (a.se && b.se) compareParsed(
return a.se.season - b.se.season || a.se.episode - b.se.episode { name: a.asset.originalFileName, parsed: a.parsed },
if (a.se) return -1 { name: b.asset.originalFileName, parsed: b.parsed },
if (b.se) return 1 ),
return a.asset.originalFileName.localeCompare(b.asset.originalFileName) )
})
}, [show, ready, filter]) }, [show, ready, filter])
const selected = candidates.filter((c) => !deselected.has(c.asset.id)) 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[] if (!show) return [] as number[]
const set = new Set<number>() const set = new Set<number>()
for (const e of show.episodes) { for (const e of show.episodes) {
const se = parseEpisode(e.assetName ?? '') const parsed = parseEpisodeName(e.assetName ?? '')
if (se) set.add(se.season) if (parsed.episode != null && parsed.season != null) set.add(parsed.season)
} }
return [...set].sort((a, b) => a - b) return [...set].sort((a, b) => a - b)
}, [show]) }, [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> <p className="px-4 py-3 text-sm text-muted-foreground">{t('admin.shows.noMatches')}</p>
) : ( ) : (
<ul className="divide-y divide-border text-sm"> <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}> <li key={asset.id}>
<label className="flex cursor-pointer items-center gap-3 px-4 py-2 hover:bg-muted"> <label className="flex cursor-pointer items-center gap-3 px-4 py-2 hover:bg-muted">
<input <input
@@ -185,17 +181,12 @@ export function ShowDetail({ showId }: { showId: string }) {
checked={!deselected.has(asset.id)} checked={!deselected.has(asset.id)}
onChange={() => toggle(asset.id)} onChange={() => toggle(asset.id)}
/> />
{se ? ( {label ? <Badge>{label}</Badge> : <Badge variant="muted"></Badge>}
<Badge>
S{pad2(se.season)}E{pad2(se.episode)}
</Badge>
) : (
<Badge variant="muted"></Badge>
)}
<span className="truncate">{asset.originalFileName}</span> <span className="truncate">{asset.originalFileName}</span>
</label> </label>
</li> </li>
))} )
})}
</ul> </ul>
)} )}
</div> </div>
@@ -215,17 +206,13 @@ export function ShowDetail({ showId }: { showId: string }) {
</thead> </thead>
<tbody> <tbody>
{show.episodes.map((episode, index) => { {show.episodes.map((episode, index) => {
const se = parseEpisode(episode.assetName ?? '') const label = formatSeasonEpisode(parseEpisodeName(episode.assetName ?? ''))
return ( return (
<tr key={episode.id} className="border-b border-border last:border-0"> <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 text-muted-foreground">{index + 1}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{se && ( {label && <Badge>{label}</Badge>}
<Badge>
S{pad2(se.season)}E{pad2(se.episode)}
</Badge>
)}
<span>{episode.assetName ?? '—'}</span> <span>{episode.assetName ?? '—'}</span>
</div> </div>
</td> </td>