Enhance code readability and maintainability by refactoring various components. Update formatting commands in CLAUDE.md for csharpier integration. Simplify method signatures in SegmentFiles, UploadLimits, and multiple endpoint classes for improved clarity. Adjust query handlers and command handlers to streamline parameter handling and enhance consistency across the application.
ci / build-backend (push) Successful in 2m8s
ci / build-frontend (push) Failing after 32s
ci / tests (push) Skipped
ci / sonar (push) Skipped

This commit is contained in:
Leonid Pershin
2026-07-27 01:20:55 +03:00
parent 5449c05b5b
commit 9d1c6d2fc3
81 changed files with 716 additions and 461 deletions
@@ -108,11 +108,10 @@ public sealed class RenderBumperPreviewCommandHandler(
var names = await (
from slot in dbContext.Slots.AsNoTracking()
join layer in dbContext.GridLayers.AsNoTracking() on slot.LayerId equals layer.Id
join item in dbContext.GroupItems.AsNoTracking()
on slot.GroupId equals item.GroupId
join item in dbContext.GroupItems.AsNoTracking() on slot.GroupId equals item.GroupId
join show in dbContext.Shows.AsNoTracking() on item.ElementId equals show.Id
where layer.TemplateId == channel.TemplateId
&& item.ElementKind == GroupElementKind.Show
where
layer.TemplateId == channel.TemplateId && item.ElementKind == GroupElementKind.Show
select show.Name
)
.Distinct()
@@ -50,5 +50,4 @@ public static class ChannelErrors
"Channels.InvalidBumperFile",
"Недопустимый файл заставки (формат или размер)."
);
}
@@ -20,7 +20,10 @@ public sealed class ListCollectionsQueryHandler(IAppDbContext dbContext)
// Единиц воспроизведения может быть больше, чем частей: сериал внутри коллекции
// разворачивается в свои серии.
var showIds = collections.SelectMany(c => c.Items.Select(i => i.ShowId)).Distinct().ToList();
var showIds = collections
.SelectMany(c => c.Items.Select(i => i.ShowId))
.Distinct()
.ToList();
var episodeCounts = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id))
@@ -4,4 +4,5 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Library.Collections.SetCollectionPoster;
/// <summary>Привязать/снять постер коллекции (<paramref name="ImageId"/> = null — отвязать).</summary>
public sealed record SetCollectionPosterCommand(Guid CollectionId, Guid? ImageId) : ICommand<Result>;
public sealed record SetCollectionPosterCommand(Guid CollectionId, Guid? ImageId)
: ICommand<Result>;
@@ -28,7 +28,12 @@ public sealed class GetShowQueryHandler(IAppDbContext dbContext)
var genreNames = await dbContext
.Genres.AsNoTracking()
.Where(g => genreIds.Contains(g.Id))
.Select(g => new { g.Id, g.Name, g.SortOrder })
.Select(g => new
{
g.Id,
g.Name,
g.SortOrder,
})
.ToListAsync(cancellationToken);
var genreDtos = genreNames
@@ -21,15 +21,14 @@ public sealed class ListInterstitialBlocksQueryHandler(IAppDbContext dbContext)
// «Блок» — не отдельная сущность, а признак состава: коллекция целиком из роликов. Смешанные
// коллекции (франшизы) остаются на своём экране и сюда не попадают.
var showIds = collections.SelectMany(c => c.Items.Select(i => i.ShowId)).Distinct().ToList();
var showIds = collections
.SelectMany(c => c.Items.Select(i => i.ShowId))
.Distinct()
.ToList();
var clips = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id) && s.Kind == ShowKind.Interstitial)
.Select(s => new
{
s.Id,
AssetIds = s.Episodes.Select(e => e.MediaAssetId).ToList(),
})
.Select(s => new { s.Id, AssetIds = s.Episodes.Select(e => e.MediaAssetId).ToList() })
.ToDictionaryAsync(s => s.Id, s => s.AssetIds, cancellationToken);
var assetIds = clips.Values.SelectMany(ids => ids).Distinct().ToList();
@@ -22,7 +22,9 @@ public sealed class ListInterstitialsQueryHandler(IAppDbContext dbContext)
// У ролика ровно одна «серия» — берём её ассет, чтобы показать длительность и статус обработки.
var assetIds = shows
.Select(s => s.Episodes.OrderBy(e => e.Position).Select(e => e.MediaAssetId).FirstOrDefault())
.Select(s =>
s.Episodes.OrderBy(e => e.Position).Select(e => e.MediaAssetId).FirstOrDefault()
)
.Where(id => id != Guid.Empty)
.Distinct()
.ToList();
@@ -44,8 +46,7 @@ public sealed class ListInterstitialsQueryHandler(IAppDbContext dbContext)
.Episodes.OrderBy(e => e.Position)
.Select(e => (Guid?)e.MediaAssetId)
.FirstOrDefault();
var asset =
assetId is { } id && assets.TryGetValue(id, out var a) ? a : null;
var asset = assetId is { } id && assets.TryGetValue(id, out var a) ? a : null;
return new InterstitialDto(
s.Id,
s.Name,
@@ -78,7 +78,8 @@ public sealed class ListShowsQueryHandler(IAppDbContext dbContext)
s.Year,
s.PosterImageId is not null,
s.CreatedAt,
s.PrimaryGenreId is { } primaryId && genreNames.TryGetValue(primaryId, out var g)
s.PrimaryGenreId is { } primaryId
&& genreNames.TryGetValue(primaryId, out var g)
? g
: null
);
@@ -28,8 +28,7 @@ public sealed record ImportManualInboxResultDto(
public sealed record ImportFailureDto(string RelativePath, string Reason);
public sealed class ImportManualInboxCommandValidator
: AbstractValidator<ImportManualInboxCommand>
public sealed class ImportManualInboxCommandValidator : AbstractValidator<ImportManualInboxCommand>
{
public ImportManualInboxCommandValidator()
{
@@ -6,10 +6,8 @@ using TeleWave.Domain.Programming;
namespace TeleWave.Application.Programming.Groups.AddGroupElements;
public sealed class AddGroupElementsCommandHandler(
IAppDbContext dbContext,
GroupStatsService stats
) : ICommandHandler<AddGroupElementsCommand, Result<int>>
public sealed class AddGroupElementsCommandHandler(IAppDbContext dbContext, GroupStatsService stats)
: ICommandHandler<AddGroupElementsCommand, Result<int>>
{
public async Task<Result<int>> Handle(
AddGroupElementsCommand command,
@@ -8,7 +8,10 @@ namespace TeleWave.Application.Programming.Groups.CreateGroup;
public sealed class CreateGroupCommandHandler(IAppDbContext dbContext)
: ICommandHandler<CreateGroupCommand, Result<Guid>>
{
public Task<Result<Guid>> Handle(CreateGroupCommand command, CancellationToken cancellationToken)
public Task<Result<Guid>> Handle(
CreateGroupCommand command,
CancellationToken cancellationToken
)
{
var group = Group.Create(command.Name, command.Description);
dbContext.Groups.Add(group);
@@ -8,7 +8,10 @@ namespace TeleWave.Application.Programming.Groups.DeleteGroup;
public sealed class DeleteGroupCommandHandler(IAppDbContext dbContext)
: ICommandHandler<DeleteGroupCommand, Result>
{
public async Task<Result> Handle(DeleteGroupCommand command, CancellationToken cancellationToken)
public async Task<Result> Handle(
DeleteGroupCommand command,
CancellationToken cancellationToken
)
{
var group = await dbContext.Groups.FirstOrDefaultAsync(
g => g.Id == command.GroupId,
@@ -58,9 +58,7 @@ public sealed class FindGroupCandidatesQueryHandler(
}
var info = await resolver.ResolveAsync(elements, cancellationToken);
var inGroup = group
.Items.Select(i => (i.ElementKind, i.ElementId))
.ToHashSet();
var inGroup = group.Items.Select(i => (i.ElementKind, i.ElementId)).ToHashSet();
var result = new List<GroupCandidateDto>();
foreach (var (kind, id) in elements)
@@ -36,7 +36,8 @@ public sealed class GetGroupQueryHandler(IAppDbContext dbContext, GroupElementRe
i.ElementId,
// Элемент мог исчезнуть из библиотеки между чисткой и чтением — показываем прочерк,
// а не роняем весь экран группы.
element?.Name ?? "—",
element?.Name
?? "—",
i.Weight,
i.Position,
element?.UnitCount ?? 0,
@@ -27,7 +27,9 @@ public sealed record GroupElementInfo(
/// </summary>
public sealed class GroupElementResolver(IAppDbContext dbContext)
{
public async Task<IReadOnlyDictionary<(GroupElementKind Kind, Guid Id), GroupElementInfo>> ResolveAsync(
public async Task<
IReadOnlyDictionary<(GroupElementKind Kind, Guid Id), GroupElementInfo>
> ResolveAsync(
IEnumerable<(GroupElementKind Kind, Guid Id)> elements,
CancellationToken cancellationToken
)
@@ -126,7 +128,9 @@ public sealed class GroupElementResolver(IAppDbContext dbContext)
DurationOf(partAssets),
null,
// Рейтинг коллекции — самый строгий среди частей: по нему отбирают в детское время.
parts.Count == 0 ? null : parts.Max(p => p!.Audience),
parts.Count == 0
? null
: parts.Max(p => p!.Audience),
parts.Count == 0 ? null : parts.Min(p => p!.Year),
collection.PosterImageId
);
@@ -8,7 +8,10 @@ namespace TeleWave.Application.Programming.Groups.UpdateGroup;
public sealed class UpdateGroupCommandHandler(IAppDbContext dbContext)
: ICommandHandler<UpdateGroupCommand, Result>
{
public async Task<Result> Handle(UpdateGroupCommand command, CancellationToken cancellationToken)
public async Task<Result> Handle(
UpdateGroupCommand command,
CancellationToken cancellationToken
)
{
var group = await dbContext.Groups.FirstOrDefaultAsync(
g => g.Id == command.GroupId,
@@ -13,10 +13,18 @@ public sealed class UpdateGroupCommandValidator : AbstractValidator<UpdateGroupC
x => x.Filter is not null,
() =>
{
RuleFor(x => x.Filter!.YearMin).InclusiveBetween(1870, 2200).When(x => x.Filter!.YearMin is not null);
RuleFor(x => x.Filter!.YearMax).InclusiveBetween(1870, 2200).When(x => x.Filter!.YearMax is not null);
RuleFor(x => x.Filter!.UnitMinutesMin).GreaterThanOrEqualTo(0).When(x => x.Filter!.UnitMinutesMin is not null);
RuleFor(x => x.Filter!.UnitMinutesMax).GreaterThanOrEqualTo(0).When(x => x.Filter!.UnitMinutesMax is not null);
RuleFor(x => x.Filter!.YearMin)
.InclusiveBetween(1870, 2200)
.When(x => x.Filter!.YearMin is not null);
RuleFor(x => x.Filter!.YearMax)
.InclusiveBetween(1870, 2200)
.When(x => x.Filter!.YearMax is not null);
RuleFor(x => x.Filter!.UnitMinutesMin)
.GreaterThanOrEqualTo(0)
.When(x => x.Filter!.UnitMinutesMin is not null);
RuleFor(x => x.Filter!.UnitMinutesMax)
.GreaterThanOrEqualTo(0)
.When(x => x.Filter!.UnitMinutesMax is not null);
}
);
}
@@ -12,9 +12,6 @@ namespace TeleWave.Application.Programming.Planning.ApplyTemplate;
public sealed record ApplyChannelTemplateCommand(Guid ChannelId) : ICommand<Result<ApplyResultDto>>;
/// <summary>Итог применения: сколько записей получилось и что стоит показать админу.</summary>
public sealed record ApplyResultDto(
int Added,
IReadOnlyList<PlanningWarningDto> Warnings
);
public sealed record ApplyResultDto(int Added, IReadOnlyList<PlanningWarningDto> Warnings);
public sealed record PlanningWarningDto(PlanningWarningKind Kind, Guid? SlotId, string Details);
@@ -11,7 +11,12 @@ using TeleWave.Domain.Programming.Planning;
namespace TeleWave.Application.Programming.Planning;
/// <summary>Ключ отрендеренной заставки: блок, подблок и пара шоу, между которыми она стоит.</summary>
public readonly record struct BumperKey(Guid TemplateId, Guid VariantId, Guid FromShowId, Guid ToShowId);
public readonly record struct BumperKey(
Guid TemplateId,
Guid VariantId,
Guid FromShowId,
Guid ToShowId
);
/// <summary>
/// Подставляет ассеты заставкам, которые планировщик зарезервировал. Резерв и рендер разделены
@@ -80,7 +80,10 @@ public sealed class PreviewApplyDiffQueryHandler(
private sealed record Described(DateTimeOffset StartsAtUtc, string Label);
private static Described Describe(ScheduleEntry entry, IReadOnlyDictionary<Guid, string> names) =>
private static Described Describe(
ScheduleEntry entry,
IReadOnlyDictionary<Guid, string> names
) =>
new(
entry.StartsAtUtc,
entry.Kind switch
@@ -89,7 +89,13 @@ public static class EffectiveGridBuilder
if (taken.Any(t => from < t.To && t.From < to))
continue;
result.Add(new ScheduledSlot(slot, date, ToUtc(date, slot.TargetStart, offset, dayStartTime)));
result.Add(
new ScheduledSlot(
slot,
date,
ToUtc(date, slot.TargetStart, offset, dayStartTime)
)
);
added.Add((from, to));
}
@@ -132,7 +132,10 @@ public sealed class GridScheduleGenerator(
foreach (var item in result.Items)
{
var assetId = item.MediaAssetId;
if (item.Kind == PlannedItemKind.Bumper && !TryResolveBumper(item, bumperAssets, out assetId))
if (
item.Kind == PlannedItemKind.Bumper
&& !TryResolveBumper(item, bumperAssets, out assetId)
)
continue; // Без ассета запись стала бы дырой в ленте.
dbContext.ScheduleEntries.Add(
@@ -207,14 +210,21 @@ public sealed class GridScheduleGenerator(
cancellationToken
);
return result with { Warnings = [.. result.Warnings, .. postWarnings] };
return result with
{
Warnings = [.. result.Warnings, .. postWarnings],
};
}
/// <summary>
/// Чистит прошлое сверх окна хранения. Окно должно покрывать самое долгое остывание среди правил —
/// история показов берётся из самой ленты, отдельного журнала нет.
/// </summary>
private Task CleanupAsync(Guid channelId, DateTimeOffset now, CancellationToken cancellationToken)
private Task CleanupAsync(
Guid channelId,
DateTimeOffset now,
CancellationToken cancellationToken
)
{
var cutoff = now.AddDays(-Math.Max(1, _options.RetentionDays));
return dbContext
@@ -326,7 +336,11 @@ public sealed class GridScheduleGenerator(
var strategy = ToPlanningStrategy(SlotStrategy.FromJson(slot.StrategyJson));
var cursor = states.TryGetValue(slot.Id, out var state)
? new PlanningCursor(state.CurrentElementKind, state.CurrentElementId, state.NextUnitIndex)
? new PlanningCursor(
state.CurrentElementKind,
state.CurrentElementId,
state.NextUnitIndex
)
: null;
slots.Add(
@@ -354,7 +368,9 @@ public sealed class GridScheduleGenerator(
),
rules?.AudienceAt(
TimeOnly.FromDateTime(
item.StartUtc.ToOffset(TimeSpan.FromMinutes(channel.UtcOffsetMinutes)).DateTime
item.StartUtc.ToOffset(
TimeSpan.FromMinutes(channel.UtcOffsetMinutes)
).DateTime
)
),
repeatLimit
@@ -489,7 +505,12 @@ public sealed class GridScheduleGenerator(
var offset = TimeSpan.FromMinutes(channel.UtcOffsetMinutes);
var sourceDate = scheduled.BroadcastDate.AddDays(-Math.Max(1, source.DaysAgo));
var from = EffectiveGridBuilder.ToUtc(sourceDate, source.Time, offset, channel.DayStartTime);
var from = EffectiveGridBuilder.ToUtc(
sourceDate,
source.Time,
offset,
channel.DayStartTime
);
var to = from.AddMinutes(Math.Max(1, source.DurationMinutes));
var entries = await dbContext
@@ -115,7 +115,10 @@ public sealed class PostCheckRunner(IAppDbContext dbContext)
.Select(s => new
{
s.Id,
GenreId = s.Genres.Where(g => g.IsPrimary).Select(g => (Guid?)g.GenreId).FirstOrDefault(),
GenreId = s
.Genres.Where(g => g.IsPrimary)
.Select(g => (Guid?)g.GenreId)
.FirstOrDefault(),
})
.Where(s => s.GenreId != null)
.ToDictionaryAsync(s => s.Id, s => s.GenreId!.Value, cancellationToken);
@@ -32,14 +32,13 @@ public sealed class GetEntryTraceQueryHandler(IAppDbContext dbContext)
if (entry is null)
return Result.Failure<EntryTraceDto>(ChannelErrors.NotFound);
var showName =
entry.ShowId is { } showId
? await dbContext
.Shows.AsNoTracking()
.Where(s => s.Id == showId)
.Select(s => s.Name)
.FirstOrDefaultAsync(cancellationToken)
: null;
var showName = entry.ShowId is { } showId
? await dbContext
.Shows.AsNoTracking()
.Where(s => s.Id == showId)
.Select(s => s.Name)
.FirstOrDefaultAsync(cancellationToken)
: null;
var collectionName = entry.CollectionId is { } collectionId
? await dbContext
@@ -80,7 +79,9 @@ public sealed class GetEntryTraceQueryHandler(IAppDbContext dbContext)
// Слот мог быть удалён или изменён после генерации — трейс от этого не портится, просто
// часть подписей окажется пустой.
var slot = trace.SlotId is { } slotId
? await dbContext.Slots.AsNoTracking().FirstOrDefaultAsync(s => s.Id == slotId, cancellationToken)
? await dbContext
.Slots.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == slotId, cancellationToken)
: null;
var layer = slot is null
? null
@@ -70,7 +70,10 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
var copyTemplate = ScheduleTemplate.Create(target.Id, source.Name);
copyTemplate.SetFallbackGroup(source.FallbackGroupId);
copyTemplate.SetRules(source.RulesJson);
if (source.DefaultJunctionId is { } defaultJunction && junctionMap.TryGetValue(defaultJunction, out var mappedDefault))
if (
source.DefaultJunctionId is { } defaultJunction
&& junctionMap.TryGetValue(defaultJunction, out var mappedDefault)
)
copyTemplate.SetDefaultJunction(mappedDefault);
var (layers, slots) = CopyGrid(source, copyTemplate, junctionMap);
@@ -24,11 +24,10 @@ public sealed class CreateChannelTemplateCommandHandler(IAppDbContext dbContext)
// Шаблон мог остаться от прошлой жизни канала, потеряв ссылку на себя, — тогда просто
// возвращаем его, а не заводим второй: одна сетка на канал.
var existing = await dbContext
.ScheduleTemplates.FirstOrDefaultAsync(
t => t.ChannelId == channel.Id,
cancellationToken
);
var existing = await dbContext.ScheduleTemplates.FirstOrDefaultAsync(
t => t.ChannelId == channel.Id,
cancellationToken
);
if (existing is not null)
{
channel.SetTemplate(existing.Id);
@@ -31,9 +31,16 @@ public sealed class CreateLayerCommandHandler(IAppDbContext dbContext)
public sealed class UpdateLayerCommandHandler(IAppDbContext dbContext)
: ICommandHandler<UpdateLayerCommand, Result>
{
public async Task<Result> Handle(UpdateLayerCommand command, CancellationToken cancellationToken)
public async Task<Result> Handle(
UpdateLayerCommand command,
CancellationToken cancellationToken
)
{
var template = await LayerLoader.ByLayerAsync(dbContext, command.LayerId, cancellationToken);
var template = await LayerLoader.ByLayerAsync(
dbContext,
command.LayerId,
cancellationToken
);
var layer = template?.FindLayer(command.LayerId);
if (template is null || layer is null)
return Result.Failure(TemplateErrors.LayerNotFound);
@@ -52,9 +59,16 @@ public sealed class UpdateLayerCommandHandler(IAppDbContext dbContext)
public sealed class DeleteLayerCommandHandler(IAppDbContext dbContext)
: ICommandHandler<DeleteLayerCommand, Result>
{
public async Task<Result> Handle(DeleteLayerCommand command, CancellationToken cancellationToken)
public async Task<Result> Handle(
DeleteLayerCommand command,
CancellationToken cancellationToken
)
{
var template = await LayerLoader.ByLayerAsync(dbContext, command.LayerId, cancellationToken);
var template = await LayerLoader.ByLayerAsync(
dbContext,
command.LayerId,
cancellationToken
);
var layer = template?.FindLayer(command.LayerId);
if (template is null || layer is null)
return Result.Failure(TemplateErrors.LayerNotFound);
@@ -292,7 +292,10 @@ public sealed class ValidateTemplateQueryHandler(IAppDbContext dbContext)
// Список id собираем до запроса: проекция по материализованной коллекции внутри дерева
// выражений заставляет EF пересобирать её на каждый вызов.
var neededShowIds = showIds.Concat(partsByCollection.Select(p => p.ShowId)).Distinct().ToList();
var neededShowIds = showIds
.Concat(partsByCollection.Select(p => p.ShowId))
.Distinct()
.ToList();
var audiences = await dbContext
.Shows.AsNoTracking()