Normalize line endings to LF via .gitattributes

Репозиторий хранил фронтенд в CRLF, а часть бэкенда — вперемешку, хотя CI и Docker-сборка
работают под Linux. Прибиваем LF атрибутом `* text=auto eol=lf` и разово нормализуем дерево,
чтобы форматтеры не переписывали файлы целиком на каждом прогоне.

Коммит чисто механический: изменений содержимого нет, только концы строк.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-07-27 01:37:33 +03:00
co-authored by Claude Opus 5
parent 9d1c6d2fc3
commit 0442056367
109 changed files with 13469 additions and 13459 deletions
@@ -1,75 +1,75 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Programming.Templates;
/// <summary>
/// Окно детского времени: с <paramref name="From"/> до <paramref name="To"/> во времени канала
/// в эфир идёт только контент не строже <paramref name="MaxAudience"/>. Окно может переходить
/// через полночь (23:00–06:00) — тогда границы сравниваются в обратную сторону.
/// </summary>
public sealed record AudienceWindow(TimeOnly From, TimeOnly To, ShowAudience MaxAudience)
{
public bool Contains(TimeOnly moment) =>
From <= To ? moment >= From && moment < To : moment >= From || moment < To;
}
/// <summary>
/// Правила отбора кандидатов на уровне канала (см. 3.8). Это жёсткие фильтры: они отсекают
/// недопустимое до жребия, поэтому не требуют пересборки и не ломают воспроизводимость.
///
/// Область действия — весь канал: дейпарты сюда не заведены намеренно, окна детского времени
/// и так задаются временем, а потолок повторов осмыслен только целиком по каналу.
/// </summary>
public sealed record PlanningRules(
IReadOnlyList<AudienceWindow>? MaxAudienceByTime = null,
/// <summary>Не чаще <c>Max</c> раз за <c>WindowDays</c> суток (null — без ограничения).</summary>
RepeatLimitRule? MaxRepeatsInWindow = null,
// ── Пост-проверки: считаются по готовой ленте, дают предупреждения и ничего не переигрывают. ──
/// <summary>Потолок врезок в часе, минуты (null — не проверять).</summary>
int? MaxBreakMinutesPerHour = null,
/// <summary>Потолок доли одного жанра за вещательные сутки, проценты (null — не проверять).</summary>
int? MaxGenreSharePercent = null,
/// <summary>Потолок доли эфира, отданной фону, проценты (null — не проверять).</summary>
int? MaxFallbackSharePercent = null
)
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
/// <summary>Возрастной потолок в указанный момент времени канала (null — без ограничения).</summary>
public ShowAudience? AudienceAt(TimeOnly moment)
{
if (MaxAudienceByTime is not { Count: > 0 } windows)
return null;
// Пересекающиеся окна разрешаются в пользу строгого: детское время не должно
// отменяться более широким окном, случайно наложенным сверху.
var applicable = windows.Where(w => w.Contains(moment)).Select(w => w.MaxAudience).ToList();
return applicable.Count > 0 ? applicable.Min() : null;
}
public string ToJson() => JsonSerializer.Serialize(this, Options);
public static PlanningRules? FromJson(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
try
{
return JsonSerializer.Deserialize<PlanningRules>(json, Options);
}
catch (JsonException)
{
return null;
}
}
}
public sealed record RepeatLimitRule(int WindowDays, int Max);
using System.Text.Json;
using System.Text.Json.Serialization;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Programming.Templates;
/// <summary>
/// Окно детского времени: с <paramref name="From"/> до <paramref name="To"/> во времени канала
/// в эфир идёт только контент не строже <paramref name="MaxAudience"/>. Окно может переходить
/// через полночь (23:00–06:00) — тогда границы сравниваются в обратную сторону.
/// </summary>
public sealed record AudienceWindow(TimeOnly From, TimeOnly To, ShowAudience MaxAudience)
{
public bool Contains(TimeOnly moment) =>
From <= To ? moment >= From && moment < To : moment >= From || moment < To;
}
/// <summary>
/// Правила отбора кандидатов на уровне канала (см. 3.8). Это жёсткие фильтры: они отсекают
/// недопустимое до жребия, поэтому не требуют пересборки и не ломают воспроизводимость.
///
/// Область действия — весь канал: дейпарты сюда не заведены намеренно, окна детского времени
/// и так задаются временем, а потолок повторов осмыслен только целиком по каналу.
/// </summary>
public sealed record PlanningRules(
IReadOnlyList<AudienceWindow>? MaxAudienceByTime = null,
/// <summary>Не чаще <c>Max</c> раз за <c>WindowDays</c> суток (null — без ограничения).</summary>
RepeatLimitRule? MaxRepeatsInWindow = null,
// ── Пост-проверки: считаются по готовой ленте, дают предупреждения и ничего не переигрывают. ──
/// <summary>Потолок врезок в часе, минуты (null — не проверять).</summary>
int? MaxBreakMinutesPerHour = null,
/// <summary>Потолок доли одного жанра за вещательные сутки, проценты (null — не проверять).</summary>
int? MaxGenreSharePercent = null,
/// <summary>Потолок доли эфира, отданной фону, проценты (null — не проверять).</summary>
int? MaxFallbackSharePercent = null
)
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
/// <summary>Возрастной потолок в указанный момент времени канала (null — без ограничения).</summary>
public ShowAudience? AudienceAt(TimeOnly moment)
{
if (MaxAudienceByTime is not { Count: > 0 } windows)
return null;
// Пересекающиеся окна разрешаются в пользу строгого: детское время не должно
// отменяться более широким окном, случайно наложенным сверху.
var applicable = windows.Where(w => w.Contains(moment)).Select(w => w.MaxAudience).ToList();
return applicable.Count > 0 ? applicable.Min() : null;
}
public string ToJson() => JsonSerializer.Serialize(this, Options);
public static PlanningRules? FromJson(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
try
{
return JsonSerializer.Deserialize<PlanningRules>(json, Options);
}
catch (JsonException)
{
return null;
}
}
}
public sealed record RepeatLimitRule(int WindowDays, int Max);