Refactor code formatting and improve readability: standardize line breaks and indentation across various endpoint files, enhancing code clarity and maintainability. Update package version formatting in Directory.Packages.props for consistency.
This commit is contained in:
@@ -3,4 +3,5 @@ using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Broadcast.AddChannelAd;
|
||||
|
||||
public sealed record AddChannelAdCommand(Guid ChannelId, Guid MediaAssetId) : ICommand<Result<Guid>>;
|
||||
public sealed record AddChannelAdCommand(Guid ChannelId, Guid MediaAssetId)
|
||||
: ICommand<Result<Guid>>;
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class AddChannelAdCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Ads)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Ads)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure<Guid>(ChannelErrors.NotFound);
|
||||
|
||||
+6
-3
@@ -13,13 +13,16 @@ public sealed class AddChannelShowCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Shows)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Shows)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure<Guid>(ChannelErrors.NotFound);
|
||||
|
||||
var showExists = await dbContext.Shows.AnyAsync(s => s.Id == command.ShowId, cancellationToken);
|
||||
var showExists = await dbContext.Shows.AnyAsync(
|
||||
s => s.Id == command.ShowId,
|
||||
cancellationToken
|
||||
);
|
||||
if (!showExists)
|
||||
return Result.Failure<Guid>(ChannelErrors.ShowNotFound);
|
||||
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class AddBumperTemplateCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure<Guid>(ChannelErrors.NotFound);
|
||||
|
||||
+3
-3
@@ -13,9 +13,9 @@ public sealed class AddBumperTextVariantCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure<Guid>(ChannelErrors.NotFound);
|
||||
|
||||
@@ -13,8 +13,10 @@ public sealed class BumperOptions
|
||||
public int Height { get; init; } = 720;
|
||||
|
||||
/// <summary>Пути к TTF-шрифтам с кириллицей внутри контейнера (см. Dockerfile, fonts-dejavu-core).</summary>
|
||||
public string FontFileSans { get; init; } = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
|
||||
public string FontFileSerif { get; init; } = "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf";
|
||||
public string FontFileSans { get; init; } =
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
|
||||
public string FontFileSerif { get; init; } =
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf";
|
||||
|
||||
/// <summary>Версия шаблона рендера — входит в кэш-ключ заставки; меняй при правке ЛОГИКИ рендера
|
||||
/// (не оформления канала), чтобы пересобрать уже отрендеренные заставки.</summary>
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ public sealed class ClearBumperTemplateAudioCommandHandler(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class ClearBumperTemplateBackgroundCommandHandler(IAppDbContext db
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
@@ -4,4 +4,5 @@ using TeleWave.Application.Common.Models;
|
||||
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||
|
||||
/// <summary>Удалить блок заставки (кроме дефолтного) и его файлы.</summary>
|
||||
public sealed record RemoveBumperTemplateCommand(Guid ChannelId, Guid TemplateId) : ICommand<Result>;
|
||||
public sealed record RemoveBumperTemplateCommand(Guid ChannelId, Guid TemplateId)
|
||||
: ICommand<Result>;
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ public sealed class RemoveBumperTemplateCommandHandler(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+3
-3
@@ -13,9 +13,9 @@ public sealed class RemoveBumperTextVariantCommandHandler(IAppDbContext dbContex
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+21
-13
@@ -28,9 +28,10 @@ public sealed class RenderBumperPreviewQueryHandler(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels.AsNoTracking()
|
||||
var channel = await dbContext
|
||||
.Channels.AsNoTracking()
|
||||
.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.Include(c => c.Shows)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken);
|
||||
@@ -49,7 +50,8 @@ public sealed class RenderBumperPreviewQueryHandler(
|
||||
string? backgroundPath = null;
|
||||
if (template.BackgroundImageId is { } bgId)
|
||||
{
|
||||
var bgExt = await dbContext.Images.AsNoTracking()
|
||||
var bgExt = await dbContext
|
||||
.Images.AsNoTracking()
|
||||
.Where(i => i.Id == bgId)
|
||||
.Select(i => i.FileExtension)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -57,8 +59,9 @@ public sealed class RenderBumperPreviewQueryHandler(
|
||||
backgroundPath = imageStore.ResolvePath(bgId, bgExt);
|
||||
}
|
||||
|
||||
var seconds =
|
||||
template.AudioDurationSeconds is { } d and > 0 ? d : DefaultBumperDurationSeconds;
|
||||
var seconds = template.AudioDurationSeconds is { } d and > 0
|
||||
? d
|
||||
: DefaultBumperDurationSeconds;
|
||||
var aligned = (int)(
|
||||
Math.Ceiling(Math.Max(_segmentSeconds, seconds) / _segmentSeconds) * _segmentSeconds
|
||||
);
|
||||
@@ -102,14 +105,19 @@ public sealed class RenderBumperPreviewQueryHandler(
|
||||
)
|
||||
{
|
||||
var showIds = channel.Shows.Select(s => s.ShowId).Distinct().Take(2).ToList();
|
||||
var names = showIds.Count == 0
|
||||
? []
|
||||
: await dbContext.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id))
|
||||
.Select(s => s.Name)
|
||||
.Take(2)
|
||||
.ToListAsync(cancellationToken);
|
||||
var names =
|
||||
showIds.Count == 0
|
||||
? []
|
||||
: await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id))
|
||||
.Select(s => s.Name)
|
||||
.Take(2)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (names.ElementAtOrDefault(0) ?? "Первое шоу", names.ElementAtOrDefault(1) ?? "Второе шоу");
|
||||
return (
|
||||
names.ElementAtOrDefault(0) ?? "Первое шоу",
|
||||
names.ElementAtOrDefault(1) ?? "Второе шоу"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class SetBumperTemplateAudioCommandHandler(IAppDbContext dbContext
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class SetBumperTemplateBackgroundCommandHandler(IAppDbContext dbCo
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class UpdateBumperTemplateCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+3
-1
@@ -24,6 +24,8 @@ public sealed partial class UpdateBumperTemplateCommandValidator
|
||||
private static bool BeSafeColor(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && ColorRegex().IsMatch(value);
|
||||
|
||||
[GeneratedRegex(@"^((0x|#)?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?|[A-Za-z]{2,20}(@[0-9]?\.?[0-9]+)?)$")]
|
||||
[GeneratedRegex(
|
||||
@"^((0x|#)?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?|[A-Za-z]{2,20}(@[0-9]?\.?[0-9]+)?)$"
|
||||
)]
|
||||
private static partial Regex ColorRegex();
|
||||
}
|
||||
|
||||
+3
-3
@@ -13,9 +13,9 @@ public sealed class UpdateBumperTextVariantCommandHandler(IAppDbContext dbContex
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+13
-5
@@ -27,7 +27,11 @@ public sealed class CreateProgrammingOverrideCommandHandler(IAppDbContext dbCont
|
||||
)
|
||||
return Result.Failure<Guid>(ChannelErrors.InvalidOverrideWindow);
|
||||
}
|
||||
else if (command.StartsAtUtc is not { } start || command.EndsAtUtc is not { } end || end <= start)
|
||||
else if (
|
||||
command.StartsAtUtc is not { } start
|
||||
|| command.EndsAtUtc is not { } end
|
||||
|| end <= start
|
||||
)
|
||||
{
|
||||
return Result.Failure<Guid>(ChannelErrors.InvalidOverrideWindow);
|
||||
}
|
||||
@@ -35,9 +39,9 @@ public sealed class CreateProgrammingOverrideCommandHandler(IAppDbContext dbCont
|
||||
if (command.Shows.Count == 0)
|
||||
return Result.Failure<Guid>(ChannelErrors.OverrideNeedsShow);
|
||||
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Overrides)
|
||||
.ThenInclude(o => o.Shows)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Overrides)
|
||||
.ThenInclude(o => o.Shows)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure<Guid>(ChannelErrors.NotFound);
|
||||
@@ -57,7 +61,11 @@ public sealed class CreateProgrammingOverrideCommandHandler(IAppDbContext dbCont
|
||||
command.StartMinute!.Value,
|
||||
command.EndMinute!.Value
|
||||
)
|
||||
: channel.AddOverride(command.Mode, command.StartsAtUtc!.Value, command.EndsAtUtc!.Value);
|
||||
: channel.AddOverride(
|
||||
command.Mode,
|
||||
command.StartsAtUtc!.Value,
|
||||
command.EndsAtUtc!.Value
|
||||
);
|
||||
foreach (var show in command.Shows)
|
||||
ovr.AddShow(show.ShowId, show.Weight);
|
||||
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ public sealed class CreateProgrammingOverrideCommandValidator
|
||||
public CreateProgrammingOverrideCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Shows).NotEmpty();
|
||||
RuleForEach(x => x.Shows).ChildRules(s => s.RuleFor(i => i.Weight).InclusiveBetween(1, 1000));
|
||||
RuleForEach(x => x.Shows)
|
||||
.ChildRules(s => s.RuleFor(i => i.Weight).InclusiveBetween(1, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class DeleteProgrammingOverrideCommandHandler(IAppDbContext dbCont
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Overrides)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Overrides)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
@@ -13,38 +13,42 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels.AsNoTracking()
|
||||
var channel = await dbContext
|
||||
.Channels.AsNoTracking()
|
||||
.Include(c => c.Shows)
|
||||
.ThenInclude(s => s.PreferredHours)
|
||||
.ThenInclude(s => s.PreferredHours)
|
||||
.Include(c => c.Ads)
|
||||
.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.Include(c => c.Overrides)
|
||||
.ThenInclude(o => o.Shows)
|
||||
.ThenInclude(o => o.Shows)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(c => c.Id == query.Id, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure<ChannelDto>(ChannelErrors.NotFound);
|
||||
|
||||
var showIds = channel.Shows.Select(s => s.ShowId)
|
||||
var showIds = channel
|
||||
.Shows.Select(s => s.ShowId)
|
||||
.Concat(channel.Overrides.SelectMany(o => o.Shows.Select(s => s.ShowId)))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var showNames = await dbContext.Shows.AsNoTracking()
|
||||
var showNames = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id))
|
||||
.Select(s => new { s.Id, s.Name })
|
||||
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
|
||||
|
||||
var poolAssetIds = channel.Ads.Select(a => a.MediaAssetId).Distinct().ToList();
|
||||
var assetNames = await dbContext.MediaAssets.AsNoTracking()
|
||||
var assetNames = await dbContext
|
||||
.MediaAssets.AsNoTracking()
|
||||
.Where(a => poolAssetIds.Contains(a.Id))
|
||||
.Select(a => new { a.Id, a.OriginalFileName })
|
||||
.ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);
|
||||
|
||||
string ShowName(Guid id) => showNames.GetValueOrDefault(id, "(удалено)");
|
||||
|
||||
var shows = channel.Shows
|
||||
.Select(s => new ChannelShowDto(
|
||||
var shows = channel
|
||||
.Shows.Select(s => new ChannelShowDto(
|
||||
s.Id,
|
||||
s.ShowId,
|
||||
ShowName(s.ShowId),
|
||||
@@ -54,15 +58,14 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
||||
s.IsEnabled,
|
||||
s.NextEpisodeIndex,
|
||||
s.PreferredWeightMultiplier,
|
||||
s.PreferredHours
|
||||
.OrderBy(h => h.StartHour)
|
||||
s.PreferredHours.OrderBy(h => h.StartHour)
|
||||
.Select(h => new HourWindowDto(h.StartHour, h.EndHour))
|
||||
.ToList()
|
||||
))
|
||||
.ToList();
|
||||
|
||||
var ads = channel.Ads
|
||||
.OrderBy(a => a.Position)
|
||||
var ads = channel
|
||||
.Ads.OrderBy(a => a.Position)
|
||||
.Select(a => new ChannelAdDto(
|
||||
a.Id,
|
||||
a.MediaAssetId,
|
||||
@@ -71,8 +74,8 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
||||
))
|
||||
.ToList();
|
||||
|
||||
var bumperTemplates = channel.BumperTemplates
|
||||
.OrderBy(t => t.Position)
|
||||
var bumperTemplates = channel
|
||||
.BumperTemplates.OrderBy(t => t.Position)
|
||||
.Select(t => new BumperTemplateDto(
|
||||
t.Id,
|
||||
t.Position,
|
||||
@@ -85,8 +88,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
||||
t.BackgroundImageId,
|
||||
t.AudioExtension is not null,
|
||||
t.AudioDurationSeconds,
|
||||
t.Variants
|
||||
.OrderBy(v => v.Position)
|
||||
t.Variants.OrderBy(v => v.Position)
|
||||
.Select(v => new BumperTextVariantDto(
|
||||
v.Id,
|
||||
v.Position,
|
||||
@@ -103,8 +105,8 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
||||
))
|
||||
.ToList();
|
||||
|
||||
var overrides = channel.Overrides
|
||||
.OrderBy(o => o.Recurrence)
|
||||
var overrides = channel
|
||||
.Overrides.OrderBy(o => o.Recurrence)
|
||||
.ThenBy(o => o.StartsAtUtc)
|
||||
.ThenBy(o => o.DayOfWeek)
|
||||
.Select(o => new ProgrammingOverrideDto(
|
||||
@@ -116,8 +118,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
||||
o.DayOfWeek,
|
||||
o.StartMinute,
|
||||
o.EndMinute,
|
||||
o.Shows
|
||||
.Select(s => new OverrideShowDto(s.ShowId, ShowName(s.ShowId), s.Weight))
|
||||
o.Shows.Select(s => new OverrideShowDto(s.ShowId, ShowName(s.ShowId), s.Weight))
|
||||
.ToList()
|
||||
))
|
||||
.ToList();
|
||||
|
||||
+24
-11
@@ -22,7 +22,8 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
return Result.Failure<IReadOnlyList<ScheduleEntryDto>>(ChannelErrors.NotFound);
|
||||
|
||||
// Пересекающиеся с окном [from, to) записи.
|
||||
var entries = await dbContext.ScheduleEntries.AsNoTracking()
|
||||
var entries = await dbContext
|
||||
.ScheduleEntries.AsNoTracking()
|
||||
.Where(e =>
|
||||
e.ChannelId == query.ChannelId
|
||||
&& e.StartsAtUtc < query.ToUtc
|
||||
@@ -31,23 +32,32 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
.OrderBy(e => e.StartsAtUtc)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var showIds = entries.Where(e => e.ShowId != null).Select(e => e.ShowId!.Value).Distinct().ToList();
|
||||
var showNames = await dbContext.Shows.AsNoTracking()
|
||||
var showIds = entries
|
||||
.Where(e => e.ShowId != null)
|
||||
.Select(e => e.ShowId!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var showNames = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id))
|
||||
.Select(s => new { s.Id, s.Name })
|
||||
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
|
||||
|
||||
// Подблоки заставок в окне — чтобы показать в расписании, какая именно заставка и с каким текстом.
|
||||
var variantIds = entries
|
||||
.Where(e => e.Kind == Domain.Broadcast.ScheduleEntryKind.Bumper && e.BumperVariantId != null)
|
||||
.Where(e =>
|
||||
e.Kind == Domain.Broadcast.ScheduleEntryKind.Bumper && e.BumperVariantId != null
|
||||
)
|
||||
.Select(e => e.BumperVariantId!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var variants = variantIds.Count == 0
|
||||
? []
|
||||
: await dbContext.BumperTextVariants.AsNoTracking()
|
||||
.Where(v => variantIds.Contains(v.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
var variants =
|
||||
variantIds.Count == 0
|
||||
? []
|
||||
: await dbContext
|
||||
.BumperTextVariants.AsNoTracking()
|
||||
.Where(v => variantIds.Contains(v.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
var variantsById = variants.ToDictionary(v => v.Id);
|
||||
|
||||
// Имена ассетов программ — чтобы показать реальную метку S16E03 в расписании админки.
|
||||
@@ -56,7 +66,8 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
.Select(e => e.MediaAssetId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var assetNames = await dbContext.MediaAssets.AsNoTracking()
|
||||
var assetNames = await dbContext
|
||||
.MediaAssets.AsNoTracking()
|
||||
.Where(a => assetIds.Contains(a.Id))
|
||||
.Select(a => new { a.Id, a.OriginalFileName })
|
||||
.ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);
|
||||
@@ -88,7 +99,9 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
e.ShowId,
|
||||
e.ShowId is { } sid ? showNames.GetValueOrDefault(sid) : null,
|
||||
e.EpisodeIndex,
|
||||
assetNames.TryGetValue(e.MediaAssetId, out var name) ? EpisodeName.ParseLabel(name) : null,
|
||||
assetNames.TryGetValue(e.MediaAssetId, out var name)
|
||||
? EpisodeName.ParseLabel(name)
|
||||
: null,
|
||||
bumperName,
|
||||
bumperText
|
||||
)
|
||||
|
||||
@@ -12,7 +12,8 @@ public sealed class ListChannelsQueryHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
return await dbContext.Channels.AsNoTracking()
|
||||
return await dbContext
|
||||
.Channels.AsNoTracking()
|
||||
.OrderBy(c => c.Name)
|
||||
.Select(c => new ChannelSummaryDto(c.Id, c.Name, c.Slug, c.IsEnabled))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class RemoveChannelAdCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Ads)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Ads)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
+2
-1
@@ -3,4 +3,5 @@ using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Broadcast.RemoveChannelShow;
|
||||
|
||||
public sealed record RemoveChannelShowCommand(Guid ChannelId, Guid ChannelShowId) : ICommand<Result>;
|
||||
public sealed record RemoveChannelShowCommand(Guid ChannelId, Guid ChannelShowId)
|
||||
: ICommand<Result>;
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ public sealed class RemoveChannelShowCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Shows)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Shows)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
|
||||
@@ -49,14 +49,14 @@ public sealed class ScheduleGenerator(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Shows)
|
||||
.ThenInclude(s => s.PreferredHours)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Shows)
|
||||
.ThenInclude(s => s.PreferredHours)
|
||||
.Include(c => c.Ads)
|
||||
.Include(c => c.BumperTemplates)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.ThenInclude(t => t.Variants)
|
||||
.Include(c => c.Overrides)
|
||||
.ThenInclude(o => o.Shows)
|
||||
.ThenInclude(o => o.Shows)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(c => c.Id == channelId, cancellationToken);
|
||||
|
||||
@@ -67,13 +67,13 @@ public sealed class ScheduleGenerator(
|
||||
|
||||
// Чистим прошлое сверх окна ретеншна.
|
||||
var retentionCutoff = now.AddHours(-_options.RetentionHours);
|
||||
await dbContext.ScheduleEntries
|
||||
.Where(e => e.ChannelId == channelId && e.EndsAtUtc < retentionCutoff)
|
||||
await dbContext
|
||||
.ScheduleEntries.Where(e => e.ChannelId == channelId && e.EndsAtUtc < retentionCutoff)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
// Точка продолжения: конец последней сохранённой записи (для regenerate — только уже стартовавшей).
|
||||
var lastEnd = await dbContext.ScheduleEntries
|
||||
.Where(e =>
|
||||
var lastEnd = await dbContext
|
||||
.ScheduleEntries.Where(e =>
|
||||
e.ChannelId == channelId && (!regenerate || e.StartsAtUtc < now)
|
||||
)
|
||||
.MaxAsync(e => (DateTimeOffset?)e.EndsAtUtc, cancellationToken);
|
||||
@@ -83,8 +83,8 @@ public sealed class ScheduleGenerator(
|
||||
startTime = now;
|
||||
|
||||
if (regenerate)
|
||||
await dbContext.ScheduleEntries
|
||||
.Where(e => e.ChannelId == channelId && e.StartsAtUtc >= now)
|
||||
await dbContext
|
||||
.ScheduleEntries.Where(e => e.ChannelId == channelId && e.StartsAtUtc >= now)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (startTime >= horizonEnd)
|
||||
@@ -176,7 +176,9 @@ public sealed class ScheduleGenerator(
|
||||
/// Для каждой уникальной тройки «из→в→подблок» из запланированных заставок возвращает id готового
|
||||
/// ассета-заставки: из кэша (<see cref="BumperAsset"/>) либо свежесгенерированного.
|
||||
/// </summary>
|
||||
private async Task<Dictionary<(Guid From, Guid To, Guid Variant), Guid>> ResolveBumperAssetsAsync(
|
||||
private async Task<
|
||||
Dictionary<(Guid From, Guid To, Guid Variant), Guid>
|
||||
> ResolveBumperAssetsAsync(
|
||||
Channel channel,
|
||||
IReadOnlyList<PlannedEntry> entries,
|
||||
IReadOnlyDictionary<Guid, string> showNames,
|
||||
@@ -185,8 +187,8 @@ public sealed class ScheduleGenerator(
|
||||
{
|
||||
var result = new Dictionary<(Guid, Guid, Guid), Guid>();
|
||||
var templatesById = channel.BumperTemplates.ToDictionary(t => t.Id);
|
||||
var variantsById = channel.BumperTemplates
|
||||
.SelectMany(t => t.Variants.Select(v => (Variant: v, Template: t)))
|
||||
var variantsById = channel
|
||||
.BumperTemplates.SelectMany(t => t.Variants.Select(v => (Variant: v, Template: t)))
|
||||
.ToDictionary(x => x.Variant.Id);
|
||||
var combos = entries
|
||||
.Where(e =>
|
||||
@@ -195,11 +197,13 @@ public sealed class ScheduleGenerator(
|
||||
&& e.ToShowId is not null
|
||||
&& e.BumperVariantId is not null
|
||||
)
|
||||
.Select(e => (
|
||||
From: e.FromShowId!.Value,
|
||||
To: e.ToShowId!.Value,
|
||||
Variant: e.BumperVariantId!.Value
|
||||
))
|
||||
.Select(e =>
|
||||
(
|
||||
From: e.FromShowId!.Value,
|
||||
To: e.ToShowId!.Value,
|
||||
Variant: e.BumperVariantId!.Value
|
||||
)
|
||||
)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (combos.Count == 0)
|
||||
@@ -211,12 +215,14 @@ public sealed class ScheduleGenerator(
|
||||
// Постеры шоу-получателей (из реестра изображений) — как фон заставки, если у блока нет
|
||||
// своей фон-картинки. Резолвим id постера → расширение → абсолютный путь.
|
||||
var showIds = fromIds.Concat(toIds).Distinct().ToList();
|
||||
var posterShows = await dbContext.Shows.AsNoTracking()
|
||||
var posterShows = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id) && s.PosterImageId != null)
|
||||
.Select(s => new { s.Id, ImageId = s.PosterImageId!.Value })
|
||||
.ToListAsync(cancellationToken);
|
||||
var posterImageIds = posterShows.Select(p => p.ImageId).Distinct().ToList();
|
||||
var posterExtById = await dbContext.Images.AsNoTracking()
|
||||
var posterExtById = await dbContext
|
||||
.Images.AsNoTracking()
|
||||
.Where(i => posterImageIds.Contains(i.Id))
|
||||
.Select(i => new { i.Id, i.FileExtension })
|
||||
.ToDictionaryAsync(i => i.Id, i => i.FileExtension, cancellationToken);
|
||||
@@ -229,12 +235,13 @@ public sealed class ScheduleGenerator(
|
||||
posterByShow[p.Id] = (p.ImageId, abs);
|
||||
|
||||
// Фон-картинки блоков (из реестра) — абсолютные пути по id.
|
||||
var bgImageIds = channel.BumperTemplates
|
||||
.Where(t => t.BackgroundImageId != null)
|
||||
var bgImageIds = channel
|
||||
.BumperTemplates.Where(t => t.BackgroundImageId != null)
|
||||
.Select(t => t.BackgroundImageId!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var bgExtById = await dbContext.Images.AsNoTracking()
|
||||
var bgExtById = await dbContext
|
||||
.Images.AsNoTracking()
|
||||
.Where(i => bgImageIds.Contains(i.Id))
|
||||
.Select(i => new { i.Id, i.FileExtension })
|
||||
.ToDictionaryAsync(i => i.Id, i => i.FileExtension, cancellationToken);
|
||||
@@ -248,7 +255,8 @@ public sealed class ScheduleGenerator(
|
||||
bgByTemplate[t.Id] = bgAbs;
|
||||
|
||||
// Кандидаты из кэша + статусы их ассетов (годятся только Ready — файлы могли удалить).
|
||||
var cached = await dbContext.BumperAssets.AsNoTracking()
|
||||
var cached = await dbContext
|
||||
.BumperAssets.AsNoTracking()
|
||||
.Where(b => fromIds.Contains(b.FromShowId) && toIds.Contains(b.ToShowId))
|
||||
.Select(b => new
|
||||
{
|
||||
@@ -260,7 +268,8 @@ public sealed class ScheduleGenerator(
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var cachedAssetIds = cached.Select(c => c.MediaAssetId).Distinct().ToList();
|
||||
var readyAssetIds = await dbContext.MediaAssets.AsNoTracking()
|
||||
var readyAssetIds = await dbContext
|
||||
.MediaAssets.AsNoTracking()
|
||||
.Where(a => cachedAssetIds.Contains(a.Id) && a.Status == MediaAssetStatus.Ready)
|
||||
.Select(a => a.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -281,7 +290,15 @@ public sealed class ScheduleGenerator(
|
||||
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
|
||||
var bgAbs = bgByTemplate.GetValueOrDefault(template.Id);
|
||||
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template));
|
||||
var signature = ComputeSignature(channel, template, variant, fromName, toName, aligned, posterToken);
|
||||
var signature = ComputeSignature(
|
||||
channel,
|
||||
template,
|
||||
variant,
|
||||
fromName,
|
||||
toName,
|
||||
aligned,
|
||||
posterToken
|
||||
);
|
||||
|
||||
var hit = cached.FirstOrDefault(c =>
|
||||
c.FromShowId == combo.From
|
||||
@@ -370,9 +387,7 @@ public sealed class ScheduleGenerator(
|
||||
);
|
||||
|
||||
dbContext.MediaAssets.Add(asset);
|
||||
dbContext.BumperAssets.Add(
|
||||
BumperAsset.Create(fromShowId, toShowId, signature, asset.Id)
|
||||
);
|
||||
dbContext.BumperAssets.Add(BumperAsset.Create(fromShowId, toShowId, signature, asset.Id));
|
||||
return asset.Id;
|
||||
}
|
||||
|
||||
@@ -475,7 +490,8 @@ public sealed class ScheduleGenerator(
|
||||
if (showIds.Count == 0)
|
||||
return new Dictionary<Guid, string>();
|
||||
|
||||
return await dbContext.Shows.AsNoTracking()
|
||||
return await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id))
|
||||
.Select(s => new { s.Id, s.Name })
|
||||
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
|
||||
@@ -491,8 +507,8 @@ public sealed class ScheduleGenerator(
|
||||
var enabledShows = channel.Shows.Where(s => s.IsEnabled).ToList();
|
||||
var showIds = enabledShows.Select(s => s.ShowId).Distinct().ToList();
|
||||
|
||||
var shows = await dbContext.Shows
|
||||
.Include(s => s.Episodes)
|
||||
var shows = await dbContext
|
||||
.Shows.Include(s => s.Episodes)
|
||||
.Where(s => showIds.Contains(s.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -501,14 +517,14 @@ public sealed class ScheduleGenerator(
|
||||
s => s.Episodes.OrderBy(e => e.Position).Select(e => e.MediaAssetId).ToList()
|
||||
);
|
||||
|
||||
var candidateAssetIds = episodesByShow.Values
|
||||
.SelectMany(x => x)
|
||||
var candidateAssetIds = episodesByShow
|
||||
.Values.SelectMany(x => x)
|
||||
.Concat(channel.Ads.Select(a => a.MediaAssetId))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var durations = await dbContext.MediaAssets
|
||||
.Where(a =>
|
||||
var durations = await dbContext
|
||||
.MediaAssets.Where(a =>
|
||||
candidateAssetIds.Contains(a.Id)
|
||||
&& a.Status == MediaAssetStatus.Ready
|
||||
&& a.Duration != null
|
||||
@@ -534,34 +550,34 @@ public sealed class ScheduleGenerator(
|
||||
channelShow.BlockValue,
|
||||
ready,
|
||||
channelShow.NextEpisodeIndex,
|
||||
channelShow.PreferredHours
|
||||
.Select(h => new PlannerHourWindow(h.StartHour, h.EndHour))
|
||||
channelShow
|
||||
.PreferredHours.Select(h => new PlannerHourWindow(h.StartHour, h.EndHour))
|
||||
.ToList(),
|
||||
channelShow.PreferredWeightMultiplier
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var adPool = channel.Ads
|
||||
.OrderBy(a => a.Position)
|
||||
var adPool = channel
|
||||
.Ads.OrderBy(a => a.Position)
|
||||
.Select(a => a.MediaAssetId)
|
||||
.Where(durations.ContainsKey)
|
||||
.ToList();
|
||||
|
||||
// Подблоки заставок (плоский список): длительность слота — по звуку блока, выровнена на сегмент.
|
||||
var bumperVariants = channel.BumperTemplates
|
||||
.OrderBy(t => t.Position)
|
||||
var bumperVariants = channel
|
||||
.BumperTemplates.OrderBy(t => t.Position)
|
||||
.SelectMany(t =>
|
||||
{
|
||||
var dur = TimeSpan.FromSeconds(AlignedDurationSeconds(TemplateDurationSeconds(t)));
|
||||
return t.Variants
|
||||
.OrderBy(v => v.Position)
|
||||
return t
|
||||
.Variants.OrderBy(v => v.Position)
|
||||
.Select(v => new PlannerBumperVariant(v.Id, t.Id, dur, v.Trigger, v.Weight));
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var overrides = channel.Overrides
|
||||
.Select(o => new PlannerOverride(
|
||||
var overrides = channel
|
||||
.Overrides.Select(o => new PlannerOverride(
|
||||
o.Mode,
|
||||
o.Shows.Select(s => new PlannerOverrideShow(s.ShowId, s.Weight)).ToList(),
|
||||
o.Recurrence,
|
||||
|
||||
+4
-1
@@ -22,7 +22,10 @@ public sealed class UpdateChannelSettingsCommandHandler(IAppDbContext dbContext)
|
||||
|
||||
if (command.FillerAssetId is { } fillerId)
|
||||
{
|
||||
var exists = await dbContext.MediaAssets.AnyAsync(a => a.Id == fillerId, cancellationToken);
|
||||
var exists = await dbContext.MediaAssets.AnyAsync(
|
||||
a => a.Id == fillerId,
|
||||
cancellationToken
|
||||
);
|
||||
if (!exists)
|
||||
return Result.Failure(ChannelErrors.AssetNotFound);
|
||||
}
|
||||
|
||||
+9
-4
@@ -13,9 +13,9 @@ public sealed class UpdateChannelShowCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var channel = await dbContext.Channels
|
||||
.Include(c => c.Shows)
|
||||
.ThenInclude(s => s.PreferredHours)
|
||||
var channel = await dbContext
|
||||
.Channels.Include(c => c.Shows)
|
||||
.ThenInclude(s => s.PreferredHours)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
|
||||
if (channel is null)
|
||||
return Result.Failure(ChannelErrors.NotFound);
|
||||
@@ -24,7 +24,12 @@ public sealed class UpdateChannelShowCommandHandler(IAppDbContext dbContext)
|
||||
if (channelShow is null)
|
||||
return Result.Failure(ChannelErrors.ChannelShowNotFound);
|
||||
|
||||
channelShow.Update(command.Weight, command.BlockMode, command.BlockValue, command.IsEnabled);
|
||||
channelShow.Update(
|
||||
command.Weight,
|
||||
command.BlockMode,
|
||||
command.BlockValue,
|
||||
command.IsEnabled
|
||||
);
|
||||
channelShow.SetPreferredHours(
|
||||
command.PreferredWeightMultiplier,
|
||||
command.PreferredHours.Select(h => (h.StartHour, h.EndHour))
|
||||
|
||||
+9
-7
@@ -9,12 +9,14 @@ public sealed class UpdateChannelShowCommandValidator : AbstractValidator<Update
|
||||
RuleFor(x => x.Weight).InclusiveBetween(1, 1000);
|
||||
RuleFor(x => x.BlockValue).InclusiveBetween(1, 10000);
|
||||
RuleFor(x => x.PreferredWeightMultiplier).InclusiveBetween(1, 100);
|
||||
RuleForEach(x => x.PreferredHours).ChildRules(w =>
|
||||
{
|
||||
w.RuleFor(h => h.StartHour).InclusiveBetween(0, 23);
|
||||
w.RuleFor(h => h.EndHour).InclusiveBetween(1, 24);
|
||||
w.RuleFor(h => h).Must(h => h.StartHour < h.EndHour)
|
||||
.WithMessage("Начало окна должно быть раньше конца.");
|
||||
});
|
||||
RuleForEach(x => x.PreferredHours)
|
||||
.ChildRules(w =>
|
||||
{
|
||||
w.RuleFor(h => h.StartHour).InclusiveBetween(0, 23);
|
||||
w.RuleFor(h => h.EndHour).InclusiveBetween(1, 24);
|
||||
w.RuleFor(h => h)
|
||||
.Must(h => h.StartHour < h.EndHour)
|
||||
.WithMessage("Начало окна должно быть раньше конца.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user