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: 'Возраст не строже',