Updated the grid import/export process to allow for the inclusion of groups and collections directly from the configuration file. This enhancement ensures that missing groups and collections are created during import, improving the flexibility of the grid management system. Adjusted related classes and methods to accommodate these changes, ensuring a cohesive integration. Enhanced documentation and user prompts to clarify the new functionality and its usage.
203 lines
7.8 KiB
C#
203 lines
7.8 KiB
C#
using LiteCqrs;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using TeleWave.Application.Broadcast;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Common.Models;
|
|
using TeleWave.Application.Programming.Groups;
|
|
using TeleWave.Domain.Programming;
|
|
|
|
namespace TeleWave.Application.Programming.Templates.Transfer;
|
|
|
|
public sealed class ExportGridQueryHandler(
|
|
IAppDbContext dbContext,
|
|
DynamicGroupResolver dynamicResolver
|
|
) : IQueryHandler<ExportGridQuery, Result<GridConfig>>
|
|
{
|
|
public async Task<Result<GridConfig>> Handle(
|
|
ExportGridQuery query,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var channel = await dbContext
|
|
.Channels.AsNoTracking()
|
|
.FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken);
|
|
if (channel is null)
|
|
return Result.Failure<GridConfig>(ChannelErrors.NotFound);
|
|
|
|
var template = await dbContext
|
|
.ScheduleTemplates.AsNoTracking()
|
|
.Include(t => t.Layers)
|
|
.ThenInclude(l => l.Slots)
|
|
.AsSplitQuery()
|
|
.FirstOrDefaultAsync(t => t.ChannelId == query.ChannelId, cancellationToken);
|
|
if (template is null)
|
|
return Result.Failure<GridConfig>(ChannelErrors.TemplateNotFound);
|
|
|
|
var groups = await dbContext
|
|
.Groups.AsNoTracking()
|
|
.ToDictionaryAsync(g => g.Id, g => g.Name, cancellationToken);
|
|
var junctions = await dbContext
|
|
.JunctionTemplates.AsNoTracking()
|
|
.ToDictionaryAsync(j => j.Id, j => j.Name, cancellationToken);
|
|
|
|
var layers = template
|
|
.Layers.OrderBy(l => l.Priority)
|
|
.Select(layer => new GridConfigLayer(
|
|
layer.Name,
|
|
layer.Priority,
|
|
[
|
|
.. layer
|
|
.Slots.OrderBy(s => s.Weekday ?? -1)
|
|
.ThenBy(s => s.TargetStart)
|
|
.Select(slot => ToConfig(slot, groups, junctions)),
|
|
],
|
|
layer.IsEnabled,
|
|
layer.IsBackground,
|
|
LayerApplicability.FromJson(layer.ApplicabilityJson)
|
|
))
|
|
.ToList();
|
|
|
|
// Состав групп едет вместе с сеткой: на другой установке тех же групп нет, а слот без
|
|
// группы поставить некуда. Собираем только использованные — выгружать всю библиотеку
|
|
// ради одного канала незачем.
|
|
var used = layers
|
|
.SelectMany(l => l.Slots)
|
|
.Select(s => s.Group)
|
|
.Append(Name(template.FallbackGroupId, groups))
|
|
.OfType<string>()
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var (exportedGroups, exportedCollections) = await ExportGroupsAsync(
|
|
used,
|
|
cancellationToken
|
|
);
|
|
|
|
return Result.Success(
|
|
new GridConfig(
|
|
layers,
|
|
Channel: new GridConfigChannel(
|
|
channel.Name,
|
|
channel.DayStartTime,
|
|
channel.UtcOffsetMinutes
|
|
),
|
|
Rules: PlanningRules.FromJson(template.RulesJson),
|
|
FallbackGroup: Name(template.FallbackGroupId, groups),
|
|
DefaultJunction: Name(template.DefaultJunctionId, junctions),
|
|
Collections: exportedCollections,
|
|
Groups: exportedGroups
|
|
)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Состав использованных групп и коллекций именами. Динамическая группа выгружается снимком
|
|
/// вычисленного состава, а не правилом: правило ссылается на жанры и рейтинги этой установки,
|
|
/// и на чужой переехало бы бессмыслицей.
|
|
/// </summary>
|
|
private async Task<(
|
|
List<GridConfigGroup> Groups,
|
|
List<GridConfigCollection> Collections
|
|
)> ExportGroupsAsync(IReadOnlySet<string> used, CancellationToken cancellationToken)
|
|
{
|
|
var groups = await dbContext
|
|
.Groups.AsNoTracking()
|
|
.Include(g => g.Items)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var showNames = await dbContext
|
|
.Shows.AsNoTracking()
|
|
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
|
|
var collectionNames = await dbContext
|
|
.Collections.AsNoTracking()
|
|
.ToDictionaryAsync(c => c.Id, c => c.Name, cancellationToken);
|
|
|
|
var exported = new List<GridConfigGroup>();
|
|
var collectionIds = new HashSet<Guid>();
|
|
|
|
foreach (var group in groups.Where(g => used.Contains(g.Name.Trim())))
|
|
{
|
|
var composition = await dynamicResolver.ResolveAsync(group, cancellationToken);
|
|
|
|
var shows = composition
|
|
.Where(e => e.Kind == GroupElementKind.Show)
|
|
.Select(e => showNames.GetValueOrDefault(e.Id))
|
|
.OfType<string>()
|
|
.ToList();
|
|
|
|
var collections = new List<string>();
|
|
foreach (var element in composition.Where(e => e.Kind == GroupElementKind.Collection))
|
|
{
|
|
if (collectionNames.GetValueOrDefault(element.Id) is not { } name)
|
|
continue;
|
|
collections.Add(name);
|
|
collectionIds.Add(element.Id);
|
|
}
|
|
|
|
exported.Add(
|
|
new GridConfigGroup(
|
|
group.Name,
|
|
shows.Count > 0 ? shows : null,
|
|
collections.Count > 0 ? collections : null,
|
|
group.Description
|
|
)
|
|
);
|
|
}
|
|
|
|
var items = await dbContext
|
|
.CollectionItems.AsNoTracking()
|
|
.Where(i => collectionIds.Contains(i.CollectionId))
|
|
.OrderBy(i => i.Position)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var exportedCollections = await dbContext
|
|
.Collections.AsNoTracking()
|
|
.Where(c => collectionIds.Contains(c.Id))
|
|
.ToListAsync(cancellationToken);
|
|
|
|
return (
|
|
exported,
|
|
[
|
|
.. exportedCollections.Select(collection => new GridConfigCollection(
|
|
collection.Name,
|
|
[
|
|
.. items
|
|
.Where(i => i.CollectionId == collection.Id)
|
|
.Select(i => showNames.GetValueOrDefault(i.ShowId))
|
|
.OfType<string>(),
|
|
],
|
|
collection.Description
|
|
)),
|
|
]
|
|
);
|
|
}
|
|
|
|
private static GridConfigSlot ToConfig(
|
|
Slot slot,
|
|
IReadOnlyDictionary<Guid, string> groups,
|
|
IReadOnlyDictionary<Guid, string> junctions
|
|
) =>
|
|
new(
|
|
slot.Title,
|
|
slot.TargetStart,
|
|
slot.TargetDurationMinutes,
|
|
slot.Daypart,
|
|
slot.SlotKind,
|
|
slot.Weekday,
|
|
Name(slot.GroupId, groups),
|
|
SlotStrategy.FromJson(slot.StrategyJson),
|
|
RepeatSource.FromJson(slot.RepeatSourceJson),
|
|
slot.BlockMode,
|
|
slot.BlockValue,
|
|
slot.OverflowPolicy,
|
|
slot.IsAnchor,
|
|
slot.MaxDriftMinutes,
|
|
slot.SnapToMinutes,
|
|
Name(slot.JunctionBetweenId, junctions),
|
|
Name(slot.JunctionAfterId, junctions)
|
|
);
|
|
|
|
/// <summary>Имя по ссылке. Пропавшая ссылка выгружается пустой — файл важнее её сохранности.</summary>
|
|
private static string? Name(Guid? id, IReadOnlyDictionary<Guid, string> names) =>
|
|
id is { } value ? names.GetValueOrDefault(value) : null;
|
|
}
|