Refactor RenderBumperPreviewCommandHandler to streamline background path resolution and rendering specification. Introduce helper methods for improved readability and maintainability. Update FindMissingEpisodesQueryHandler and RefreshShowEpisodesMetadataCommandHandler to encapsulate episode number resolution logic. Enhance CopyTemplateCommandHandler by modularizing junction copying and slot handling for better clarity. Improve FfmpegMediaProcessor by separating duration and stream parsing into dedicated methods. Refactor MediaClaimingBackgroundService to simplify idle waiting logic. Update GenreSeeder to modularize alias addition logic, enhancing code organization.
This commit is contained in:
+131
-71
@@ -48,44 +48,12 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
.Select(t => new { t.Id, t.Name })
|
||||
.ToDictionaryAsync(t => t.Id, t => t.Name, cancellationToken);
|
||||
|
||||
var junctionMap = new Dictionary<Guid, Guid>();
|
||||
var droppedBumperRefs = 0;
|
||||
|
||||
foreach (var junction in sourceJunctions)
|
||||
{
|
||||
var copy = JunctionTemplate.Create(target.Id, junction.Name);
|
||||
junctionMap[junction.Id] = copy.Id;
|
||||
|
||||
foreach (var element in junction.Elements.OrderBy(e => e.Position))
|
||||
{
|
||||
var created = copy.AddElement(element.Kind);
|
||||
|
||||
Guid? bumperTemplateId = null;
|
||||
if (element.Kind == JunctionElementKind.Bumper)
|
||||
{
|
||||
if (
|
||||
element.BumperTemplateId is { } sourceId
|
||||
&& sourceBumperNames.TryGetValue(sourceId, out var name)
|
||||
&& bumperByName.TryGetValue(name, out var mapped)
|
||||
)
|
||||
bumperTemplateId = mapped;
|
||||
else
|
||||
droppedBumperRefs++;
|
||||
}
|
||||
|
||||
created.Update(
|
||||
element.Kind,
|
||||
element.GroupId,
|
||||
bumperTemplateId,
|
||||
element.AmountMode,
|
||||
element.AmountValue,
|
||||
element.IsRequired,
|
||||
element.ConditionsJson
|
||||
);
|
||||
}
|
||||
|
||||
dbContext.JunctionTemplates.Add(copy);
|
||||
}
|
||||
var (junctionMap, droppedBumperRefs) = CopyJunctions(
|
||||
sourceJunctions,
|
||||
target.Id,
|
||||
sourceBumperNames,
|
||||
bumperByName
|
||||
);
|
||||
|
||||
// Прежняя сетка приёмника заменяется целиком: слить две сетки автоматически нельзя,
|
||||
// а «добавить поверх» дало бы кашу из пересекающихся слотов.
|
||||
@@ -105,6 +73,94 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
if (source.DefaultJunctionId is { } defaultJunction && junctionMap.TryGetValue(defaultJunction, out var mappedDefault))
|
||||
copyTemplate.SetDefaultJunction(mappedDefault);
|
||||
|
||||
var (layers, slots) = CopyGrid(source, copyTemplate, junctionMap);
|
||||
|
||||
dbContext.ScheduleTemplates.Add(copyTemplate);
|
||||
target.SetTemplate(copyTemplate.Id);
|
||||
|
||||
return Result.Success(
|
||||
new CopyTemplateResultDto(layers, slots, junctionMap.Count, droppedBumperRefs)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Копирует стыки на канал-приёмник. Возвращает соответствие «стык источника → копия» (по нему
|
||||
/// потом перевешиваются ссылки слотов) и число врезок, потерявших ссылку на блок заставки.
|
||||
/// </summary>
|
||||
private (Dictionary<Guid, Guid> Map, int DroppedBumperRefs) CopyJunctions(
|
||||
IReadOnlyList<JunctionTemplate> sourceJunctions,
|
||||
Guid targetChannelId,
|
||||
IReadOnlyDictionary<Guid, string> sourceBumperNames,
|
||||
IReadOnlyDictionary<string, Guid> targetBumperByName
|
||||
)
|
||||
{
|
||||
var map = new Dictionary<Guid, Guid>();
|
||||
var dropped = 0;
|
||||
|
||||
foreach (var junction in sourceJunctions)
|
||||
{
|
||||
var copy = JunctionTemplate.Create(targetChannelId, junction.Name);
|
||||
map[junction.Id] = copy.Id;
|
||||
|
||||
foreach (var element in junction.Elements.OrderBy(e => e.Position))
|
||||
{
|
||||
var bumperTemplateId = MapBumper(
|
||||
element,
|
||||
sourceBumperNames,
|
||||
targetBumperByName,
|
||||
ref dropped
|
||||
);
|
||||
copy.AddElement(element.Kind)
|
||||
.Update(
|
||||
element.Kind,
|
||||
element.GroupId,
|
||||
bumperTemplateId,
|
||||
element.AmountMode,
|
||||
element.AmountValue,
|
||||
element.IsRequired,
|
||||
element.ConditionsJson
|
||||
);
|
||||
}
|
||||
|
||||
dbContext.JunctionTemplates.Add(copy);
|
||||
}
|
||||
|
||||
return (map, dropped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Блок заставки у приёмника, соответствующий блоку источника по имени. Заставки живут на канале
|
||||
/// и на диске, поэтому не копируются: не нашлось одноимённого — врезка остаётся без ссылки,
|
||||
/// и это попадает в отчёт.
|
||||
/// </summary>
|
||||
private static Guid? MapBumper(
|
||||
JunctionElement element,
|
||||
IReadOnlyDictionary<Guid, string> sourceBumperNames,
|
||||
IReadOnlyDictionary<string, Guid> targetBumperByName,
|
||||
ref int dropped
|
||||
)
|
||||
{
|
||||
if (element.Kind != JunctionElementKind.Bumper)
|
||||
return null;
|
||||
|
||||
if (
|
||||
element.BumperTemplateId is { } sourceId
|
||||
&& sourceBumperNames.TryGetValue(sourceId, out var name)
|
||||
&& targetBumperByName.TryGetValue(name, out var mapped)
|
||||
)
|
||||
return mapped;
|
||||
|
||||
dropped++;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Переносит слои со слотами. Возвращает, сколько слоёв (кроме фонового) и слотов скопировано.</summary>
|
||||
private static (int Layers, int Slots) CopyGrid(
|
||||
ScheduleTemplate source,
|
||||
ScheduleTemplate copyTemplate,
|
||||
IReadOnlyDictionary<Guid, Guid> junctionMap
|
||||
)
|
||||
{
|
||||
var layers = 0;
|
||||
var slots = 0;
|
||||
|
||||
@@ -120,44 +176,48 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
|
||||
foreach (var slot in layer.Slots)
|
||||
{
|
||||
var copySlot = copyLayer.AddSlot(
|
||||
slot.Title,
|
||||
slot.TargetStart,
|
||||
slot.TargetDurationMinutes,
|
||||
slot.Daypart,
|
||||
slot.SlotKind,
|
||||
slot.Weekday
|
||||
);
|
||||
copySlot.UpdateTiming(
|
||||
slot.Weekday,
|
||||
slot.TargetStart,
|
||||
slot.TargetDurationMinutes,
|
||||
slot.Daypart,
|
||||
slot.IsAnchor,
|
||||
slot.MaxDriftMinutes,
|
||||
slot.SnapToMinutes
|
||||
);
|
||||
copySlot.UpdateContent(
|
||||
slot.Title,
|
||||
slot.SlotKind,
|
||||
slot.GroupId,
|
||||
slot.StrategyJson,
|
||||
slot.RepeatSourceJson,
|
||||
slot.BlockMode,
|
||||
slot.BlockValue,
|
||||
slot.OverflowPolicy,
|
||||
Map(slot.JunctionBetweenId, junctionMap),
|
||||
Map(slot.JunctionAfterId, junctionMap)
|
||||
);
|
||||
CopySlot(slot, copyLayer, junctionMap);
|
||||
slots++;
|
||||
}
|
||||
}
|
||||
|
||||
dbContext.ScheduleTemplates.Add(copyTemplate);
|
||||
target.SetTemplate(copyTemplate.Id);
|
||||
return (layers, slots);
|
||||
}
|
||||
|
||||
return Result.Success(
|
||||
new CopyTemplateResultDto(layers, slots, junctionMap.Count, droppedBumperRefs)
|
||||
private static void CopySlot(
|
||||
Slot slot,
|
||||
GridLayer copyLayer,
|
||||
IReadOnlyDictionary<Guid, Guid> junctionMap
|
||||
)
|
||||
{
|
||||
var copySlot = copyLayer.AddSlot(
|
||||
slot.Title,
|
||||
slot.TargetStart,
|
||||
slot.TargetDurationMinutes,
|
||||
slot.Daypart,
|
||||
slot.SlotKind,
|
||||
slot.Weekday
|
||||
);
|
||||
copySlot.UpdateTiming(
|
||||
slot.Weekday,
|
||||
slot.TargetStart,
|
||||
slot.TargetDurationMinutes,
|
||||
slot.Daypart,
|
||||
slot.IsAnchor,
|
||||
slot.MaxDriftMinutes,
|
||||
slot.SnapToMinutes
|
||||
);
|
||||
copySlot.UpdateContent(
|
||||
slot.Title,
|
||||
slot.SlotKind,
|
||||
slot.GroupId,
|
||||
slot.StrategyJson,
|
||||
slot.RepeatSourceJson,
|
||||
slot.BlockMode,
|
||||
slot.BlockValue,
|
||||
slot.OverflowPolicy,
|
||||
Map(slot.JunctionBetweenId, junctionMap),
|
||||
Map(slot.JunctionAfterId, junctionMap)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+22
-15
@@ -154,24 +154,31 @@ public sealed class ValidateTemplateQueryHandler(IAppDbContext dbContext)
|
||||
{
|
||||
var slots = layer.Slots.OrderBy(s => OffsetInDay(s.TargetStart, dayStart)).ToList();
|
||||
|
||||
// Внешний цикл в фигурных скобках намеренно: без них вложенный for выглядит как соседний,
|
||||
// и «телом» внешнего цикла его читает только компилятор.
|
||||
for (var i = 0; i < slots.Count; i++)
|
||||
for (var j = i + 1; j < slots.Count; j++)
|
||||
{
|
||||
var a = slots[i];
|
||||
var b = slots[j];
|
||||
if (!SameDays(a, b))
|
||||
continue;
|
||||
for (var j = i + 1; j < slots.Count; j++)
|
||||
{
|
||||
var a = slots[i];
|
||||
var b = slots[j];
|
||||
if (!SameDays(a, b))
|
||||
continue;
|
||||
|
||||
var aFrom = OffsetInDay(a.TargetStart, dayStart);
|
||||
var bFrom = OffsetInDay(b.TargetStart, dayStart);
|
||||
if (aFrom < bFrom + b.TargetDurationMinutes && bFrom < aFrom + a.TargetDurationMinutes)
|
||||
yield return new TemplateIssueDto(
|
||||
TemplateIssueKind.SlotOverlap,
|
||||
TemplateIssueSeverity.Warning,
|
||||
layer.Id,
|
||||
b.Id,
|
||||
$"«{a.Title}» и «{b.Title}» пересекаются в слое «{layer.Name}»."
|
||||
);
|
||||
var aFrom = OffsetInDay(a.TargetStart, dayStart);
|
||||
var bFrom = OffsetInDay(b.TargetStart, dayStart);
|
||||
if (
|
||||
aFrom < bFrom + b.TargetDurationMinutes
|
||||
&& bFrom < aFrom + a.TargetDurationMinutes
|
||||
)
|
||||
yield return new TemplateIssueDto(
|
||||
TemplateIssueKind.SlotOverlap,
|
||||
TemplateIssueSeverity.Warning,
|
||||
layer.Id,
|
||||
b.Id,
|
||||
$"«{a.Title}» и «{b.Title}» пересекаются в слое «{layer.Name}»."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user