diff --git a/frontend/src/features/admin/media/episode-parse.ts b/frontend/src/features/admin/media/episode-parse.ts index 60be8d9..6deb6b2 100644 --- a/frontend/src/features/admin/media/episode-parse.ts +++ b/frontend/src/features/admin/media/episode-parse.ts @@ -25,6 +25,10 @@ export function parseEpisodeName(name: string, opts?: ParseOptions): ParsedEpiso if (nx) { season = Number(nx[1]) episode = Number(nx[2]) + } else { + // Ведущий номер серии: «01. Название», «02 - Название», «03_Название», «4) Название». + const lead = name.match(/^\s*(\d{1,3})[\s._)\]-]/) + if (lead) episode = Number(lead[1]) } } diff --git a/frontend/src/features/admin/shows/ShowDetail.tsx b/frontend/src/features/admin/shows/ShowDetail.tsx index b38ee15..58af034 100644 --- a/frontend/src/features/admin/shows/ShowDetail.tsx +++ b/frontend/src/features/admin/shows/ShowDetail.tsx @@ -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() 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 }) {

{t('admin.shows.noMatches')}

) : ( )} @@ -215,17 +206,13 @@ export function ShowDetail({ showId }: { showId: string }) { {show.episodes.map((episode, index) => { - const se = parseEpisode(episode.assetName ?? '') + const label = formatSeasonEpisode(parseEpisodeName(episode.assetName ?? '')) return ( {index + 1}
- {se && ( - - S{pad2(se.season)}E{pad2(se.episode)} - - )} + {label && {label}} {episode.assetName ?? '—'}