Enhance schedule and show management: add season and episode information to ScheduleEntryDto, update ListShowsQueryHandler to include season counts, and modify frontend components to display this new data. Improve EPG handling by consolidating program entries into blocks for better clarity in the streaming interface.

This commit is contained in:
Leonid Pershin
2026-07-24 22:18:50 +03:00
parent 517f11c897
commit 2523808e3b
11 changed files with 134 additions and 26 deletions
@@ -614,8 +614,12 @@ function SchedulePreview({ entries }: { entries: ScheduleEntryDto[] }) {
) : (
<span>
{e.showName ?? '—'}
{e.episodeIndex != null && (
<span className="text-muted-foreground"> · {t('air.episode')} {e.episodeIndex + 1}</span>
{e.seasonEpisode ? (
<span className="text-muted-foreground"> · {e.seasonEpisode}</span>
) : (
e.episodeIndex != null && (
<span className="text-muted-foreground"> · {t('air.episode')} {e.episodeIndex + 1}</span>
)
)}
</span>
)}
@@ -67,6 +67,7 @@ export function ShowsPanel() {
<tr>
<th className="px-4 py-2 font-medium">{t('admin.shows.name')}</th>
<th className="px-4 py-2 font-medium">{t('admin.shows.kind')}</th>
<th className="px-4 py-2 font-medium">{t('admin.shows.seasons')}</th>
<th className="px-4 py-2 font-medium">{t('admin.shows.episodes')}</th>
<th className="px-4 py-2 font-medium">{t('common.actions')}</th>
</tr>
@@ -74,7 +75,7 @@ export function ShowsPanel() {
<tbody>
{isLoading && (
<tr>
<td className="px-4 py-3 text-muted-foreground" colSpan={4}>
<td className="px-4 py-3 text-muted-foreground" colSpan={5}>
{t('common.loading')}
</td>
</tr>
@@ -93,6 +94,7 @@ export function ShowsPanel() {
<td className="px-4 py-2">
<Badge variant="muted">{t(`admin.shows.kinds.${show.kind}`)}</Badge>
</td>
<td className="px-4 py-2 text-muted-foreground">{show.seasonCount}</td>
<td className="px-4 py-2 text-muted-foreground">{show.episodeCount}</td>
<td className="px-4 py-2">
<Button
+41 -18
View File
@@ -62,7 +62,7 @@ export function AirPage() {
refetchInterval: 60_000,
})
const { current, upcoming } = useMemo(() => splitEpg(epg ?? []), [epg])
const { current, upcoming } = useMemo(() => buildGuide(epg ?? []), [epg])
if (isLoading) return <p className="text-muted-foreground">{t('common.loading')}</p>
@@ -125,7 +125,7 @@ export function AirPage() {
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Badge>{t('air.now')}</Badge>
<span className="font-medium">{programLabel(current, t)}</span>
<span className="font-medium">{current.showName}</span>
</div>
<span className="text-xs text-muted-foreground">
{formatTime(current.startsAtUtc)} {formatTime(current.endsAtUtc)}
@@ -139,12 +139,12 @@ export function AirPage() {
{t('air.next')}
</div>
<ul className="divide-y divide-border">
{upcoming.slice(0, 6).map((entry) => (
<li key={entry.id} className="flex items-center gap-3 px-4 py-2 text-sm">
<span className="w-12 shrink-0 text-muted-foreground">
{formatTime(entry.startsAtUtc)}
{upcoming.slice(0, 6).map((block) => (
<li key={block.key} className="flex items-center gap-3 px-4 py-2 text-sm">
<span className="w-24 shrink-0 text-muted-foreground">
{formatTime(block.startsAtUtc)} {formatTime(block.endsAtUtc)}
</span>
<span>{programLabel(entry, t)}</span>
<span>{block.showName}</span>
</li>
))}
</ul>
@@ -157,17 +157,40 @@ export function AirPage() {
)
}
function splitEpg(entries: ScheduleEntryDto[]) {
const now = Date.now()
const current = entries.find(
(e) => new Date(e.startsAtUtc).getTime() <= now && new Date(e.endsAtUtc).getTime() > now,
)
const upcoming = entries.filter((e) => new Date(e.startsAtUtc).getTime() > now)
return { current, upcoming }
type GuideBlock = {
key: string
showId: string | null
showName: string
startsAtUtc: string
endsAtUtc: string
}
function programLabel(entry: ScheduleEntryDto, t: (key: string) => string) {
// На странице эфира — только название шоу (сезон/серия видны в админке).
if (entry.kind === 'Ad') return t('air.ad')
return entry.showName ?? '—'
/**
* Строит телегид: рекламу не показываем, а подряд идущие серии одного шоу склеиваем в один блок
* с диапазоном «с – по». Реклама между сериями одного шоу поглощается блоком (как в обычном EPG).
*/
function buildGuide(entries: ScheduleEntryDto[]): { current?: GuideBlock; upcoming: GuideBlock[] } {
const blocks: GuideBlock[] = []
for (const entry of entries) {
if (entry.kind !== 'Program') continue
const last = blocks[blocks.length - 1]
if (last && last.showId === entry.showId) {
last.endsAtUtc = entry.endsAtUtc
} else {
blocks.push({
key: entry.id,
showId: entry.showId,
showName: entry.showName ?? '—',
startsAtUtc: entry.startsAtUtc,
endsAtUtc: entry.endsAtUtc,
})
}
}
const now = Date.now()
const current = blocks.find(
(b) => new Date(b.startsAtUtc).getTime() <= now && new Date(b.endsAtUtc).getTime() > now,
)
const upcoming = blocks.filter((b) => new Date(b.startsAtUtc).getTime() > now)
return { current, upcoming }
}
+2
View File
@@ -66,6 +66,7 @@ export type ShowSummaryDto = {
name: string
kind: ShowKind
episodeCount: number
seasonCount: number
createdAt: string
}
@@ -149,6 +150,7 @@ export type ScheduleEntryDto = {
showId: string | null
showName: string | null
episodeIndex: number | null
seasonEpisode: string | null
}
// ── Публичный эфир ─────────────────────────────────────────────────────────
+2
View File
@@ -127,6 +127,7 @@ const resources = {
name: 'Название',
kind: 'Тип',
kinds: { Series: 'Сериал', Single: 'Полнометражка' },
seasons: 'Сезоны',
episodes: 'Серии',
episode: 'Серия',
addEpisode: 'Добавить серию',
@@ -321,6 +322,7 @@ const resources = {
name: 'Name',
kind: 'Kind',
kinds: { Series: 'Series', Single: 'Movie' },
seasons: 'Seasons',
episodes: 'Episodes',
episode: 'Episode',
addEpisode: 'Add episode',