Refactor various components to improve code clarity and maintainability. Update ListUsersQueryHandler to utilize UserListFilter for parameter handling. Refactor BumperSpecFactory and related classes to encapsulate input parameters into dedicated records, enhancing readability. Adjust MediaAsset and Slot classes to streamline content updates with new content models. Improve BumperRenderBackgroundService and MediaProcessingBackgroundService to use MediaReadyInfo for asset readiness, ensuring consistent parameter management across the application.
ci / build-backend (push) Successful in 1m55s
ci / build-frontend (push) Successful in 45s
ci / tests (push) Successful in 2m33s
ci / sonar (push) Successful in 6m28s

This commit is contained in:
Leonid Pershin
2026-07-27 00:56:04 +03:00
parent 419aff54fa
commit 19fa23b619
47 changed files with 556 additions and 423 deletions
@@ -12,13 +12,15 @@ public sealed class ListUsersQueryHandler(IIdentityService identityService)
CancellationToken cancellationToken
) =>
identityService.ListUsersAsync(
query.Page,
query.PageSize,
query.Search,
query.RoleId,
query.IsBlocked,
query.Sort,
query.Desc,
new UserListFilter(
query.Page,
query.PageSize,
query.Search,
query.RoleId,
query.IsBlocked,
query.Sort,
query.Desc
),
cancellationToken
);
}
@@ -15,11 +15,7 @@ public static class BumperSpecFactory
BumperTemplate template,
BumperTextVariant variant,
int alignedDurationSeconds,
string fromName,
string toName,
string? audioPath,
string? posterAbsolutePath,
string? backgroundAbsolutePath
BumperSpecInputs inputs
)
{
var free = variant.Kind == BumperTextKind.Free;
@@ -33,12 +29,12 @@ public static class BumperSpecFactory
template.TextColor,
font == BumperFont.Serif ? bumper.FontFileSerif : bumper.FontFileSans,
free ? "" : variant.NowLabel,
free ? "" : fromName,
free ? "" : inputs.FromName,
free ? "" : variant.NextLabel,
free ? "" : toName,
backgroundAbsolutePath,
audioPath,
posterAbsolutePath,
free ? "" : inputs.ToName,
inputs.BackgroundAbsolutePath,
inputs.AudioPath,
inputs.PosterAbsolutePath,
free,
variant.Line1,
variant.Line2
@@ -0,0 +1,15 @@
namespace TeleWave.Application.Broadcast.Bumpers;
/// <summary>
/// Уже разрешённые входы рендера заставки: названия шоу «из/в» и пути к файлам. Разрешает их
/// вызывающий (генератор эфира — по реальной паре соседей, превью — по образцам канала), а
/// <see cref="BumperSpecFactory"/> только раскладывает их по спецификации.
/// </summary>
public sealed record BumperSpecInputs(
string FromName,
string ToName,
string? AudioPath = null,
/// <summary>Постер «следующего» шоу как фон; в превью не подставляется — шоу ещё неизвестно.</summary>
string? PosterAbsolutePath = null,
string? BackgroundAbsolutePath = null
);
@@ -72,11 +72,13 @@ public sealed class BumperSpecLoader(
template,
variant,
aligned,
names.GetValueOrDefault(cache.FromShowId, "…"),
names.GetValueOrDefault(cache.ToShowId, "…"),
bumperStorage.AudioPath(template.Id, template.AudioExtension),
posterPath,
bgPath
new BumperSpecInputs(
names.GetValueOrDefault(cache.FromShowId, "…"),
names.GetValueOrDefault(cache.ToShowId, "…"),
bumperStorage.AudioPath(template.Id, template.AudioExtension),
posterPath,
bgPath
)
);
}
@@ -43,9 +43,6 @@ public sealed class RenderBumperPreviewCommandHandler(
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
var (fromName, toName) = await SampleNamesAsync(channel, cancellationToken);
var fontFile =
channel.BumperFont == BumperFont.Serif ? _bumper.FontFileSerif : _bumper.FontFileSans;
var backgroundPath = await ResolveBackgroundPathAsync(template, cancellationToken);
var seconds = template.AudioDurationSeconds is { } d and > 0
? d
@@ -55,18 +52,25 @@ public sealed class RenderBumperPreviewCommandHandler(
);
var audioPath = storage.AudioPath(template.Id, template.AudioExtension);
// Постер зависит от конкретного «следующего» шоу — в превью его не подставляем.
var inputs = new BumperSpecInputs(
fromName,
toName,
audioPath,
PosterAbsolutePath: null,
backgroundPath
);
// Рендерим каждый подблок в свой ассет-превью (id по подблоку).
foreach (var variant in template.Variants.OrderBy(v => v.Position))
{
var spec = BuildSpec(
variant,
var spec = BumperSpecFactory.Build(
_bumper,
channel.BumperFont,
template,
variant,
aligned,
fontFile,
backgroundPath,
audioPath,
fromName,
toName
inputs
);
await renderer.RenderAsync(BumperPreview.AssetId(variant.Id), spec, cancellationToken);
}
@@ -92,45 +96,6 @@ public sealed class RenderBumperPreviewCommandHandler(
return extension is null ? null : imageStore.ResolvePath(imageId, extension);
}
/// <summary>
/// Спецификация рендера одного подблока. В режиме <see cref="BumperTextKind.Free"/> подписи и
/// названия шоу гасятся: там на экране произвольные строки, а не «Сейчас/Далее».
/// </summary>
private BumperRenderSpec BuildSpec(
BumperTextVariant variant,
BumperTemplate template,
int alignedSeconds,
string fontFile,
string? backgroundPath,
string? audioPath,
string fromName,
string toName
)
{
var free = variant.Kind == BumperTextKind.Free;
return new BumperRenderSpec(
alignedSeconds,
_bumper.Width,
_bumper.Height,
template.BackgroundColor,
template.BackgroundColor2,
template.AccentColor,
template.TextColor,
fontFile,
free ? "" : variant.NowLabel,
free ? "" : fromName,
free ? "" : variant.NextLabel,
free ? "" : toName,
backgroundPath,
audioPath,
// Постер зависит от конкретного «следующего» шоу — в превью не подставляем.
null,
free,
variant.Line1,
variant.Line2
);
}
/// <summary>
/// Примерные названия «из/в» для превью. Берём шоу из групп, на которые ссылаются слоты канала:
/// так превью показывает реальные названия этого канала, а не случайные из библиотеки.
@@ -2,6 +2,7 @@ using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Broadcast;
namespace TeleWave.Application.Broadcast.Bumpers;
@@ -31,11 +32,13 @@ public sealed class UpdateBumperTextVariantCommandHandler(IAppDbContext dbContex
variant.Update(
command.Name.Trim(),
command.Kind,
command.NowLabel,
command.NextLabel,
command.Line1,
command.Line2,
new BumperTextContent(
command.Kind,
command.NowLabel,
command.NextLabel,
command.Line1,
command.Line2
),
command.Trigger,
command.Weight
);
@@ -57,13 +57,7 @@ public interface IIdentityService
Task<Result> UnblockUserAsync(Guid userId, CancellationToken cancellationToken);
Task<PagedList<UserSummaryDto>> ListUsersAsync(
int page,
int pageSize,
string? search,
Guid? roleId,
bool? isBlocked,
string? sort,
bool desc,
UserListFilter filter,
CancellationToken cancellationToken
);
}
@@ -0,0 +1,16 @@
namespace TeleWave.Application.Common.Interfaces;
/// <summary>
/// Параметры выборки пользователей: страница, фильтры и сортировка. Отдельный тип, а не запрос
/// CQRS: список пользователей живёт в Identity (вне <c>IAppDbContext</c>), и порт не должен зависеть
/// от конкретной фичи.
/// </summary>
public sealed record UserListFilter(
int Page,
int PageSize,
string? Search,
Guid? RoleId,
bool? IsBlocked,
string? Sort = null,
bool Desc = false
);
@@ -39,16 +39,19 @@ public sealed class ListInterstitialBlocksQueryHandler(IAppDbContext dbContext)
.Select(a => new { a.Id, a.Duration })
.ToDictionaryAsync(a => a.Id, a => a.Duration!.Value.TotalSeconds, cancellationToken);
// Длительность шоу-ролика считаем один раз: одно и то же шоу встречается в нескольких блоках.
var clipSeconds = clips.ToDictionary(
pair => pair.Key,
pair => pair.Value.Sum((Guid assetId) => durations.GetValueOrDefault(assetId, 0d))
);
return collections
.Where(c => c.Items.Count > 0 && c.Items.All(i => clips.ContainsKey(i.ShowId)))
.Select(c => new InterstitialBlockDto(
c.Id,
c.Name,
c.Items.Count,
c.Items.Sum(i =>
clips[i.ShowId]
.Sum(assetId => durations.TryGetValue(assetId, out var d) ? d : 0)
)
c.Items.Sum(i => clipSeconds[i.ShowId])
))
.ToList();
}
@@ -25,7 +25,11 @@ public sealed class ListMediaAssetsQueryHandler(IAppDbContext dbContext)
// ToLower().Contains переводится в LIKE lower(...) — регистронезависимо и без привязки к
// Npgsql-специфичному ILike (Application не ссылается на провайдер).
var term = query.Search.Trim().ToLower();
// CA1862 (Contains со StringComparison) здесь неприменим: это дерево выражений EF, а
// перегрузку с StringComparison провайдер в SQL не переводит — будет исключение в рантайме.
#pragma warning disable CA1862
q = q.Where(x => x.OriginalFileName.ToLower().Contains(term));
#pragma warning restore CA1862
}
var total = await q.CountAsync(cancellationToken);
@@ -142,13 +142,15 @@ public sealed class GridScheduleGenerator(
ToEntryKind(item.Kind),
item.StartsAtUtc,
item.EndsAtUtc,
item.ShowId,
item.UnitIndex,
item.SlotId,
item.Trace is null
? null
: JsonSerializer.Serialize(item.Trace, TraceJsonOptions),
item.CollectionId
new ScheduleEntryOrigin(
item.ShowId,
item.UnitIndex,
item.SlotId,
item.Trace is null
? null
: JsonSerializer.Serialize(item.Trace, TraceJsonOptions),
item.CollectionId
)
)
);
added++;
@@ -208,16 +208,18 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
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)
new SlotContent(
slot.Title,
slot.SlotKind,
slot.GroupId,
slot.StrategyJson,
slot.RepeatSourceJson,
slot.BlockMode,
slot.BlockValue,
slot.OverflowPolicy,
Map(slot.JunctionBetweenId, junctionMap),
Map(slot.JunctionAfterId, junctionMap)
)
);
}
@@ -66,16 +66,18 @@ public sealed class SlotWriter(IAppDbContext dbContext)
input.SnapToMinutes
);
target.UpdateContent(
input.Title,
input.SlotKind,
input.GroupId,
input.Strategy?.ToJson(),
input.RepeatSource?.ToJson(),
input.BlockMode,
input.BlockValue,
input.OverflowPolicy,
input.JunctionBetweenId,
input.JunctionAfterId
new SlotContent(
input.Title,
input.SlotKind,
input.GroupId,
input.Strategy?.ToJson(),
input.RepeatSource?.ToJson(),
input.BlockMode,
input.BlockValue,
input.OverflowPolicy,
input.JunctionBetweenId,
input.JunctionAfterId
)
);
return Result.Success();