From 6f74e66f0c25eaa49b2a87ee2fa33d7bf2c73bcb Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Wed, 29 Jul 2026 23:32:30 +0300 Subject: [PATCH] Enhance GroupFilterMatcher to handle interstitial shows and update related tests and documentation Updated the GroupFilterMatcher to exclude interstitial shows by default unless explicitly specified in the filter. Added new tests to verify the behavior of the filter with and without interstitial shows. Updated localization strings to clarify the handling of show types in the user interface and documentation, ensuring users understand the distinction between films, series, and clips. --- .../Programming/Groups/GroupFilterMatcher.cs | 6 ++++ .../Programming/GroupFilterMatcherTests.cs | 34 +++++++++++++++++++ docs/tv-scheduler-architecture.md | 6 ++++ .../admin/groups/GroupFilterPanel.tsx | 7 +++- frontend/src/shared/lib/locales/en.ts | 2 ++ frontend/src/shared/lib/locales/ru.ts | 2 ++ 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/backend/src/TeleWave.Application/Programming/Groups/GroupFilterMatcher.cs b/backend/src/TeleWave.Application/Programming/Groups/GroupFilterMatcher.cs index 046b528..248735c 100644 --- a/backend/src/TeleWave.Application/Programming/Groups/GroupFilterMatcher.cs +++ b/backend/src/TeleWave.Application/Programming/Groups/GroupFilterMatcher.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TeleWave.Application.Common.Interfaces; +using TeleWave.Domain.Library; using TeleWave.Domain.Programming; namespace TeleWave.Application.Programming.Groups; @@ -63,6 +64,11 @@ public sealed class GroupFilterMatcher(IAppDbContext dbContext) if (filter.ShowKinds is { Count: > 0 } kinds) shows = shows.Where(s => kinds.Contains(s.Kind)); + else + // Ролики в набор сами не идут: они живут отдельной библиотекой (см. 6.7), и правило без + // указанного типа означает «кино и сериалы», а не «всё, что есть в базе». Группу роликов + // собирают, отметив тип «Ролик» явно. + shows = shows.Where(s => s.Kind != ShowKind.Interstitial); // Рейтинги упорядочены по возрастанию строгости, поэтому «не строже» — обычное сравнение. // Шоу без рейтинга остаются в выборке — ровно как в планировщике (ElementSelector): источники diff --git a/backend/tests/TeleWave.Application.Tests/Programming/GroupFilterMatcherTests.cs b/backend/tests/TeleWave.Application.Tests/Programming/GroupFilterMatcherTests.cs index a43c0ce..6297f10 100644 --- a/backend/tests/TeleWave.Application.Tests/Programming/GroupFilterMatcherTests.cs +++ b/backend/tests/TeleWave.Application.Tests/Programming/GroupFilterMatcherTests.cs @@ -26,6 +26,40 @@ public class GroupFilterMatcherTests Assert.Equal([(GroupElementKind.Show, movie.Id)], matched); } + /// Группа рекламы собирается тем же правилом — иначе её нечем набрать, кроме как руками. + [Fact] + public async Task ShowKinds_Interstitial_SelectsClips() + { + var fixture = new TestDb(); + var movie = Show.Create("Фильм", ShowKind.Single); + var clip = Show.Create("Ролик", ShowKind.Interstitial); + await SeedAsync(fixture, movie, clip); + + var matched = await MatchAsync( + fixture, + new GroupFilter(ShowKinds: [ShowKind.Interstitial]) + ); + + Assert.Equal([(GroupElementKind.Show, clip.Id)], matched); + } + + /// + /// Без указанного типа правило означает «кино и сериалы»: ролики живут отдельной библиотекой, + /// и сотня рекламных вставок в группе фильмов — это не «широкий отбор», а сломанный эфир. + /// + [Fact] + public async Task ShowKinds_Empty_LeavesOutClips() + { + var fixture = new TestDb(); + var movie = Show.Create("Фильм", ShowKind.Single); + var clip = Show.Create("Ролик", ShowKind.Interstitial); + await SeedAsync(fixture, movie, clip); + + var matched = await MatchAsync(fixture, new GroupFilter()); + + Assert.Equal([(GroupElementKind.Show, movie.Id)], matched); + } + [Fact] public async Task MaxAudience_KeepsUnrated() { diff --git a/docs/tv-scheduler-architecture.md b/docs/tv-scheduler-architecture.md index ea14d95..632bd88 100644 --- a/docs/tv-scheduler-architecture.md +++ b/docs/tv-scheduler-architecture.md @@ -221,6 +221,12 @@ GroupItem } ``` +`showKinds` принимает и `interstitial`: группа рекламы собирается тем же правилом, что и группа кино, +иначе набрать её можно было бы только руками. Но **не указанный `showKinds` означает «кино и +сериалы», а не «всё подряд»**: ролики живут отдельной библиотекой (см. 6.7), и сотня рекламных +вставок, молча просочившаяся в группу фильмов, — это не широкий отбор, а сломанный эфир. Чтобы они +попали в набор, тип нужно отметить явно. + **Веса.** `GroupItem.weight` смещает вероятность внутри группы при случайном выборе. Нужен для реального случая: в группе на 200 боевиков полтора десятка сильных, остальное — наполнение; с одним только остыванием хорошее утонет в среднем. diff --git a/frontend/src/features/admin/groups/GroupFilterPanel.tsx b/frontend/src/features/admin/groups/GroupFilterPanel.tsx index be8c66a..bad1432 100644 --- a/frontend/src/features/admin/groups/GroupFilterPanel.tsx +++ b/frontend/src/features/admin/groups/GroupFilterPanel.tsx @@ -8,7 +8,11 @@ import { Input } from '@/shared/ui/input' import { Label } from '@/shared/ui/label' const ELEMENT_KINDS: GroupElementKind[] = ['Show', 'Collection'] -const SHOW_KINDS: ShowKind[] = ['Series', 'Single'] +/** + * Ролики в списке есть намеренно: группа рекламы собирается тем же правилом, что и группа кино. + * Сами по себе они в набор не попадают — сервер отдаёт их, только если тип отмечен явно. + */ +const SHOW_KINDS: ShowKind[] = ['Series', 'Single', 'Interstitial'] /** Конструктор правила набора. Правило не применяется само — оно только ищет кандидатов. */ export function GroupFilterPanel({ @@ -63,6 +67,7 @@ export function GroupFilterPanel({ ))} +

{t('admin.groups.filter.showKindsHint')}

diff --git a/frontend/src/shared/lib/locales/en.ts b/frontend/src/shared/lib/locales/en.ts index c55d63f..57ae6d6 100644 --- a/frontend/src/shared/lib/locales/en.ts +++ b/frontend/src/shared/lib/locales/en.ts @@ -147,6 +147,8 @@ export const en = { hint: 'In an explicit list the rule only finds candidates; in a rule-based group it is the composition.', elementKinds: 'What to search', showKinds: 'Show type', + showKindsHint: + 'Unchecked means films and series. Clips live in their own library and join the selection only when checked explicitly.', genres: 'Genres', genresHint: 'Any of the checked ones.', maxAudience: 'No stricter than', diff --git a/frontend/src/shared/lib/locales/ru.ts b/frontend/src/shared/lib/locales/ru.ts index 17de4f5..7611924 100644 --- a/frontend/src/shared/lib/locales/ru.ts +++ b/frontend/src/shared/lib/locales/ru.ts @@ -147,6 +147,8 @@ export const ru = { hint: 'В явном списке правило только ищет кандидатов; в группе «по правилу» оно и есть состав.', elementKinds: 'Что искать', showKinds: 'Тип шоу', + showKindsHint: + 'Не отмечено — кино и сериалы. Ролики живут отдельной библиотекой и попадают в набор, только если отметить их явно.', genres: 'Жанры', genresHint: 'Любой из отмеченных.', maxAudience: 'Возраст не строже',