diff --git a/backend/src/TeleWave.Application/Programming/Templates/Generate/GroupCatalog.cs b/backend/src/TeleWave.Application/Programming/Templates/Generate/GroupCatalog.cs
index c586db5..256f8f6 100644
--- a/backend/src/TeleWave.Application/Programming/Templates/Generate/GroupCatalog.cs
+++ b/backend/src/TeleWave.Application/Programming/Templates/Generate/GroupCatalog.cs
@@ -113,14 +113,6 @@ public sealed class GroupCatalog(
if (units == 0)
return null;
- // У коллекции своего типа нет: франшиза — это почти всегда полнометражки.
- var seriesUnits = resolved.Where(r => r.ShowKind == ShowKind.Series).Sum(r => r.UnitCount);
- // Ролики распознаются первыми: группа рекламы даёт сотни коротких единиц, то есть самый
- // большой UnitCount из всех, и записанная «полным метром» она побеждала бы по баллам
- // в любой полосе без предпочтения по типу — в эфир уходила бы полоса рекламы.
- var interstitialUnits = resolved
- .Where(r => r.ShowKind == ShowKind.Interstitial)
- .Sum(r => r.UnitCount);
var duration = resolved.Aggregate(TimeSpan.Zero, (sum, r) => sum + r.TotalDuration);
return new GroupCandidate(
@@ -129,11 +121,30 @@ public sealed class GroupCatalog(
composition.Count,
units,
resolved.Max(r => r.Audience),
- interstitialUnits * 2 >= units ? ShowKind.Interstitial
- : seriesUnits * 2 >= units ? ShowKind.Series
- : ShowKind.Single,
+ DominantKindOf(resolved, units),
duration.TotalMinutes / units,
primaryGenreId
);
}
+
+ ///
+ /// Чем группа является по составу — по единицам, а не по позициям: полоса собирается под длину
+ /// того, что в ней играет.
+ ///
+ /// Ролики проверяются первыми: группа рекламы даёт сотни коротких единиц, то есть самый большой
+ /// UnitCount на канале, и записанная «полным метром» она побеждала бы по баллам в любой
+ /// полосе без предпочтения по типу — в эфир уходила бы полоса рекламы. У коллекции своего типа
+ /// нет: франшиза — это почти всегда полнометражки.
+ ///
+ private static ShowKind DominantKindOf(IReadOnlyList resolved, int units)
+ {
+ var interstitialUnits = resolved
+ .Where(r => r.ShowKind == ShowKind.Interstitial)
+ .Sum(r => r.UnitCount);
+ if (interstitialUnits * 2 >= units)
+ return ShowKind.Interstitial;
+
+ var seriesUnits = resolved.Where(r => r.ShowKind == ShowKind.Series).Sum(r => r.UnitCount);
+ return seriesUnits * 2 >= units ? ShowKind.Series : ShowKind.Single;
+ }
}
diff --git a/frontend/src/features/admin/shows/AddEpisodesDialog.tsx b/frontend/src/features/admin/shows/AddEpisodesDialog.tsx
index af539fc..38c77ca 100644
--- a/frontend/src/features/admin/shows/AddEpisodesDialog.tsx
+++ b/frontend/src/features/admin/shows/AddEpisodesDialog.tsx
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import { Play, X } from 'lucide-react'
import { qk } from '@/shared/api/query-keys'
import type { MediaAssetDto, ShowDto } from '@/shared/api/types'
import { useApiError } from '@/shared/lib/use-api-error'
@@ -15,9 +16,10 @@ import {
DialogTitle,
} from '@/shared/ui/dialog'
import { Input } from '@/shared/ui/input'
+import { HlsVideo } from '@/shared/ui/hls-video'
import { Pager } from '@/shared/ui/pager'
import { toast } from '@/shared/ui/toast-store'
-import { listAllMedia } from '@/features/admin/media/api'
+import { listAllMedia, mediaPreviewUrl } from '@/features/admin/media/api'
import {
type ParsedEpisode,
compareParsed,
@@ -56,6 +58,9 @@ export function AddEpisodesDialog({
const [deselected, setDeselected] = useState>(new Set())
const [adding, setAdding] = useState<{ current: number; total: number } | null>(null)
const [page, setPage] = useState(1)
+ // Предпросмотр встроен в окно, а не открывается поверх: выбирают по именам файлов, и смотреть
+ // приходится подряд несколько — со второй модалкой это были бы два клика на каждый.
+ const [preview, setPreview] = useState(null)
const { data: ready, isLoading } = useQuery({
queryKey: qk.media.ready,
@@ -162,8 +167,9 @@ export function AddEpisodesDialog({
{items.map(({ asset, parsed }) => {
const label = formatSeasonEpisode(parsed)
return (
-