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:
Leonid Pershin
2026-07-25 19:55:01 +03:00
parent 1f6fa6f1ae
commit f57b7503ed
122 changed files with 1856 additions and 892 deletions
@@ -7,6 +7,8 @@ namespace TeleWave.Application.Admin.Roles.ChangeUserRole;
public sealed class ChangeUserRoleCommandHandler(IRoleService roleService)
: ICommandHandler<ChangeUserRoleCommand, Result>
{
public Task<Result> Handle(ChangeUserRoleCommand command, CancellationToken cancellationToken) =>
roleService.ChangeUserRoleAsync(command.UserId, command.RoleId, cancellationToken);
public Task<Result> Handle(
ChangeUserRoleCommand command,
CancellationToken cancellationToken
) => roleService.ChangeUserRoleAsync(command.UserId, command.RoleId, cancellationToken);
}
@@ -31,7 +31,11 @@ public sealed class CreateUserCommandHandler(
var userId = createResult.Value;
// CreateUserAsync назначает роль по умолчанию — выставляем выбранную админом.
var roleResult = await roleService.ChangeUserRoleAsync(userId, command.RoleId, cancellationToken);
var roleResult = await roleService.ChangeUserRoleAsync(
userId,
command.RoleId,
cancellationToken
);
if (!roleResult.IsSuccess)
return Result.Failure<Guid>(roleResult.Error);
@@ -4,7 +4,10 @@ namespace TeleWave.Application.Admin.Users;
public static class UserErrors
{
public static readonly Error NotFound = Error.NotFound("Users.NotFound", "Пользователь не найден.");
public static readonly Error NotFound = Error.NotFound(
"Users.NotFound",
"Пользователь не найден."
);
public static readonly Error CannotDeleteSelf = Error.Forbidden(
"Users.CannotDeleteSelf",
@@ -4,8 +4,10 @@ namespace TeleWave.Application.Auth;
public static class AuthErrors
{
public static readonly Error InvalidCredentials =
Error.Unauthorized("Auth.InvalidCredentials", "Неверное имя пользователя или пароль.");
public static readonly Error InvalidCredentials = Error.Unauthorized(
"Auth.InvalidCredentials",
"Неверное имя пользователя или пароль."
);
public static readonly Error Unauthorized = Error.Unauthorized(
"Auth.Unauthorized",
@@ -3,4 +3,5 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Auth.ChangePassword;
public sealed record ChangePasswordCommand(string CurrentPassword, string NewPassword) : ICommand<Result>;
public sealed record ChangePasswordCommand(string CurrentPassword, string NewPassword)
: ICommand<Result>;
@@ -3,4 +3,5 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Auth.Register;
public sealed record RegisterCommand(string UserName, string Password) : ICommand<Result<AuthResult>>;
public sealed record RegisterCommand(string UserName, string Password)
: ICommand<Result<AuthResult>>;
@@ -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>>;
@@ -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);
@@ -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);
@@ -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);
@@ -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>
@@ -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);
@@ -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>;
@@ -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);
@@ -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);
@@ -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) ?? "Второе шоу"
);
}
}
@@ -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);
@@ -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);
@@ -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);
@@ -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();
}
@@ -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);
@@ -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);
@@ -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));
}
}
@@ -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();
@@ -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);
@@ -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);
@@ -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>;
@@ -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,
@@ -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);
}
@@ -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,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("Начало окна должно быть раньше конца.");
});
}
}
@@ -2,7 +2,13 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Common.Interfaces;
public sealed record CurrentUserProfile(Guid Id, string UserName, Guid RoleId, string Role, bool IsBlocked);
public sealed record CurrentUserProfile(
Guid Id,
string UserName,
Guid RoleId,
string Role,
bool IsBlocked
);
public sealed record UserSummaryDto(
Guid Id,
@@ -15,7 +15,11 @@ public interface IMediaStorage
/// Стримит загружаемый контент во временный файл в <c>uploads/</c> без буферизации в память.
/// Возвращает непрозрачный токен (имя временного файла) для последующего <see cref="PromoteToOriginalAsync"/>.
/// </summary>
Task<string> SaveUploadAsync(Stream content, string extension, CancellationToken cancellationToken);
Task<string> SaveUploadAsync(
Stream content,
string extension,
CancellationToken cancellationToken
);
/// <summary>Удаляет временный файл загрузки (откат при ошибке до регистрации ассета).</summary>
void DeleteUpload(string uploadToken);
@@ -29,13 +29,20 @@ public static class DependencyInjection
return services;
}
private static void RegisterClosedGeneric(IServiceCollection services, Assembly assembly, Type openInterface)
private static void RegisterClosedGeneric(
IServiceCollection services,
Assembly assembly,
Type openInterface
)
{
var implementations = assembly.GetTypes()
var implementations = assembly
.GetTypes()
.Where(t => t is { IsClass: true, IsAbstract: false })
.SelectMany(t => t.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface)
.Select(i => (Service: i, Implementation: t)));
.SelectMany(t =>
t.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface)
.Select(i => (Service: i, Implementation: t))
);
foreach (var (service, implementation) in implementations)
services.AddScoped(service, implementation);
@@ -8,7 +8,10 @@ namespace TeleWave.Application.Images.DeleteImage;
public sealed class DeleteImageCommandHandler(IAppDbContext dbContext, IImageStore storage)
: ICommandHandler<DeleteImageCommand, Result>
{
public async Task<Result> Handle(DeleteImageCommand command, CancellationToken cancellationToken)
public async Task<Result> Handle(
DeleteImageCommand command,
CancellationToken cancellationToken
)
{
var image = await dbContext.Images.FirstOrDefaultAsync(
i => i.Id == command.Id,
@@ -13,7 +13,8 @@ public sealed class GetImageFileQueryHandler(IAppDbContext dbContext, IImageStor
CancellationToken cancellationToken
)
{
var image = await dbContext.Images.AsNoTracking()
var image = await dbContext
.Images.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == query.Id, cancellationToken);
if (image is null)
return Result.Failure<string>(ImageErrors.NotFound);
@@ -13,7 +13,8 @@ public sealed class ListImagesQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken
)
{
var images = await dbContext.Images.AsNoTracking()
var images = await dbContext
.Images.AsNoTracking()
.Where(i => i.Category == query.Category)
.OrderByDescending(i => i.CreatedAt)
.Select(i => new ImageDto(i.Id, i.Category, i.OriginalFileName, i.CreatedAt))
@@ -8,7 +8,10 @@ namespace TeleWave.Application.Images.UploadImage;
public sealed class UploadImageCommandHandler(IAppDbContext dbContext)
: ICommandHandler<UploadImageCommand, Result<Guid>>
{
public Task<Result<Guid>> Handle(UploadImageCommand command, CancellationToken cancellationToken)
public Task<Result<Guid>> Handle(
UploadImageCommand command,
CancellationToken cancellationToken
)
{
var image = Image.Create(command.Category, command.Extension, command.OriginalFileName);
dbContext.Images.Add(image);
@@ -13,8 +13,8 @@ public sealed class AddEpisodeCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken
)
{
var show = await dbContext.Shows
.Include(s => s.Episodes)
var show = await dbContext
.Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null)
return Result.Failure<Guid>(ShowErrors.NotFound);
@@ -22,8 +22,8 @@ public sealed class AddEpisodeCommandHandler(IAppDbContext dbContext)
if (!show.CanAddEpisode)
return Result.Failure<Guid>(ShowErrors.SingleAlreadyHasEpisode);
var fileName = await dbContext.MediaAssets
.Where(a => a.Id == command.MediaAssetId)
var fileName = await dbContext
.MediaAssets.Where(a => a.Id == command.MediaAssetId)
.Select(a => a.OriginalFileName)
.FirstOrDefaultAsync(cancellationToken);
if (fileName is null)
@@ -10,7 +10,12 @@ public sealed class CreateShowCommandHandler(IAppDbContext dbContext)
{
public Task<Result<Guid>> Handle(CreateShowCommand command, CancellationToken cancellationToken)
{
var show = Show.Create(command.Name, command.Kind, command.Description, command.OriginalName);
var show = Show.Create(
command.Name,
command.Kind,
command.Description,
command.OriginalName
);
dbContext.Shows.Add(show);
return Task.FromResult(Result.Success(show.Id));
}
@@ -8,9 +8,13 @@ namespace TeleWave.Application.Library.GetShow;
public sealed class GetShowQueryHandler(IAppDbContext dbContext)
: IQueryHandler<GetShowQuery, Result<ShowDto>>
{
public async Task<Result<ShowDto>> Handle(GetShowQuery query, CancellationToken cancellationToken)
public async Task<Result<ShowDto>> Handle(
GetShowQuery query,
CancellationToken cancellationToken
)
{
var show = await dbContext.Shows.AsNoTracking()
var show = await dbContext
.Shows.AsNoTracking()
.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == query.Id, cancellationToken);
if (show is null)
@@ -18,7 +22,8 @@ public sealed class GetShowQueryHandler(IAppDbContext dbContext)
var episodes = show.Episodes.OrderBy(e => e.Position).ToList();
var assetIds = episodes.Select(e => e.MediaAssetId).ToList();
var assets = await dbContext.MediaAssets.AsNoTracking()
var assets = await dbContext
.MediaAssets.AsNoTracking()
.Where(a => assetIds.Contains(a.Id))
.Select(a => new
{
@@ -12,14 +12,19 @@ public sealed class ListShowsQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken
)
{
var shows = await dbContext.Shows.AsNoTracking()
var shows = await dbContext
.Shows.AsNoTracking()
.Include(s => s.Episodes)
.OrderBy(s => s.Name)
.ToListAsync(cancellationToken);
// Имена ассетов нужны, чтобы распознать сезоны (номера в модели не хранятся).
var assetIds = shows.SelectMany(s => s.Episodes.Select(e => e.MediaAssetId)).Distinct().ToList();
var names = await dbContext.MediaAssets.AsNoTracking()
var assetIds = shows
.SelectMany(s => s.Episodes.Select(e => e.MediaAssetId))
.Distinct()
.ToList();
var names = 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);
@@ -27,8 +32,12 @@ public sealed class ListShowsQueryHandler(IAppDbContext dbContext)
return shows
.Select(s =>
{
var seasons = s.Episodes
.Select(e => names.TryGetValue(e.MediaAssetId, out var n) ? EpisodeName.ParseSeason(n) : null)
var seasons = s
.Episodes.Select(e =>
names.TryGetValue(e.MediaAssetId, out var n)
? EpisodeName.ParseSeason(n)
: null
)
.Where(season => season is not null)
.Distinct()
.Count();
@@ -13,8 +13,8 @@ public sealed class RemoveEpisodeCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken
)
{
var show = await dbContext.Shows
.Include(s => s.Episodes)
var show = await dbContext
.Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null)
return Result.Failure(ShowErrors.NotFound);
@@ -13,8 +13,8 @@ public sealed class ClearAllMediaCommandHandler(IAppDbContext dbContext, IMediaS
CancellationToken cancellationToken
)
{
var assets = await dbContext.MediaAssets
.Select(a => new { a.Id, a.OriginalExtension })
var assets = await dbContext
.MediaAssets.Select(a => new { a.Id, a.OriginalExtension })
.ToListAsync(cancellationToken);
foreach (var asset in assets)
@@ -14,8 +14,8 @@ public sealed class DeleteShowMediaCommandHandler(IAppDbContext dbContext, IMedi
CancellationToken cancellationToken
)
{
var show = await dbContext.Shows
.Include(s => s.Episodes)
var show = await dbContext
.Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null)
return Result.Failure<int>(ShowErrors.NotFound);
@@ -23,19 +23,19 @@ public sealed class DeleteShowMediaCommandHandler(IAppDbContext dbContext, IMedi
var episodes = show.Episodes.ToList();
var assetIds = episodes.Select(e => e.MediaAssetId).Distinct().ToList();
var assets = await dbContext.MediaAssets
.Where(a => assetIds.Contains(a.Id))
var assets = await dbContext
.MediaAssets.Where(a => assetIds.Contains(a.Id))
.Select(a => new { a.Id, a.OriginalExtension })
.ToListAsync(cancellationToken);
foreach (var asset in assets)
storage.DeleteAssetArtifacts(asset.Id, asset.OriginalExtension);
await dbContext.ScheduleEntries
.Where(e => assetIds.Contains(e.MediaAssetId))
await dbContext
.ScheduleEntries.Where(e => assetIds.Contains(e.MediaAssetId))
.ExecuteDeleteAsync(cancellationToken);
await dbContext.MediaAssets
.Where(a => assetIds.Contains(a.Id))
await dbContext
.MediaAssets.Where(a => assetIds.Contains(a.Id))
.ExecuteDeleteAsync(cancellationToken);
// Серии шоу теперь указывают на удалённые ассеты — убираем их (сохранится через UnitOfWork).
@@ -13,7 +13,8 @@ public sealed class GetMediaAssetQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken
)
{
var asset = await dbContext.MediaAssets.AsNoTracking()
var asset = await dbContext
.MediaAssets.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == query.Id, cancellationToken);
return asset is null
@@ -15,8 +15,7 @@ public sealed class ListMediaAssetsQueryHandler(IAppDbContext dbContext)
)
{
// Сгенерированные системой ассеты (ТВ-заставки) не показываем в библиотеке медиа.
var q = dbContext.MediaAssets.AsNoTracking()
.Where(x => x.Source != MediaSource.Generated);
var q = dbContext.MediaAssets.AsNoTracking().Where(x => x.Source != MediaSource.Generated);
if (query.Statuses.Count > 0)
q = q.Where(x => query.Statuses.Contains(x.Status));
@@ -6,10 +6,8 @@ using TeleWave.Domain.Media;
namespace TeleWave.Application.Media.Register;
public sealed class RegisterMediaAssetCommandHandler(
IAppDbContext dbContext,
IMediaStorage storage
) : ICommandHandler<RegisterMediaAssetCommand, Result<Guid>>
public sealed class RegisterMediaAssetCommandHandler(IAppDbContext dbContext, IMediaStorage storage)
: ICommandHandler<RegisterMediaAssetCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
RegisterMediaAssetCommand command,
@@ -2,7 +2,8 @@ using FluentValidation;
namespace TeleWave.Application.Media.Register;
public sealed class RegisterMediaAssetCommandValidator : AbstractValidator<RegisterMediaAssetCommand>
public sealed class RegisterMediaAssetCommandValidator
: AbstractValidator<RegisterMediaAssetCommand>
{
public RegisterMediaAssetCommandValidator()
{
@@ -53,7 +53,13 @@ public sealed class ApplyShowMetadataCommandHandler(
}
}
show.ApplyMetadata(command.Provider, meta.ExternalId, meta.Overview, meta.Year, posterImageId);
show.ApplyMetadata(
command.Provider,
meta.ExternalId,
meta.Overview,
meta.Year,
posterImageId
);
return Result.Success();
}
}
@@ -19,8 +19,8 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
CancellationToken cancellationToken
)
{
var show = await dbContext.Shows
.Include(s => s.Episodes)
var show = await dbContext
.Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null)
return Result.Failure<int>(ShowErrors.NotFound);
@@ -34,7 +34,8 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
// Имена файлов — чтобы распознать номера у серий, где они ещё не проставлены.
var assetIds = show.Episodes.Select(e => e.MediaAssetId).Distinct().ToList();
var names = await dbContext.MediaAssets.AsNoTracking()
var names = 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);
@@ -14,7 +14,9 @@ public sealed class SearchShowMetadataQueryHandler(IMetadataProviderResolver res
{
var provider = resolver.Resolve(query.Provider);
if (provider is null)
return Result.Failure<IReadOnlyList<MetadataCandidate>>(MetadataErrors.ProviderNotAvailable);
return Result.Failure<IReadOnlyList<MetadataCandidate>>(
MetadataErrors.ProviderNotAvailable
);
if (string.IsNullOrWhiteSpace(query.Query))
return Result.Success<IReadOnlyList<MetadataCandidate>>([]);
@@ -13,7 +13,8 @@ public static class AppSettingsReader
CancellationToken cancellationToken
)
{
var value = await db.AppSettings.AsNoTracking()
var value = await db
.AppSettings.AsNoTracking()
.Where(s => s.Key == key)
.Select(s => s.Value)
.FirstOrDefaultAsync(cancellationToken);
@@ -13,7 +13,10 @@ public sealed class UpdateSiteSettingsCommandHandler(ISiteSettings siteSettings)
)
{
// Сохранение выполняет UnitOfWorkBehavior команды.
await siteSettings.SetRegistrationEnabledAsync(command.RegistrationEnabled, cancellationToken);
await siteSettings.SetRegistrationEnabledAsync(
command.RegistrationEnabled,
cancellationToken
);
return Result.Success();
}
}
@@ -21,9 +21,15 @@ public sealed class GetLivePlaylistQueryHandler(
CancellationToken cancellationToken
)
{
var channel = await dbContext.Channels.AsNoTracking()
var channel = await dbContext
.Channels.AsNoTracking()
.Where(c => c.Slug == query.Slug && c.IsEnabled)
.Select(c => new { c.Id, c.EpochUtc, c.FillerAssetId })
.Select(c => new
{
c.Id,
c.EpochUtc,
c.FillerAssetId,
})
.FirstOrDefaultAsync(cancellationToken);
if (channel is null)
return Result.Failure<LivePlaylistDto>(ChannelErrors.NotFound);
@@ -33,7 +39,8 @@ public sealed class GetLivePlaylistQueryHandler(
// Загружаем записи, пересекающиеся с окном (с небольшим запасом назад).
var windowStartTime = query.Now.AddSeconds(-(window + 2) * seg);
var rawEntries = await dbContext.ScheduleEntries.AsNoTracking()
var rawEntries = await dbContext
.ScheduleEntries.AsNoTracking()
.Where(e =>
e.ChannelId == channel.Id
&& e.StartsAtUtc < query.Now
@@ -52,7 +59,8 @@ public sealed class GetLivePlaylistQueryHandler(
if (channel.FillerAssetId is { } fillerId)
assetIds.Add(fillerId);
var segmentCounts = await dbContext.MediaAssets.AsNoTracking()
var segmentCounts = await dbContext
.MediaAssets.AsNoTracking()
.Where(a =>
assetIds.Contains(a.Id)
&& a.Status == MediaAssetStatus.Ready
@@ -72,7 +80,10 @@ public sealed class GetLivePlaylistQueryHandler(
.ToList();
LiveFiller? filler = null;
if (channel.FillerAssetId is { } fid && segmentCounts.TryGetValue(fid, out var fillerSegments))
if (
channel.FillerAssetId is { } fid
&& segmentCounts.TryGetValue(fid, out var fillerSegments)
)
filler = new LiveFiller(fid, fillerSegments);
var playlist = LiveWindowCalculator.Build(
@@ -82,8 +93,8 @@ public sealed class GetLivePlaylistQueryHandler(
var dto = new LivePlaylistDto(
playlist.MediaSequence,
playlist.TargetDuration,
playlist.Segments
.Select(s => new LiveSegmentDto(s.AssetId, s.LocalIndex, s.Discontinuity))
playlist
.Segments.Select(s => new LiveSegmentDto(s.AssetId, s.LocalIndex, s.Discontinuity))
.ToList()
);
return Result.Success(dto);
@@ -14,14 +14,16 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken
)
{
var channelId = await dbContext.Channels.AsNoTracking()
var channelId = await dbContext
.Channels.AsNoTracking()
.Where(c => c.Slug == query.Slug && c.IsEnabled)
.Select(c => (Guid?)c.Id)
.FirstOrDefaultAsync(cancellationToken);
if (channelId is null)
return Result.Failure<IReadOnlyList<PublicEpgEntryDto>>(ChannelErrors.NotFound);
var entries = await dbContext.ScheduleEntries.AsNoTracking()
var entries = await dbContext
.ScheduleEntries.AsNoTracking()
.Where(e =>
e.ChannelId == channelId
&& e.StartsAtUtc < query.ToUtc
@@ -38,15 +40,26 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
})
.ToListAsync(cancellationToken);
var showIds = entries.Where(e => e.ShowId != null).Select(e => e.ShowId!.Value).Distinct().ToList();
var shows = await dbContext.Shows.AsNoTracking()
var showIds = entries
.Where(e => e.ShowId != null)
.Select(e => e.ShowId!.Value)
.Distinct()
.ToList();
var shows = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id))
.Select(s => new { s.Id, s.Name, s.PosterImageId })
.Select(s => new
{
s.Id,
s.Name,
s.PosterImageId,
})
.ToDictionaryAsync(s => s.Id, cancellationToken);
// Метаданные серий: ключ — (шоу, ассет).
var assetIds = entries.Select(e => e.MediaAssetId).Distinct().ToList();
var episodes = await dbContext.Shows.AsNoTracking()
var episodes = await dbContext
.Shows.AsNoTracking()
.SelectMany(s => s.Episodes)
.Where(e => showIds.Contains(e.ShowId) && assetIds.Contains(e.MediaAssetId))
.Select(e => new
@@ -13,10 +13,16 @@ public sealed class ListPublicChannelsQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken
)
{
var channels = await dbContext.Channels.AsNoTracking()
var channels = await dbContext
.Channels.AsNoTracking()
.Where(c => c.IsEnabled)
.OrderBy(c => c.Name)
.Select(c => new { c.Id, c.Slug, c.Name })
.Select(c => new
{
c.Id,
c.Slug,
c.Name,
})
.ToListAsync(cancellationToken);
if (channels.Count == 0)
return [];
@@ -25,7 +31,8 @@ public sealed class ListPublicChannelsQueryHandler(IAppDbContext dbContext)
var channelIds = channels.Select(c => c.Id).ToList();
// Что идёт прямо сейчас на каждом канале (программа) — для постера-обложки плитки.
var currentByChannel = await dbContext.ScheduleEntries.AsNoTracking()
var currentByChannel = await dbContext
.ScheduleEntries.AsNoTracking()
.Where(e =>
channelIds.Contains(e.ChannelId)
&& e.Kind == ScheduleEntryKind.Program
@@ -40,9 +47,15 @@ public sealed class ListPublicChannelsQueryHandler(IAppDbContext dbContext)
.ToDictionary(g => g.Key, g => g.First().ShowId);
var showIds = currentShowByChannel.Values.Distinct().ToList();
var shows = await dbContext.Shows.AsNoTracking()
var shows = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id))
.Select(s => new { s.Id, s.Name, s.PosterImageId })
.Select(s => new
{
s.Id,
s.Name,
s.PosterImageId,
})
.ToDictionaryAsync(s => s.Id, cancellationToken);
return channels