128 lines
5.9 KiB
C#
128 lines
5.9 KiB
C#
namespace TeleWave.Domain.Programming;
|
||
|
||
/// <summary>
|
||
/// Группа контента — «что может попасть в эфир». Слот планировщика ссылается на группу, стратегия
|
||
/// выбирает из неё элемент. Группы общие для всех каналов и переиспользуются.
|
||
///
|
||
/// Состав — всегда явный список (<see cref="Items"/>), а <see cref="FilterJson"/> — лишь правило
|
||
/// быстрого набора: «применить фильтр» добавляет найденное в список, дальше состав правится руками.
|
||
/// Живой запрос при генерации был бы проще в коде, но тогда добавление одного шоу в библиотеку
|
||
/// перетасовывало бы всё будущее расписание всех каналов.
|
||
/// </summary>
|
||
public class Group
|
||
{
|
||
private readonly List<GroupItem> _items = new();
|
||
|
||
public Guid Id { get; private set; }
|
||
public string Name { get; private set; } = string.Empty;
|
||
public string? Description { get; private set; }
|
||
|
||
/// <summary>Правило набора (JSON) или null. Домен его не интерпретирует — схема живёт в Application.</summary>
|
||
public string? FilterJson { get; private set; }
|
||
|
||
// ── Кэш статистики: считается при правке состава и фоново, нужен UI («342 позиции · 118 ч») ──
|
||
|
||
/// <summary>Число позиций в группе.</summary>
|
||
public int ItemCount { get; private set; }
|
||
|
||
/// <summary>Число единиц воспроизведения: у сериала и коллекции их больше одной.</summary>
|
||
public int UnitCount { get; private set; }
|
||
|
||
/// <summary>Суммарная длительность готовых единиц.</summary>
|
||
public TimeSpan TotalDuration { get; private set; }
|
||
|
||
public DateTimeOffset? StatsComputedAt { get; private set; }
|
||
|
||
public DateTimeOffset CreatedAt { get; private set; }
|
||
|
||
/// <summary>Позиции группы (backing-field для EF); порядок — по <see cref="GroupItem.Position"/>.</summary>
|
||
public IReadOnlyList<GroupItem> Items => _items;
|
||
|
||
private Group() { }
|
||
|
||
public static Group Create(string name, string? description = null) =>
|
||
new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Name = name.Trim(),
|
||
Description = description,
|
||
CreatedAt = DateTimeOffset.UtcNow,
|
||
};
|
||
|
||
public void Rename(string name, string? description)
|
||
{
|
||
Name = name.Trim();
|
||
Description = description;
|
||
}
|
||
|
||
/// <summary>Сохранить правило набора (null — снять). Состав при этом не меняется.</summary>
|
||
public void SetFilter(string? filterJson) =>
|
||
FilterJson = string.IsNullOrWhiteSpace(filterJson) ? null : filterJson;
|
||
|
||
/// <summary>Записать пересчитанную статистику.</summary>
|
||
public void UpdateStats(int itemCount, int unitCount, TimeSpan totalDuration, DateTimeOffset at)
|
||
{
|
||
ItemCount = itemCount;
|
||
UnitCount = unitCount;
|
||
TotalDuration = totalDuration;
|
||
StatsComputedAt = at;
|
||
}
|
||
|
||
public bool Contains(GroupElementKind kind, Guid elementId) =>
|
||
_items.Any(i => i.ElementKind == kind && i.ElementId == elementId);
|
||
|
||
/// <summary>Добавляет элемент в конец. Повторное добавление игнорируется — возвращает null.</summary>
|
||
public GroupItem? AddElement(GroupElementKind kind, Guid elementId)
|
||
{
|
||
if (Contains(kind, elementId))
|
||
return null;
|
||
|
||
var nextPosition = _items.Count == 0 ? 0 : _items.Max(i => i.Position) + 1;
|
||
var item = GroupItem.Create(Id, kind, elementId, nextPosition);
|
||
_items.Add(item);
|
||
return item;
|
||
}
|
||
|
||
public bool RemoveItem(Guid itemId)
|
||
{
|
||
var item = _items.FirstOrDefault(i => i.Id == itemId);
|
||
if (item is null)
|
||
return false;
|
||
_items.Remove(item);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Убирает все позиции, ссылающиеся на элемент. Вызывается при удалении шоу/коллекции
|
||
/// из библиотеки — внешнего ключа на полиморфную ссылку нет. Возвращает число снятых позиций.</summary>
|
||
public int RemoveElement(GroupElementKind kind, Guid elementId) =>
|
||
_items.RemoveAll(i => i.ElementKind == kind && i.ElementId == elementId);
|
||
|
||
public bool SetWeight(Guid itemId, int weight)
|
||
{
|
||
var item = _items.FirstOrDefault(i => i.Id == itemId);
|
||
if (item is null)
|
||
return false;
|
||
item.SetWeight(weight);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Переставляет позиции в порядке переданных идентификаторов. Не упомянутые остаются после них,
|
||
/// сохраняя относительный порядок, а неизвестные игнорируются — так перетаскивание одного
|
||
/// элемента не теряет остальные.
|
||
/// </summary>
|
||
public void Reorder(IEnumerable<Guid> itemIdsInOrder)
|
||
{
|
||
var known = _items.Select(i => i.Id).ToHashSet();
|
||
var requested = itemIdsInOrder.Distinct().Where(known.Contains).ToList();
|
||
var rest = _items
|
||
.Where(i => !requested.Contains(i.Id))
|
||
.OrderBy(i => i.Position)
|
||
.Select(i => i.Id);
|
||
|
||
var position = 0;
|
||
foreach (var itemId in requested.Concat(rest))
|
||
_items.First(i => i.Id == itemId).SetPosition(position++);
|
||
}
|
||
}
|