Add interstitial endpoints and enhance media and template functionalities: implement MapInterstitialEndpoints in Program.cs, add preview functionality for media assets in MediaEndpoints, and introduce template preview capabilities in TemplateEndpoints. Remove obsolete bumper settings from Channel and related classes to streamline configuration.
build / backend (push) Successful in 5m58s
build / frontend (push) Successful in 47s
tests / backend-tests (push) Successful in 5m57s

This commit is contained in:
Leonid Pershin
2026-07-26 14:03:53 +03:00
parent 66040a8841
commit 7b12a06d1b
53 changed files with 4076 additions and 469 deletions
@@ -1,17 +1,17 @@
namespace TeleWave.Domain.Broadcast;
/// <summary>Как выбирать блок заставки на каждом переходе между шоу.</summary>
/// <summary>
/// Как выбирать подблок заставки на переходе. Значения заданы явно: прежний вариант «по кругу»
/// (0) убран — курсора ротации в новом пайплайне нет, и он молча вырождался в случайный выбор.
/// </summary>
public enum BumperSelection
{
/// <summary>По кругу в порядке блоков (курсор <see cref="Channel.NextBumperIndex"/>).</summary>
Rotation,
/// <summary>Случайный подблок на каждом переходе (равновероятно).</summary>
Random = 1,
/// <summary>Случайный блок на каждом переходе (равновероятно).</summary>
Random,
/// <summary>Всегда первый (дефолтный) подблок.</summary>
AlwaysFirst = 2,
/// <summary>Всегда первый (дефолтный) блок.</summary>
AlwaysFirst,
/// <summary>Случайный блок с учётом веса подблока (<see cref="BumperTextVariant.Weight"/>).</summary>
WeightedRandom,
/// <summary>Случайный подблок с учётом веса (<see cref="BumperTextVariant.Weight"/>).</summary>
WeightedRandom = 3,
}
+145 -176
View File
@@ -1,176 +1,145 @@
namespace TeleWave.Domain.Broadcast;
/// <summary>
/// Канал линейного эфира. Что и когда идёт в эфире, определяет шаблон сетки (<see cref="TemplateId"/>,
/// см. <c>Domain/Programming</c>); канал хранит только собственные свойства: время, номер, аварийный
/// филлер и общие настройки заставок.
/// </summary>
public class Channel
{
private readonly List<BumperTemplate> _bumperTemplates = new();
public Guid Id { get; private set; }
public string Name { get; private set; } = string.Empty;
public string Slug { get; private set; } = string.Empty;
public bool IsEnabled { get; private set; }
/// <summary>Точка отсчёта эфирной ленты (UTC) — база для MEDIA-SEQUENCE на этапе раздачи.</summary>
public DateTimeOffset EpochUtc { get; private set; }
/// <summary>
/// Номер канала — на телевизоре канал это номер, а не карточка в сетке. Переключение по номерам
/// включается глобальным флагом настроек сайта; null — номер не задан.
/// </summary>
public int? Number { get; private set; }
/// <summary>
/// Смещение времени канала от UTC в минутах (по умолчанию 180 — московское). Фиксированный
/// оффсет, а не IANA-зона: с переводом часов сутки становятся 23- или 25-часовыми, и сетке
/// понадобилось бы отдельное правило подрезки. Появятся каналы в зонах с DST — перейдём на IANA.
/// </summary>
public int UtcOffsetMinutes { get; private set; }
/// <summary>
/// Начало вещательных суток в времени канала (по умолчанию 06:00). Ночной блок с 00:00 до 06:00
/// относится к предыдущему дню: «ночь с пятницы на субботу» — это пятница.
/// </summary>
public TimeOnly DayStartTime { get; private set; }
/// <summary>Активный шаблон сетки канала (один на канал).</summary>
public Guid? TemplateId { get; private set; }
public const int DefaultUtcOffsetMinutes = 180;
public static readonly TimeOnly DefaultDayStartTime = new(6, 0);
// ── Настройки ТВ-заставок. В срезе 2 условия показа переезжают в элементы стыка,
// здесь останется только общий для канала шрифт. ──
/// <summary>Вставлять ли ТВ-заставки на переходах между разными шоу.</summary>
public bool BumpersEnabled { get; private set; }
/// <summary>Как выбирать блок заставки на каждом переходе (по кругу/случайно/всегда первый).</summary>
public BumperSelection BumperSelection { get; private set; }
/// <summary>Курсор ротации блоков заставок.</summary>
public int NextBumperIndex { get; private set; }
public BumperFont BumperFont { get; private set; }
/// <summary>Не вставлять заставку чаще, чем раз в N минут (0 — на каждом подходящем переходе).</summary>
public int BumperMinIntervalMinutes { get; private set; }
/// <summary>Вероятность заставки на смене шоу (0..1; 1 — на каждой смене, 0 — никогда).</summary>
public double BumperShowChangeChance { get; private set; } = 1.0;
/// <summary>Вероятность заставки между блоками одного шоу (0..1; напр. 0.3 — примерно в 30% случаев).</summary>
public double BumperEpisodeChangeChance { get; private set; } = 1.0;
private const string DefaultTemplateName = "Заставка 1";
/// <summary>Ассет-заглушка на случай пустого расписания (аварийная подстраховка).</summary>
public Guid? FillerAssetId { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
/// <summary>Блоки заставок (звук+стиль); первый (Position 0) — дефолтный, порядок — по Position.</summary>
public IReadOnlyList<BumperTemplate> BumperTemplates => _bumperTemplates;
private Channel() { }
public static Channel Create(string name, string slug, DateTimeOffset epochUtc)
{
var channel = new Channel
{
Id = Guid.NewGuid(),
Name = name,
Slug = slug,
IsEnabled = true,
EpochUtc = epochUtc,
BumpersEnabled = false,
BumperSelection = BumperSelection.Rotation,
NextBumperIndex = 0,
BumperFont = BumperFont.Sans,
BumperMinIntervalMinutes = 0,
BumperShowChangeChance = 1.0,
BumperEpisodeChangeChance = 1.0,
UtcOffsetMinutes = DefaultUtcOffsetMinutes,
DayStartTime = DefaultDayStartTime,
CreatedAt = DateTimeOffset.UtcNow,
};
// На канале всегда есть дефолтный блок заставки (без звука → синтезированный джингл).
channel._bumperTemplates.Add(BumperTemplate.Create(channel.Id, 0, DefaultTemplateName));
return channel;
}
public void UpdateSettings(
string name,
bool isEnabled,
bool bumpersEnabled,
Guid? fillerAssetId
)
{
Name = name;
IsEnabled = isEnabled;
BumpersEnabled = bumpersEnabled;
FillerAssetId = fillerAssetId;
}
/// <summary>
/// Общие настройки ТВ-заставок канала: шрифт, мин. интервал, стратегия выбора подблока и
/// вероятности появления на смене шоу / между блоками одного шоу (0..1).
/// </summary>
public void UpdateBumperSettings(
BumperFont font,
int minIntervalMinutes,
BumperSelection selection,
double showChangeChance,
double episodeChangeChance
)
{
BumperFont = font;
BumperMinIntervalMinutes = Math.Max(0, minIntervalMinutes);
BumperSelection = selection;
BumperShowChangeChance = Math.Clamp(showChangeChance, 0.0, 1.0);
BumperEpisodeChangeChance = Math.Clamp(episodeChangeChance, 0.0, 1.0);
}
/// <summary>Добавить блок заставки в конец списка. Возвращает созданный блок.</summary>
public BumperTemplate AddBumperTemplate(string name)
{
var nextPosition =
_bumperTemplates.Count == 0 ? 0 : _bumperTemplates.Max(t => t.Position) + 1;
var template = BumperTemplate.Create(Id, nextPosition, name);
_bumperTemplates.Add(template);
return template;
}
public BumperTemplate? FindBumperTemplate(Guid templateId) =>
_bumperTemplates.FirstOrDefault(t => t.Id == templateId);
/// <summary>Удалить блок заставки. Дефолтный (Position 0) удалить нельзя — вернёт false.</summary>
public bool RemoveBumperTemplate(Guid templateId)
{
var template = _bumperTemplates.FirstOrDefault(t => t.Id == templateId);
if (template is null || template.IsDefault)
return false;
_bumperTemplates.Remove(template);
return true;
}
/// <summary>Планировщик двигает курсор ротации блоков заставок по мере вставки.</summary>
public void SetNextBumperIndex(int index) => NextBumperIndex = index;
/// <summary>Привязать активный шаблон сетки.</summary>
public void SetTemplate(Guid? templateId) => TemplateId = templateId;
/// <summary>
/// Настройки времени канала: номер, смещение от UTC и начало вещательных суток. Смещение
/// ограничено сутками — за пределами этого диапазона сетка потеряла бы связь с календарём.
/// </summary>
public void UpdateTimeSettings(int? number, int utcOffsetMinutes, TimeOnly dayStartTime)
{
Number = number is > 0 ? number : null;
UtcOffsetMinutes = Math.Clamp(utcOffsetMinutes, -12 * 60, 14 * 60);
DayStartTime = dayStartTime;
}
}
namespace TeleWave.Domain.Broadcast;
/// <summary>
/// Канал линейного эфира. Что и когда идёт в эфире, определяет шаблон сетки (<see cref="TemplateId"/>,
/// см. <c>Domain/Programming</c>); канал хранит только собственные свойства: время, номер, аварийный
/// филлер и общие настройки заставок.
/// </summary>
public class Channel
{
private readonly List<BumperTemplate> _bumperTemplates = new();
public Guid Id { get; private set; }
public string Name { get; private set; } = string.Empty;
public string Slug { get; private set; } = string.Empty;
public bool IsEnabled { get; private set; }
/// <summary>Точка отсчёта эфирной ленты (UTC) — база для MEDIA-SEQUENCE на этапе раздачи.</summary>
public DateTimeOffset EpochUtc { get; private set; }
/// <summary>
/// Номер канала — на телевизоре канал это номер, а не карточка в сетке. Переключение по номерам
/// включается глобальным флагом настроек сайта; null — номер не задан.
/// </summary>
public int? Number { get; private set; }
/// <summary>
/// Смещение времени канала от UTC в минутах (по умолчанию 180 — московское). Фиксированный
/// оффсет, а не IANA-зона: с переводом часов сутки становятся 23- или 25-часовыми, и сетке
/// понадобилось бы отдельное правило подрезки. Появятся каналы в зонах с DST — перейдём на IANA.
/// </summary>
public int UtcOffsetMinutes { get; private set; }
/// <summary>
/// Начало вещательных суток в времени канала (по умолчанию 06:00). Ночной блок с 00:00 до 06:00
/// относится к предыдущему дню: «ночь с пятницы на субботу» — это пятница.
/// </summary>
public TimeOnly DayStartTime { get; private set; }
/// <summary>Активный шаблон сетки канала (один на канал).</summary>
public Guid? TemplateId { get; private set; }
public const int DefaultUtcOffsetMinutes = 180;
public static readonly TimeOnly DefaultDayStartTime = new(6, 0);
// ── Настройки ТВ-заставок. Условия показа (как часто, на смене шоу или между сериями)
// живут в элементах стыка; на канале осталось только общее для всех заставок. ──
/// <summary>Вставлять ли ТВ-заставки вообще: общий выключатель канала.</summary>
public bool BumpersEnabled { get; private set; }
/// <summary>Как выбирать подблок заставки на переходе (случайно/по весам/всегда первый).</summary>
public BumperSelection BumperSelection { get; private set; }
public BumperFont BumperFont { get; private set; }
private const string DefaultTemplateName = "Заставка 1";
/// <summary>Ассет-заглушка на случай пустого расписания (аварийная подстраховка).</summary>
public Guid? FillerAssetId { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
/// <summary>Блоки заставок (звук+стиль); первый (Position 0) — дефолтный, порядок — по Position.</summary>
public IReadOnlyList<BumperTemplate> BumperTemplates => _bumperTemplates;
private Channel() { }
public static Channel Create(string name, string slug, DateTimeOffset epochUtc)
{
var channel = new Channel
{
Id = Guid.NewGuid(),
Name = name,
Slug = slug,
IsEnabled = true,
EpochUtc = epochUtc,
BumpersEnabled = false,
BumperSelection = BumperSelection.WeightedRandom,
BumperFont = BumperFont.Sans,
UtcOffsetMinutes = DefaultUtcOffsetMinutes,
DayStartTime = DefaultDayStartTime,
CreatedAt = DateTimeOffset.UtcNow,
};
// На канале всегда есть дефолтный блок заставки (без звука → синтезированный джингл).
channel._bumperTemplates.Add(BumperTemplate.Create(channel.Id, 0, DefaultTemplateName));
return channel;
}
public void UpdateSettings(
string name,
bool isEnabled,
bool bumpersEnabled,
Guid? fillerAssetId
)
{
Name = name;
IsEnabled = isEnabled;
BumpersEnabled = bumpersEnabled;
FillerAssetId = fillerAssetId;
}
/// <summary>Общие настройки ТВ-заставок канала: шрифт и стратегия выбора подблока.</summary>
public void UpdateBumperSettings(BumperFont font, BumperSelection selection)
{
BumperFont = font;
BumperSelection = selection;
}
/// <summary>Добавить блок заставки в конец списка. Возвращает созданный блок.</summary>
public BumperTemplate AddBumperTemplate(string name)
{
var nextPosition =
_bumperTemplates.Count == 0 ? 0 : _bumperTemplates.Max(t => t.Position) + 1;
var template = BumperTemplate.Create(Id, nextPosition, name);
_bumperTemplates.Add(template);
return template;
}
public BumperTemplate? FindBumperTemplate(Guid templateId) =>
_bumperTemplates.FirstOrDefault(t => t.Id == templateId);
/// <summary>Удалить блок заставки. Дефолтный (Position 0) удалить нельзя — вернёт false.</summary>
public bool RemoveBumperTemplate(Guid templateId)
{
var template = _bumperTemplates.FirstOrDefault(t => t.Id == templateId);
if (template is null || template.IsDefault)
return false;
_bumperTemplates.Remove(template);
return true;
}
/// <summary>Привязать активный шаблон сетки.</summary>
public void SetTemplate(Guid? templateId) => TemplateId = templateId;
/// <summary>
/// Настройки времени канала: номер, смещение от UTC и начало вещательных суток. Смещение
/// ограничено сутками — за пределами этого диапазона сетка потеряла бы связь с календарём.
/// </summary>
public void UpdateTimeSettings(int? number, int utcOffsetMinutes, TimeOnly dayStartTime)
{
Number = number is > 0 ? number : null;
UtcOffsetMinutes = Math.Clamp(utcOffsetMinutes, -12 * 60, 14 * 60);
DayStartTime = dayStartTime;
}
}
+4 -2
View File
@@ -109,7 +109,7 @@ public class Show
{
if (!CanAddEpisode)
throw new InvalidOperationException(
"Полнометражка (ShowKind.Single) может содержать только одну серию."
"Только сериал может содержать больше одной серии."
);
var nextPosition = _episodes.Count == 0 ? 0 : _episodes.Max(e => e.Position) + 1;
@@ -127,7 +127,9 @@ public class Show
return true;
}
public bool CanAddEpisode => Kind != ShowKind.Single || _episodes.Count == 0;
/// <summary>Несколько серий бывает только у сериала. У полнометражки и у ролика-врезки серия ровно
/// одна: ролик с тремя сериями вёл бы себя в планировщике как мини-сериал, а задуман как единица.</summary>
public bool CanAddEpisode => Kind == ShowKind.Series || _episodes.Count == 0;
/// <summary>Применить метаданные из внешнего источника. Постер (уже зарегистрирован в реестре) может быть null.</summary>
public void ApplyMetadata(