namespace TeleWave.Domain.Programming; /// /// Группа контента — «что может попасть в эфир». Слот планировщика ссылается на группу, стратегия /// выбирает из неё элемент. Группы общие для всех каналов и переиспользуются. /// /// Состав — всегда явный список (), а — лишь правило /// быстрого набора: «применить фильтр» добавляет найденное в список, дальше состав правится руками. /// Живой запрос при генерации был бы проще в коде, но тогда добавление одного шоу в библиотеку /// перетасовывало бы всё будущее расписание всех каналов. /// public class Group { private readonly List _items = new(); public Guid Id { get; private set; } public string Name { get; private set; } = string.Empty; public string? Description { get; private set; } /// Правило набора (JSON) или null. Домен его не интерпретирует — схема живёт в Application. public string? FilterJson { get; private set; } // ── Кэш статистики: считается при правке состава и фоново, нужен UI («342 позиции · 118 ч») ── /// Число позиций в группе. public int ItemCount { get; private set; } /// Число единиц воспроизведения: у сериала и коллекции их больше одной. public int UnitCount { get; private set; } /// Суммарная длительность готовых единиц. public TimeSpan TotalDuration { get; private set; } public DateTimeOffset? StatsComputedAt { get; private set; } public DateTimeOffset CreatedAt { get; private set; } /// Позиции группы (backing-field для EF); порядок — по . public IReadOnlyList 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; } /// Сохранить правило набора (null — снять). Состав при этом не меняется. public void SetFilter(string? filterJson) => FilterJson = string.IsNullOrWhiteSpace(filterJson) ? null : filterJson; /// Записать пересчитанную статистику. 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); /// Добавляет элемент в конец. Повторное добавление игнорируется — возвращает null. 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; } /// Убирает все позиции, ссылающиеся на элемент. Вызывается при удалении шоу/коллекции /// из библиотеки — внешнего ключа на полиморфную ссылку нет. Возвращает число снятых позиций. 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; } /// /// Переставляет позиции в порядке переданных идентификаторов. Не упомянутые остаются после них, /// сохраняя относительный порядок, а неизвестные игнорируются — так перетаскивание одного /// элемента не теряет остальные. /// public void Reorder(IEnumerable 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++); } }