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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user