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
+8 -2
View File
@@ -9,7 +9,10 @@
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" /> <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.10" /> <PackageVersion
Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore"
Version="10.0.10"
/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<!-- Держим 2.x: Microsoft.AspNetCore.OpenApi 10.0.10 сам зависит от Microsoft.OpenApi >=2.0.0 <!-- Держим 2.x: Microsoft.AspNetCore.OpenApi 10.0.10 сам зависит от Microsoft.OpenApi >=2.0.0
(nuspec) и его Roslyn source generator (XmlCommentGenerator) скомпилирован под 2.x API — (nuspec) и его Roslyn source generator (XmlCommentGenerator) скомпилирован под 2.x API —
@@ -24,7 +27,10 @@
</PackageVersion> </PackageVersion>
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" /> <PackageVersion
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="10.0.10"
/>
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" />
@@ -24,12 +24,8 @@ public static class AdminUserEndpoints
admin.MapGet("", ListUsers).Produces<PagedList<UserSummaryDto>>(); admin.MapGet("", ListUsers).Produces<PagedList<UserSummaryDto>>();
admin.MapPost("", CreateUser).Produces<CreatedIdResponse>(StatusCodes.Status201Created); admin.MapPost("", CreateUser).Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin.MapGet("/{id:guid}", GetUser).Produces<UserSummaryDto>(); admin.MapGet("/{id:guid}", GetUser).Produces<UserSummaryDto>();
admin admin.MapPost("/{id:guid}/block", BlockUser).Produces(StatusCodes.Status204NoContent);
.MapPost("/{id:guid}/block", BlockUser) admin.MapPost("/{id:guid}/unblock", UnblockUser).Produces(StatusCodes.Status204NoContent);
.Produces(StatusCodes.Status204NoContent);
admin
.MapPost("/{id:guid}/unblock", UnblockUser)
.Produces(StatusCodes.Status204NoContent);
admin admin
.MapPost("/{id:guid}/password", ResetPassword) .MapPost("/{id:guid}/password", ResetPassword)
.Produces(StatusCodes.Status204NoContent); .Produces(StatusCodes.Status204NoContent);
@@ -49,7 +45,13 @@ public static class AdminUserEndpoints
) )
{ {
var result = await sender.Send( var result = await sender.Send(
new ListUsersQuery(page <= 0 ? 1 : page, pageSize <= 0 ? 20 : pageSize, search, roleId, isBlocked), new ListUsersQuery(
page <= 0 ? 1 : page,
pageSize <= 0 ? 20 : pageSize,
search,
roleId,
isBlocked
),
cancellationToken cancellationToken
); );
return Results.Ok(result); return Results.Ok(result);
@@ -66,7 +68,10 @@ public static class AdminUserEndpoints
cancellationToken cancellationToken
); );
return result.IsSuccess return result.IsSuccess
? Results.Created($"/api/admin/users/{result.Value}", new CreatedIdResponse(result.Value)) ? Results.Created(
$"/api/admin/users/{result.Value}",
new CreatedIdResponse(result.Value)
)
: result.ToHttpResult(); : result.ToHttpResult();
} }
@@ -40,7 +40,9 @@ public static class ChannelEndpoints
admin.MapPost("", CreateChannel).Produces<CreatedIdResponse>(StatusCodes.Status201Created); admin.MapPost("", CreateChannel).Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin.MapGet("", ListChannels).Produces<IReadOnlyList<ChannelSummaryDto>>(); admin.MapGet("", ListChannels).Produces<IReadOnlyList<ChannelSummaryDto>>();
admin.MapGet("/{id:guid}", GetChannel).Produces<ChannelDto>(); admin.MapGet("/{id:guid}", GetChannel).Produces<ChannelDto>();
admin.MapPut("/{id:guid}/settings", UpdateSettings).Produces(StatusCodes.Status204NoContent); admin
.MapPut("/{id:guid}/settings", UpdateSettings)
.Produces(StatusCodes.Status204NoContent);
admin admin
.MapPost("/{id:guid}/shows", AddShow) .MapPost("/{id:guid}/shows", AddShow)
@@ -75,10 +77,16 @@ public static class ChannelEndpoints
.MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/audio", ClearTemplateAudio) .MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/audio", ClearTemplateAudio)
.Produces(StatusCodes.Status204NoContent); .Produces(StatusCodes.Status204NoContent);
admin admin
.MapPut("/{id:guid}/bumper/templates/{templateId:guid}/background", SetTemplateBackground) .MapPut(
"/{id:guid}/bumper/templates/{templateId:guid}/background",
SetTemplateBackground
)
.Produces(StatusCodes.Status204NoContent); .Produces(StatusCodes.Status204NoContent);
admin admin
.MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/background", ClearTemplateBackground) .MapDelete(
"/{id:guid}/bumper/templates/{templateId:guid}/background",
ClearTemplateBackground
)
.Produces(StatusCodes.Status204NoContent); .Produces(StatusCodes.Status204NoContent);
admin admin
@@ -139,7 +147,10 @@ public static class ChannelEndpoints
: result.ToHttpResult(); : result.ToHttpResult();
} }
private static async Task<IResult> ListChannels(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> ListChannels(
ISender sender,
CancellationToken cancellationToken
)
{ {
var result = await sender.Send(new ListChannelsQuery(), cancellationToken); var result = await sender.Send(new ListChannelsQuery(), cancellationToken);
return Results.Ok(result); return Results.Ok(result);
@@ -186,7 +197,13 @@ public static class ChannelEndpoints
) )
{ {
var result = await sender.Send( var result = await sender.Send(
new AddChannelShowCommand(id, body.ShowId, body.Weight, body.BlockMode, body.BlockValue), new AddChannelShowCommand(
id,
body.ShowId,
body.Weight,
body.BlockMode,
body.BlockValue
),
cancellationToken cancellationToken
); );
return result.IsSuccess return result.IsSuccess
@@ -225,7 +242,10 @@ public static class ChannelEndpoints
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var result = await sender.Send(new RemoveChannelShowCommand(id, channelShowId), cancellationToken); var result = await sender.Send(
new RemoveChannelShowCommand(id, channelShowId),
cancellationToken
);
return result.ToHttpResult(); return result.ToHttpResult();
} }
@@ -236,7 +256,10 @@ public static class ChannelEndpoints
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var result = await sender.Send(new AddChannelAdCommand(id, body.MediaAssetId), cancellationToken); var result = await sender.Send(
new AddChannelAdCommand(id, body.MediaAssetId),
cancellationToken
);
return result.IsSuccess return result.IsSuccess
? Results.Created($"/api/admin/channels/{id}", new CreatedIdResponse(result.Value)) ? Results.Created($"/api/admin/channels/{id}", new CreatedIdResponse(result.Value))
: result.ToHttpResult(); : result.ToHttpResult();
@@ -249,7 +272,10 @@ public static class ChannelEndpoints
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var result = await sender.Send(new RemoveChannelAdCommand(id, channelAdId), cancellationToken); var result = await sender.Send(
new RemoveChannelAdCommand(id, channelAdId),
cancellationToken
);
return result.ToHttpResult(); return result.ToHttpResult();
} }
@@ -456,7 +482,12 @@ public static class ChannelEndpoints
} }
/// <summary>Плейлист превью подблока: переписываем ffmpeg-index.m3u8, направляя сегменты на admin-роут.</summary> /// <summary>Плейлист превью подблока: переписываем ffmpeg-index.m3u8, направляя сегменты на admin-роут.</summary>
private static IResult PreviewPlaylist(Guid id, Guid templateId, Guid variantId, MediaPathResolver paths) private static IResult PreviewPlaylist(
Guid id,
Guid templateId,
Guid variantId,
MediaPathResolver paths
)
{ {
var previewId = BumperPreview.AssetId(variantId); var previewId = BumperPreview.AssetId(variantId);
string indexPath; string indexPath;
@@ -471,7 +502,8 @@ public static class ChannelEndpoints
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
return Results.NotFound(); return Results.NotFound();
var baseUrl = $"/api/admin/channels/{id}/bumper/templates/{templateId}/preview/{variantId}/"; var baseUrl =
$"/api/admin/channels/{id}/bumper/templates/{templateId}/preview/{variantId}/";
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var line in File.ReadLines(indexPath)) foreach (var line in File.ReadLines(indexPath))
{ {
@@ -605,7 +637,12 @@ public sealed record UpdateChannelSettingsBody(
Guid? FillerAssetId Guid? FillerAssetId
); );
public sealed record AddChannelShowBody(Guid ShowId, int Weight, BlockMode BlockMode, int BlockValue); public sealed record AddChannelShowBody(
Guid ShowId,
int Weight,
BlockMode BlockMode,
int BlockValue
);
public sealed record UpdateChannelShowBody( public sealed record UpdateChannelShowBody(
int Weight, int Weight,
@@ -86,7 +86,10 @@ public static class ImageEndpoints
throw; throw;
} }
return Results.Created($"/api/images/{created.Value}", new CreatedIdResponse(created.Value)); return Results.Created(
$"/api/images/{created.Value}",
new CreatedIdResponse(created.Value)
);
} }
private static async Task<IResult> DeleteImage( private static async Task<IResult> DeleteImage(
@@ -22,13 +22,19 @@ public static class MaintenanceEndpoints
return app; return app;
} }
private static async Task<IResult> ClearMedia(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> ClearMedia(
ISender sender,
CancellationToken cancellationToken
)
{ {
var result = await sender.Send(new ClearAllMediaCommand(), cancellationToken); var result = await sender.Send(new ClearAllMediaCommand(), cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
} }
private static async Task<IResult> ClearShows(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> ClearShows(
ISender sender,
CancellationToken cancellationToken
)
{ {
var result = await sender.Send(new DeleteAllShowsCommand(), cancellationToken); var result = await sender.Send(new DeleteAllShowsCommand(), cancellationToken);
return result.ToHttpResult(); return result.ToHttpResult();
@@ -9,8 +9,8 @@ using TeleWave.Application.Media.GetMedia;
using TeleWave.Application.Media.ListMedia; using TeleWave.Application.Media.ListMedia;
using TeleWave.Application.Media.Register; using TeleWave.Application.Media.Register;
using TeleWave.Domain.Media; using TeleWave.Domain.Media;
using TeleWave.Infrastructure.Media;
using TeleWave.Infrastructure.Identity; using TeleWave.Infrastructure.Identity;
using TeleWave.Infrastructure.Media;
namespace TeleWave.Api.Endpoints; namespace TeleWave.Api.Endpoints;
@@ -89,7 +89,10 @@ public static class MediaEndpoints
} }
queue.Enqueue(result.Value); queue.Enqueue(result.Value);
return Results.Created($"/api/admin/media/{result.Value}", new UploadMediaResponse(result.Value)); return Results.Created(
$"/api/admin/media/{result.Value}",
new UploadMediaResponse(result.Value)
);
} }
private static async Task<IResult> List( private static async Task<IResult> List(
@@ -41,7 +41,9 @@ public static class MetadataEndpoints
admin.MapPost("/shows/{showId:guid}/apply", Apply).Produces(StatusCodes.Status204NoContent); admin.MapPost("/shows/{showId:guid}/apply", Apply).Produces(StatusCodes.Status204NoContent);
admin.MapPut("/shows/{showId:guid}", Update).Produces(StatusCodes.Status204NoContent); admin.MapPut("/shows/{showId:guid}", Update).Produces(StatusCodes.Status204NoContent);
admin.MapDelete("/shows/{showId:guid}", Clear).Produces(StatusCodes.Status204NoContent); admin.MapDelete("/shows/{showId:guid}", Clear).Produces(StatusCodes.Status204NoContent);
admin.MapPut("/shows/{showId:guid}/poster", UploadPoster).Produces(StatusCodes.Status204NoContent); admin
.MapPut("/shows/{showId:guid}/poster", UploadPoster)
.Produces(StatusCodes.Status204NoContent);
admin admin
.MapPut("/shows/{showId:guid}/poster-image", SetPosterImage) .MapPut("/shows/{showId:guid}/poster-image", SetPosterImage)
.Produces(StatusCodes.Status204NoContent); .Produces(StatusCodes.Status204NoContent);
@@ -51,7 +53,10 @@ public static class MetadataEndpoints
return app; return app;
} }
private static async Task<IResult> GetProviders(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> GetProviders(
ISender sender,
CancellationToken cancellationToken
)
{ {
var providers = await sender.Send(new GetMetadataProvidersQuery(), cancellationToken); var providers = await sender.Send(new GetMetadataProvidersQuery(), cancellationToken);
return Results.Ok(providers); return Results.Ok(providers);
@@ -64,7 +69,10 @@ public static class MetadataEndpoints
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var result = await sender.Send(new SearchShowMetadataQuery(provider, query), cancellationToken); var result = await sender.Send(
new SearchShowMetadataQuery(provider, query),
cancellationToken
);
return result.ToHttpResult(); return result.ToHttpResult();
} }
@@ -29,7 +29,10 @@ public static class RoleEndpoints
return app; return app;
} }
private static async Task<IResult> ListRoles(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> ListRoles(
ISender sender,
CancellationToken cancellationToken
)
{ {
var roles = await sender.Send(new ListRolesQuery(), cancellationToken); var roles = await sender.Send(new ListRolesQuery(), cancellationToken);
return Results.Ok(roles); return Results.Ok(roles);
@@ -21,7 +21,10 @@ public static class SettingsEndpoints
return app; return app;
} }
private static async Task<IResult> GetSettings(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> GetSettings(
ISender sender,
CancellationToken cancellationToken
)
{ {
var settings = await sender.Send(new GetSiteSettingsQuery(), cancellationToken); var settings = await sender.Send(new GetSiteSettingsQuery(), cancellationToken);
return Results.Ok(settings); return Results.Ok(settings);
@@ -47,11 +47,17 @@ public static class ShowEndpoints
{ {
var result = await sender.Send(command, cancellationToken); var result = await sender.Send(command, cancellationToken);
return result.IsSuccess return result.IsSuccess
? Results.Created($"/api/admin/shows/{result.Value}", new CreatedIdResponse(result.Value)) ? Results.Created(
$"/api/admin/shows/{result.Value}",
new CreatedIdResponse(result.Value)
)
: result.ToHttpResult(); : result.ToHttpResult();
} }
private static async Task<IResult> ListShows(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> ListShows(
ISender sender,
CancellationToken cancellationToken
)
{ {
var result = await sender.Send(new ListShowsQuery(), cancellationToken); var result = await sender.Send(new ListShowsQuery(), cancellationToken);
return Results.Ok(result); return Results.Ok(result);
@@ -109,7 +115,10 @@ public static class ShowEndpoints
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var result = await sender.Send(new AddEpisodeCommand(id, body.MediaAssetId), cancellationToken); var result = await sender.Send(
new AddEpisodeCommand(id, body.MediaAssetId),
cancellationToken
);
return result.IsSuccess return result.IsSuccess
? Results.Created($"/api/admin/shows/{id}", new CreatedIdResponse(result.Value)) ? Results.Created($"/api/admin/shows/{id}", new CreatedIdResponse(result.Value))
: result.ToHttpResult(); : result.ToHttpResult();
@@ -33,7 +33,10 @@ public static class StreamingEndpoints
return app; return app;
} }
private static async Task<IResult> ListChannels(ISender sender, CancellationToken cancellationToken) private static async Task<IResult> ListChannels(
ISender sender,
CancellationToken cancellationToken
)
{ {
var result = await sender.Send(new ListPublicChannelsQuery(), cancellationToken); var result = await sender.Send(new ListPublicChannelsQuery(), cancellationToken);
return Results.Ok(result); return Results.Ok(result);
@@ -145,8 +148,14 @@ public static class StreamingEndpoints
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("#EXTM3U\n"); sb.Append("#EXTM3U\n");
sb.Append("#EXT-X-VERSION:3\n"); sb.Append("#EXT-X-VERSION:3\n");
sb.Append(CultureInfo.InvariantCulture, $"#EXT-X-TARGETDURATION:{playlist.TargetDuration}\n"); sb.Append(
sb.Append(CultureInfo.InvariantCulture, $"#EXT-X-MEDIA-SEQUENCE:{playlist.MediaSequence}\n"); CultureInfo.InvariantCulture,
$"#EXT-X-TARGETDURATION:{playlist.TargetDuration}\n"
);
sb.Append(
CultureInfo.InvariantCulture,
$"#EXT-X-MEDIA-SEQUENCE:{playlist.MediaSequence}\n"
);
foreach (var segment in playlist.Segments) foreach (var segment in playlist.Segments)
{ {
+2 -2
View File
@@ -2,14 +2,14 @@ using System.Net;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Scalar.AspNetCore;
using Serilog;
using TeleWave.Api.Common; using TeleWave.Api.Common;
using TeleWave.Api.Endpoints; using TeleWave.Api.Endpoints;
using TeleWave.Application; using TeleWave.Application;
using TeleWave.Infrastructure; using TeleWave.Infrastructure;
using TeleWave.Infrastructure.Identity; using TeleWave.Infrastructure.Identity;
using TeleWave.Infrastructure.Persistence; using TeleWave.Infrastructure.Persistence;
using Scalar.AspNetCore;
using Serilog;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -7,6 +7,8 @@ namespace TeleWave.Application.Admin.Roles.ChangeUserRole;
public sealed class ChangeUserRoleCommandHandler(IRoleService roleService) public sealed class ChangeUserRoleCommandHandler(IRoleService roleService)
: ICommandHandler<ChangeUserRoleCommand, Result> : ICommandHandler<ChangeUserRoleCommand, Result>
{ {
public Task<Result> Handle(ChangeUserRoleCommand command, CancellationToken cancellationToken) => public Task<Result> Handle(
roleService.ChangeUserRoleAsync(command.UserId, command.RoleId, cancellationToken); ChangeUserRoleCommand command,
CancellationToken cancellationToken
) => roleService.ChangeUserRoleAsync(command.UserId, command.RoleId, cancellationToken);
} }
@@ -31,7 +31,11 @@ public sealed class CreateUserCommandHandler(
var userId = createResult.Value; var userId = createResult.Value;
// CreateUserAsync назначает роль по умолчанию — выставляем выбранную админом. // CreateUserAsync назначает роль по умолчанию — выставляем выбранную админом.
var roleResult = await roleService.ChangeUserRoleAsync(userId, command.RoleId, cancellationToken); var roleResult = await roleService.ChangeUserRoleAsync(
userId,
command.RoleId,
cancellationToken
);
if (!roleResult.IsSuccess) if (!roleResult.IsSuccess)
return Result.Failure<Guid>(roleResult.Error); return Result.Failure<Guid>(roleResult.Error);
@@ -4,7 +4,10 @@ namespace TeleWave.Application.Admin.Users;
public static class UserErrors 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( public static readonly Error CannotDeleteSelf = Error.Forbidden(
"Users.CannotDeleteSelf", "Users.CannotDeleteSelf",
@@ -4,8 +4,10 @@ namespace TeleWave.Application.Auth;
public static class AuthErrors public static class AuthErrors
{ {
public static readonly Error InvalidCredentials = public static readonly Error InvalidCredentials = Error.Unauthorized(
Error.Unauthorized("Auth.InvalidCredentials", "Неверное имя пользователя или пароль."); "Auth.InvalidCredentials",
"Неверное имя пользователя или пароль."
);
public static readonly Error Unauthorized = Error.Unauthorized( public static readonly Error Unauthorized = Error.Unauthorized(
"Auth.Unauthorized", "Auth.Unauthorized",
@@ -3,4 +3,5 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Auth.ChangePassword; 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; 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; 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 CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Ads) .Channels.Include(c => c.Ads)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure<Guid>(ChannelErrors.NotFound); return Result.Failure<Guid>(ChannelErrors.NotFound);
@@ -13,13 +13,16 @@ public sealed class AddChannelShowCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Shows) .Channels.Include(c => c.Shows)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure<Guid>(ChannelErrors.NotFound); 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) if (!showExists)
return Result.Failure<Guid>(ChannelErrors.ShowNotFound); return Result.Failure<Guid>(ChannelErrors.ShowNotFound);
@@ -13,8 +13,8 @@ public sealed class AddBumperTemplateCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure<Guid>(ChannelErrors.NotFound); return Result.Failure<Guid>(ChannelErrors.NotFound);
@@ -13,9 +13,9 @@ public sealed class AddBumperTextVariantCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants) .ThenInclude(t => t.Variants)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure<Guid>(ChannelErrors.NotFound); return Result.Failure<Guid>(ChannelErrors.NotFound);
@@ -13,8 +13,10 @@ public sealed class BumperOptions
public int Height { get; init; } = 720; public int Height { get; init; } = 720;
/// <summary>Пути к TTF-шрифтам с кириллицей внутри контейнера (см. Dockerfile, fonts-dejavu-core).</summary> /// <summary>Пути к TTF-шрифтам с кириллицей внутри контейнера (см. Dockerfile, fonts-dejavu-core).</summary>
public string FontFileSans { get; init; } = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"; public string FontFileSans { get; init; } =
public string FontFileSerif { get; init; } = "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf"; "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
public string FontFileSerif { get; init; } =
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf";
/// <summary>Версия шаблона рендера — входит в кэш-ключ заставки; меняй при правке ЛОГИКИ рендера /// <summary>Версия шаблона рендера — входит в кэш-ключ заставки; меняй при правке ЛОГИКИ рендера
/// (не оформления канала), чтобы пересобрать уже отрендеренные заставки.</summary> /// (не оформления канала), чтобы пересобрать уже отрендеренные заставки.</summary>
@@ -15,8 +15,8 @@ public sealed class ClearBumperTemplateAudioCommandHandler(
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -13,8 +13,8 @@ public sealed class ClearBumperTemplateBackgroundCommandHandler(IAppDbContext db
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -4,4 +4,5 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Broadcast.Bumpers; namespace TeleWave.Application.Broadcast.Bumpers;
/// <summary>Удалить блок заставки (кроме дефолтного) и его файлы.</summary> /// <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 CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -13,9 +13,9 @@ public sealed class RemoveBumperTextVariantCommandHandler(IAppDbContext dbContex
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants) .ThenInclude(t => t.Variants)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -28,9 +28,10 @@ public sealed class RenderBumperPreviewQueryHandler(
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels.AsNoTracking() var channel = await dbContext
.Channels.AsNoTracking()
.Include(c => c.BumperTemplates) .Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants) .ThenInclude(t => t.Variants)
.Include(c => c.Shows) .Include(c => c.Shows)
.AsSplitQuery() .AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken);
@@ -49,7 +50,8 @@ public sealed class RenderBumperPreviewQueryHandler(
string? backgroundPath = null; string? backgroundPath = null;
if (template.BackgroundImageId is { } bgId) if (template.BackgroundImageId is { } bgId)
{ {
var bgExt = await dbContext.Images.AsNoTracking() var bgExt = await dbContext
.Images.AsNoTracking()
.Where(i => i.Id == bgId) .Where(i => i.Id == bgId)
.Select(i => i.FileExtension) .Select(i => i.FileExtension)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -57,8 +59,9 @@ public sealed class RenderBumperPreviewQueryHandler(
backgroundPath = imageStore.ResolvePath(bgId, bgExt); backgroundPath = imageStore.ResolvePath(bgId, bgExt);
} }
var seconds = var seconds = template.AudioDurationSeconds is { } d and > 0
template.AudioDurationSeconds is { } d and > 0 ? d : DefaultBumperDurationSeconds; ? d
: DefaultBumperDurationSeconds;
var aligned = (int)( var aligned = (int)(
Math.Ceiling(Math.Max(_segmentSeconds, seconds) / _segmentSeconds) * _segmentSeconds 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 showIds = channel.Shows.Select(s => s.ShowId).Distinct().Take(2).ToList();
var names = showIds.Count == 0 var names =
? [] showIds.Count == 0
: await dbContext.Shows.AsNoTracking() ? []
.Where(s => showIds.Contains(s.Id)) : await dbContext
.Select(s => s.Name) .Shows.AsNoTracking()
.Take(2) .Where(s => showIds.Contains(s.Id))
.ToListAsync(cancellationToken); .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 CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -13,8 +13,8 @@ public sealed class SetBumperTemplateBackgroundCommandHandler(IAppDbContext dbCo
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -13,8 +13,8 @@ public sealed class UpdateBumperTemplateCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -24,6 +24,8 @@ public sealed partial class UpdateBumperTemplateCommandValidator
private static bool BeSafeColor(string? value) => private static bool BeSafeColor(string? value) =>
!string.IsNullOrWhiteSpace(value) && ColorRegex().IsMatch(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(); private static partial Regex ColorRegex();
} }
@@ -13,9 +13,9 @@ public sealed class UpdateBumperTextVariantCommandHandler(IAppDbContext dbContex
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.BumperTemplates) .Channels.Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants) .ThenInclude(t => t.Variants)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -27,7 +27,11 @@ public sealed class CreateProgrammingOverrideCommandHandler(IAppDbContext dbCont
) )
return Result.Failure<Guid>(ChannelErrors.InvalidOverrideWindow); 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); return Result.Failure<Guid>(ChannelErrors.InvalidOverrideWindow);
} }
@@ -35,9 +39,9 @@ public sealed class CreateProgrammingOverrideCommandHandler(IAppDbContext dbCont
if (command.Shows.Count == 0) if (command.Shows.Count == 0)
return Result.Failure<Guid>(ChannelErrors.OverrideNeedsShow); return Result.Failure<Guid>(ChannelErrors.OverrideNeedsShow);
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Overrides) .Channels.Include(c => c.Overrides)
.ThenInclude(o => o.Shows) .ThenInclude(o => o.Shows)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure<Guid>(ChannelErrors.NotFound); return Result.Failure<Guid>(ChannelErrors.NotFound);
@@ -57,7 +61,11 @@ public sealed class CreateProgrammingOverrideCommandHandler(IAppDbContext dbCont
command.StartMinute!.Value, command.StartMinute!.Value,
command.EndMinute!.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) foreach (var show in command.Shows)
ovr.AddShow(show.ShowId, show.Weight); ovr.AddShow(show.ShowId, show.Weight);
@@ -8,6 +8,7 @@ public sealed class CreateProgrammingOverrideCommandValidator
public CreateProgrammingOverrideCommandValidator() public CreateProgrammingOverrideCommandValidator()
{ {
RuleFor(x => x.Shows).NotEmpty(); 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 CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Overrides) .Channels.Include(c => c.Overrides)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -13,38 +13,42 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels.AsNoTracking() var channel = await dbContext
.Channels.AsNoTracking()
.Include(c => c.Shows) .Include(c => c.Shows)
.ThenInclude(s => s.PreferredHours) .ThenInclude(s => s.PreferredHours)
.Include(c => c.Ads) .Include(c => c.Ads)
.Include(c => c.BumperTemplates) .Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants) .ThenInclude(t => t.Variants)
.Include(c => c.Overrides) .Include(c => c.Overrides)
.ThenInclude(o => o.Shows) .ThenInclude(o => o.Shows)
.AsSplitQuery() .AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == query.Id, cancellationToken); .FirstOrDefaultAsync(c => c.Id == query.Id, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure<ChannelDto>(ChannelErrors.NotFound); 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))) .Concat(channel.Overrides.SelectMany(o => o.Shows.Select(s => s.ShowId)))
.Distinct() .Distinct()
.ToList(); .ToList();
var showNames = await dbContext.Shows.AsNoTracking() var showNames = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id)) .Where(s => showIds.Contains(s.Id))
.Select(s => new { s.Id, s.Name }) .Select(s => new { s.Id, s.Name })
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken); .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
var poolAssetIds = channel.Ads.Select(a => a.MediaAssetId).Distinct().ToList(); 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)) .Where(a => poolAssetIds.Contains(a.Id))
.Select(a => new { a.Id, a.OriginalFileName }) .Select(a => new { a.Id, a.OriginalFileName })
.ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken); .ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);
string ShowName(Guid id) => showNames.GetValueOrDefault(id, "(удалено)"); string ShowName(Guid id) => showNames.GetValueOrDefault(id, "(удалено)");
var shows = channel.Shows var shows = channel
.Select(s => new ChannelShowDto( .Shows.Select(s => new ChannelShowDto(
s.Id, s.Id,
s.ShowId, s.ShowId,
ShowName(s.ShowId), ShowName(s.ShowId),
@@ -54,15 +58,14 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
s.IsEnabled, s.IsEnabled,
s.NextEpisodeIndex, s.NextEpisodeIndex,
s.PreferredWeightMultiplier, s.PreferredWeightMultiplier,
s.PreferredHours s.PreferredHours.OrderBy(h => h.StartHour)
.OrderBy(h => h.StartHour)
.Select(h => new HourWindowDto(h.StartHour, h.EndHour)) .Select(h => new HourWindowDto(h.StartHour, h.EndHour))
.ToList() .ToList()
)) ))
.ToList(); .ToList();
var ads = channel.Ads var ads = channel
.OrderBy(a => a.Position) .Ads.OrderBy(a => a.Position)
.Select(a => new ChannelAdDto( .Select(a => new ChannelAdDto(
a.Id, a.Id,
a.MediaAssetId, a.MediaAssetId,
@@ -71,8 +74,8 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
)) ))
.ToList(); .ToList();
var bumperTemplates = channel.BumperTemplates var bumperTemplates = channel
.OrderBy(t => t.Position) .BumperTemplates.OrderBy(t => t.Position)
.Select(t => new BumperTemplateDto( .Select(t => new BumperTemplateDto(
t.Id, t.Id,
t.Position, t.Position,
@@ -85,8 +88,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
t.BackgroundImageId, t.BackgroundImageId,
t.AudioExtension is not null, t.AudioExtension is not null,
t.AudioDurationSeconds, t.AudioDurationSeconds,
t.Variants t.Variants.OrderBy(v => v.Position)
.OrderBy(v => v.Position)
.Select(v => new BumperTextVariantDto( .Select(v => new BumperTextVariantDto(
v.Id, v.Id,
v.Position, v.Position,
@@ -103,8 +105,8 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
)) ))
.ToList(); .ToList();
var overrides = channel.Overrides var overrides = channel
.OrderBy(o => o.Recurrence) .Overrides.OrderBy(o => o.Recurrence)
.ThenBy(o => o.StartsAtUtc) .ThenBy(o => o.StartsAtUtc)
.ThenBy(o => o.DayOfWeek) .ThenBy(o => o.DayOfWeek)
.Select(o => new ProgrammingOverrideDto( .Select(o => new ProgrammingOverrideDto(
@@ -116,8 +118,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
o.DayOfWeek, o.DayOfWeek,
o.StartMinute, o.StartMinute,
o.EndMinute, o.EndMinute,
o.Shows o.Shows.Select(s => new OverrideShowDto(s.ShowId, ShowName(s.ShowId), s.Weight))
.Select(s => new OverrideShowDto(s.ShowId, ShowName(s.ShowId), s.Weight))
.ToList() .ToList()
)) ))
.ToList(); .ToList();
@@ -22,7 +22,8 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
return Result.Failure<IReadOnlyList<ScheduleEntryDto>>(ChannelErrors.NotFound); return Result.Failure<IReadOnlyList<ScheduleEntryDto>>(ChannelErrors.NotFound);
// Пересекающиеся с окном [from, to) записи. // Пересекающиеся с окном [from, to) записи.
var entries = await dbContext.ScheduleEntries.AsNoTracking() var entries = await dbContext
.ScheduleEntries.AsNoTracking()
.Where(e => .Where(e =>
e.ChannelId == query.ChannelId e.ChannelId == query.ChannelId
&& e.StartsAtUtc < query.ToUtc && e.StartsAtUtc < query.ToUtc
@@ -31,23 +32,32 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
.OrderBy(e => e.StartsAtUtc) .OrderBy(e => e.StartsAtUtc)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var showIds = entries.Where(e => e.ShowId != null).Select(e => e.ShowId!.Value).Distinct().ToList(); var showIds = entries
var showNames = await dbContext.Shows.AsNoTracking() .Where(e => e.ShowId != null)
.Select(e => e.ShowId!.Value)
.Distinct()
.ToList();
var showNames = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id)) .Where(s => showIds.Contains(s.Id))
.Select(s => new { s.Id, s.Name }) .Select(s => new { s.Id, s.Name })
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken); .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
// Подблоки заставок в окне — чтобы показать в расписании, какая именно заставка и с каким текстом. // Подблоки заставок в окне — чтобы показать в расписании, какая именно заставка и с каким текстом.
var variantIds = entries 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) .Select(e => e.BumperVariantId!.Value)
.Distinct() .Distinct()
.ToList(); .ToList();
var variants = variantIds.Count == 0 var variants =
? [] variantIds.Count == 0
: await dbContext.BumperTextVariants.AsNoTracking() ? []
.Where(v => variantIds.Contains(v.Id)) : await dbContext
.ToListAsync(cancellationToken); .BumperTextVariants.AsNoTracking()
.Where(v => variantIds.Contains(v.Id))
.ToListAsync(cancellationToken);
var variantsById = variants.ToDictionary(v => v.Id); var variantsById = variants.ToDictionary(v => v.Id);
// Имена ассетов программ — чтобы показать реальную метку S16E03 в расписании админки. // Имена ассетов программ — чтобы показать реальную метку S16E03 в расписании админки.
@@ -56,7 +66,8 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
.Select(e => e.MediaAssetId) .Select(e => e.MediaAssetId)
.Distinct() .Distinct()
.ToList(); .ToList();
var assetNames = await dbContext.MediaAssets.AsNoTracking() var assetNames = await dbContext
.MediaAssets.AsNoTracking()
.Where(a => assetIds.Contains(a.Id)) .Where(a => assetIds.Contains(a.Id))
.Select(a => new { a.Id, a.OriginalFileName }) .Select(a => new { a.Id, a.OriginalFileName })
.ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken); .ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);
@@ -88,7 +99,9 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
e.ShowId, e.ShowId,
e.ShowId is { } sid ? showNames.GetValueOrDefault(sid) : null, e.ShowId is { } sid ? showNames.GetValueOrDefault(sid) : null,
e.EpisodeIndex, 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, bumperName,
bumperText bumperText
) )
@@ -12,7 +12,8 @@ public sealed class ListChannelsQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
return await dbContext.Channels.AsNoTracking() return await dbContext
.Channels.AsNoTracking()
.OrderBy(c => c.Name) .OrderBy(c => c.Name)
.Select(c => new ChannelSummaryDto(c.Id, c.Name, c.Slug, c.IsEnabled)) .Select(c => new ChannelSummaryDto(c.Id, c.Name, c.Slug, c.IsEnabled))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -13,8 +13,8 @@ public sealed class RemoveChannelAdCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Ads) .Channels.Include(c => c.Ads)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -3,4 +3,5 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Broadcast.RemoveChannelShow; 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 CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Shows) .Channels.Include(c => c.Shows)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -49,14 +49,14 @@ public sealed class ScheduleGenerator(
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Shows) .Channels.Include(c => c.Shows)
.ThenInclude(s => s.PreferredHours) .ThenInclude(s => s.PreferredHours)
.Include(c => c.Ads) .Include(c => c.Ads)
.Include(c => c.BumperTemplates) .Include(c => c.BumperTemplates)
.ThenInclude(t => t.Variants) .ThenInclude(t => t.Variants)
.Include(c => c.Overrides) .Include(c => c.Overrides)
.ThenInclude(o => o.Shows) .ThenInclude(o => o.Shows)
.AsSplitQuery() .AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == channelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == channelId, cancellationToken);
@@ -67,13 +67,13 @@ public sealed class ScheduleGenerator(
// Чистим прошлое сверх окна ретеншна. // Чистим прошлое сверх окна ретеншна.
var retentionCutoff = now.AddHours(-_options.RetentionHours); var retentionCutoff = now.AddHours(-_options.RetentionHours);
await dbContext.ScheduleEntries await dbContext
.Where(e => e.ChannelId == channelId && e.EndsAtUtc < retentionCutoff) .ScheduleEntries.Where(e => e.ChannelId == channelId && e.EndsAtUtc < retentionCutoff)
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
// Точка продолжения: конец последней сохранённой записи (для regenerate — только уже стартовавшей). // Точка продолжения: конец последней сохранённой записи (для regenerate — только уже стартовавшей).
var lastEnd = await dbContext.ScheduleEntries var lastEnd = await dbContext
.Where(e => .ScheduleEntries.Where(e =>
e.ChannelId == channelId && (!regenerate || e.StartsAtUtc < now) e.ChannelId == channelId && (!regenerate || e.StartsAtUtc < now)
) )
.MaxAsync(e => (DateTimeOffset?)e.EndsAtUtc, cancellationToken); .MaxAsync(e => (DateTimeOffset?)e.EndsAtUtc, cancellationToken);
@@ -83,8 +83,8 @@ public sealed class ScheduleGenerator(
startTime = now; startTime = now;
if (regenerate) if (regenerate)
await dbContext.ScheduleEntries await dbContext
.Where(e => e.ChannelId == channelId && e.StartsAtUtc >= now) .ScheduleEntries.Where(e => e.ChannelId == channelId && e.StartsAtUtc >= now)
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
if (startTime >= horizonEnd) if (startTime >= horizonEnd)
@@ -176,7 +176,9 @@ public sealed class ScheduleGenerator(
/// Для каждой уникальной тройки «из→в→подблок» из запланированных заставок возвращает id готового /// Для каждой уникальной тройки «из→в→подблок» из запланированных заставок возвращает id готового
/// ассета-заставки: из кэша (<see cref="BumperAsset"/>) либо свежесгенерированного. /// ассета-заставки: из кэша (<see cref="BumperAsset"/>) либо свежесгенерированного.
/// </summary> /// </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, Channel channel,
IReadOnlyList<PlannedEntry> entries, IReadOnlyList<PlannedEntry> entries,
IReadOnlyDictionary<Guid, string> showNames, IReadOnlyDictionary<Guid, string> showNames,
@@ -185,8 +187,8 @@ public sealed class ScheduleGenerator(
{ {
var result = new Dictionary<(Guid, Guid, Guid), Guid>(); var result = new Dictionary<(Guid, Guid, Guid), Guid>();
var templatesById = channel.BumperTemplates.ToDictionary(t => t.Id); var templatesById = channel.BumperTemplates.ToDictionary(t => t.Id);
var variantsById = channel.BumperTemplates var variantsById = channel
.SelectMany(t => t.Variants.Select(v => (Variant: v, Template: t))) .BumperTemplates.SelectMany(t => t.Variants.Select(v => (Variant: v, Template: t)))
.ToDictionary(x => x.Variant.Id); .ToDictionary(x => x.Variant.Id);
var combos = entries var combos = entries
.Where(e => .Where(e =>
@@ -195,11 +197,13 @@ public sealed class ScheduleGenerator(
&& e.ToShowId is not null && e.ToShowId is not null
&& e.BumperVariantId is not null && e.BumperVariantId is not null
) )
.Select(e => ( .Select(e =>
From: e.FromShowId!.Value, (
To: e.ToShowId!.Value, From: e.FromShowId!.Value,
Variant: e.BumperVariantId!.Value To: e.ToShowId!.Value,
)) Variant: e.BumperVariantId!.Value
)
)
.Distinct() .Distinct()
.ToList(); .ToList();
if (combos.Count == 0) if (combos.Count == 0)
@@ -211,12 +215,14 @@ public sealed class ScheduleGenerator(
// Постеры шоу-получателей (из реестра изображений) — как фон заставки, если у блока нет // Постеры шоу-получателей (из реестра изображений) — как фон заставки, если у блока нет
// своей фон-картинки. Резолвим id постера → расширение → абсолютный путь. // своей фон-картинки. Резолвим id постера → расширение → абсолютный путь.
var showIds = fromIds.Concat(toIds).Distinct().ToList(); 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) .Where(s => showIds.Contains(s.Id) && s.PosterImageId != null)
.Select(s => new { s.Id, ImageId = s.PosterImageId!.Value }) .Select(s => new { s.Id, ImageId = s.PosterImageId!.Value })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var posterImageIds = posterShows.Select(p => p.ImageId).Distinct().ToList(); 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)) .Where(i => posterImageIds.Contains(i.Id))
.Select(i => new { i.Id, i.FileExtension }) .Select(i => new { i.Id, i.FileExtension })
.ToDictionaryAsync(i => i.Id, i => i.FileExtension, cancellationToken); .ToDictionaryAsync(i => i.Id, i => i.FileExtension, cancellationToken);
@@ -229,12 +235,13 @@ public sealed class ScheduleGenerator(
posterByShow[p.Id] = (p.ImageId, abs); posterByShow[p.Id] = (p.ImageId, abs);
// Фон-картинки блоков (из реестра) — абсолютные пути по id. // Фон-картинки блоков (из реестра) — абсолютные пути по id.
var bgImageIds = channel.BumperTemplates var bgImageIds = channel
.Where(t => t.BackgroundImageId != null) .BumperTemplates.Where(t => t.BackgroundImageId != null)
.Select(t => t.BackgroundImageId!.Value) .Select(t => t.BackgroundImageId!.Value)
.Distinct() .Distinct()
.ToList(); .ToList();
var bgExtById = await dbContext.Images.AsNoTracking() var bgExtById = await dbContext
.Images.AsNoTracking()
.Where(i => bgImageIds.Contains(i.Id)) .Where(i => bgImageIds.Contains(i.Id))
.Select(i => new { i.Id, i.FileExtension }) .Select(i => new { i.Id, i.FileExtension })
.ToDictionaryAsync(i => i.Id, i => i.FileExtension, cancellationToken); .ToDictionaryAsync(i => i.Id, i => i.FileExtension, cancellationToken);
@@ -248,7 +255,8 @@ public sealed class ScheduleGenerator(
bgByTemplate[t.Id] = bgAbs; bgByTemplate[t.Id] = bgAbs;
// Кандидаты из кэша + статусы их ассетов (годятся только Ready — файлы могли удалить). // Кандидаты из кэша + статусы их ассетов (годятся только Ready — файлы могли удалить).
var cached = await dbContext.BumperAssets.AsNoTracking() var cached = await dbContext
.BumperAssets.AsNoTracking()
.Where(b => fromIds.Contains(b.FromShowId) && toIds.Contains(b.ToShowId)) .Where(b => fromIds.Contains(b.FromShowId) && toIds.Contains(b.ToShowId))
.Select(b => new .Select(b => new
{ {
@@ -260,7 +268,8 @@ public sealed class ScheduleGenerator(
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var cachedAssetIds = cached.Select(c => c.MediaAssetId).Distinct().ToList(); 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) .Where(a => cachedAssetIds.Contains(a.Id) && a.Status == MediaAssetStatus.Ready)
.Select(a => a.Id) .Select(a => a.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -281,7 +290,15 @@ public sealed class ScheduleGenerator(
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath; var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
var bgAbs = bgByTemplate.GetValueOrDefault(template.Id); var bgAbs = bgByTemplate.GetValueOrDefault(template.Id);
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template)); 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 => var hit = cached.FirstOrDefault(c =>
c.FromShowId == combo.From c.FromShowId == combo.From
@@ -370,9 +387,7 @@ public sealed class ScheduleGenerator(
); );
dbContext.MediaAssets.Add(asset); dbContext.MediaAssets.Add(asset);
dbContext.BumperAssets.Add( dbContext.BumperAssets.Add(BumperAsset.Create(fromShowId, toShowId, signature, asset.Id));
BumperAsset.Create(fromShowId, toShowId, signature, asset.Id)
);
return asset.Id; return asset.Id;
} }
@@ -475,7 +490,8 @@ public sealed class ScheduleGenerator(
if (showIds.Count == 0) if (showIds.Count == 0)
return new Dictionary<Guid, string>(); return new Dictionary<Guid, string>();
return await dbContext.Shows.AsNoTracking() return await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id)) .Where(s => showIds.Contains(s.Id))
.Select(s => new { s.Id, s.Name }) .Select(s => new { s.Id, s.Name })
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken); .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 enabledShows = channel.Shows.Where(s => s.IsEnabled).ToList();
var showIds = enabledShows.Select(s => s.ShowId).Distinct().ToList(); var showIds = enabledShows.Select(s => s.ShowId).Distinct().ToList();
var shows = await dbContext.Shows var shows = await dbContext
.Include(s => s.Episodes) .Shows.Include(s => s.Episodes)
.Where(s => showIds.Contains(s.Id)) .Where(s => showIds.Contains(s.Id))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -501,14 +517,14 @@ public sealed class ScheduleGenerator(
s => s.Episodes.OrderBy(e => e.Position).Select(e => e.MediaAssetId).ToList() s => s.Episodes.OrderBy(e => e.Position).Select(e => e.MediaAssetId).ToList()
); );
var candidateAssetIds = episodesByShow.Values var candidateAssetIds = episodesByShow
.SelectMany(x => x) .Values.SelectMany(x => x)
.Concat(channel.Ads.Select(a => a.MediaAssetId)) .Concat(channel.Ads.Select(a => a.MediaAssetId))
.Distinct() .Distinct()
.ToList(); .ToList();
var durations = await dbContext.MediaAssets var durations = await dbContext
.Where(a => .MediaAssets.Where(a =>
candidateAssetIds.Contains(a.Id) candidateAssetIds.Contains(a.Id)
&& a.Status == MediaAssetStatus.Ready && a.Status == MediaAssetStatus.Ready
&& a.Duration != null && a.Duration != null
@@ -534,34 +550,34 @@ public sealed class ScheduleGenerator(
channelShow.BlockValue, channelShow.BlockValue,
ready, ready,
channelShow.NextEpisodeIndex, channelShow.NextEpisodeIndex,
channelShow.PreferredHours channelShow
.Select(h => new PlannerHourWindow(h.StartHour, h.EndHour)) .PreferredHours.Select(h => new PlannerHourWindow(h.StartHour, h.EndHour))
.ToList(), .ToList(),
channelShow.PreferredWeightMultiplier channelShow.PreferredWeightMultiplier
) )
); );
} }
var adPool = channel.Ads var adPool = channel
.OrderBy(a => a.Position) .Ads.OrderBy(a => a.Position)
.Select(a => a.MediaAssetId) .Select(a => a.MediaAssetId)
.Where(durations.ContainsKey) .Where(durations.ContainsKey)
.ToList(); .ToList();
// Подблоки заставок (плоский список): длительность слота — по звуку блока, выровнена на сегмент. // Подблоки заставок (плоский список): длительность слота — по звуку блока, выровнена на сегмент.
var bumperVariants = channel.BumperTemplates var bumperVariants = channel
.OrderBy(t => t.Position) .BumperTemplates.OrderBy(t => t.Position)
.SelectMany(t => .SelectMany(t =>
{ {
var dur = TimeSpan.FromSeconds(AlignedDurationSeconds(TemplateDurationSeconds(t))); var dur = TimeSpan.FromSeconds(AlignedDurationSeconds(TemplateDurationSeconds(t)));
return t.Variants return t
.OrderBy(v => v.Position) .Variants.OrderBy(v => v.Position)
.Select(v => new PlannerBumperVariant(v.Id, t.Id, dur, v.Trigger, v.Weight)); .Select(v => new PlannerBumperVariant(v.Id, t.Id, dur, v.Trigger, v.Weight));
}) })
.ToList(); .ToList();
var overrides = channel.Overrides var overrides = channel
.Select(o => new PlannerOverride( .Overrides.Select(o => new PlannerOverride(
o.Mode, o.Mode,
o.Shows.Select(s => new PlannerOverrideShow(s.ShowId, s.Weight)).ToList(), o.Shows.Select(s => new PlannerOverrideShow(s.ShowId, s.Weight)).ToList(),
o.Recurrence, o.Recurrence,
@@ -22,7 +22,10 @@ public sealed class UpdateChannelSettingsCommandHandler(IAppDbContext dbContext)
if (command.FillerAssetId is { } fillerId) 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) if (!exists)
return Result.Failure(ChannelErrors.AssetNotFound); return Result.Failure(ChannelErrors.AssetNotFound);
} }
@@ -13,9 +13,9 @@ public sealed class UpdateChannelShowCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels var channel = await dbContext
.Include(c => c.Shows) .Channels.Include(c => c.Shows)
.ThenInclude(s => s.PreferredHours) .ThenInclude(s => s.PreferredHours)
.FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken); .FirstOrDefaultAsync(c => c.Id == command.ChannelId, cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure(ChannelErrors.NotFound); return Result.Failure(ChannelErrors.NotFound);
@@ -24,7 +24,12 @@ public sealed class UpdateChannelShowCommandHandler(IAppDbContext dbContext)
if (channelShow is null) if (channelShow is null)
return Result.Failure(ChannelErrors.ChannelShowNotFound); 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( channelShow.SetPreferredHours(
command.PreferredWeightMultiplier, command.PreferredWeightMultiplier,
command.PreferredHours.Select(h => (h.StartHour, h.EndHour)) 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.Weight).InclusiveBetween(1, 1000);
RuleFor(x => x.BlockValue).InclusiveBetween(1, 10000); RuleFor(x => x.BlockValue).InclusiveBetween(1, 10000);
RuleFor(x => x.PreferredWeightMultiplier).InclusiveBetween(1, 100); RuleFor(x => x.PreferredWeightMultiplier).InclusiveBetween(1, 100);
RuleForEach(x => x.PreferredHours).ChildRules(w => 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.StartHour).InclusiveBetween(0, 23);
w.RuleFor(h => h).Must(h => h.StartHour < h.EndHour) w.RuleFor(h => h.EndHour).InclusiveBetween(1, 24);
.WithMessage("Начало окна должно быть раньше конца."); 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; 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( public sealed record UserSummaryDto(
Guid Id, Guid Id,
@@ -15,7 +15,11 @@ public interface IMediaStorage
/// Стримит загружаемый контент во временный файл в <c>uploads/</c> без буферизации в память. /// Стримит загружаемый контент во временный файл в <c>uploads/</c> без буферизации в память.
/// Возвращает непрозрачный токен (имя временного файла) для последующего <see cref="PromoteToOriginalAsync"/>. /// Возвращает непрозрачный токен (имя временного файла) для последующего <see cref="PromoteToOriginalAsync"/>.
/// </summary> /// </summary>
Task<string> SaveUploadAsync(Stream content, string extension, CancellationToken cancellationToken); Task<string> SaveUploadAsync(
Stream content,
string extension,
CancellationToken cancellationToken
);
/// <summary>Удаляет временный файл загрузки (откат при ошибке до регистрации ассета).</summary> /// <summary>Удаляет временный файл загрузки (откат при ошибке до регистрации ассета).</summary>
void DeleteUpload(string uploadToken); void DeleteUpload(string uploadToken);
@@ -29,13 +29,20 @@ public static class DependencyInjection
return services; 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 }) .Where(t => t is { IsClass: true, IsAbstract: false })
.SelectMany(t => t.GetInterfaces() .SelectMany(t =>
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface) t.GetInterfaces()
.Select(i => (Service: i, Implementation: t))); .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface)
.Select(i => (Service: i, Implementation: t))
);
foreach (var (service, implementation) in implementations) foreach (var (service, implementation) in implementations)
services.AddScoped(service, implementation); services.AddScoped(service, implementation);
@@ -8,7 +8,10 @@ namespace TeleWave.Application.Images.DeleteImage;
public sealed class DeleteImageCommandHandler(IAppDbContext dbContext, IImageStore storage) public sealed class DeleteImageCommandHandler(IAppDbContext dbContext, IImageStore storage)
: ICommandHandler<DeleteImageCommand, Result> : 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( var image = await dbContext.Images.FirstOrDefaultAsync(
i => i.Id == command.Id, i => i.Id == command.Id,
@@ -13,7 +13,8 @@ public sealed class GetImageFileQueryHandler(IAppDbContext dbContext, IImageStor
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var image = await dbContext.Images.AsNoTracking() var image = await dbContext
.Images.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == query.Id, cancellationToken); .FirstOrDefaultAsync(i => i.Id == query.Id, cancellationToken);
if (image is null) if (image is null)
return Result.Failure<string>(ImageErrors.NotFound); return Result.Failure<string>(ImageErrors.NotFound);
@@ -13,7 +13,8 @@ public sealed class ListImagesQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var images = await dbContext.Images.AsNoTracking() var images = await dbContext
.Images.AsNoTracking()
.Where(i => i.Category == query.Category) .Where(i => i.Category == query.Category)
.OrderByDescending(i => i.CreatedAt) .OrderByDescending(i => i.CreatedAt)
.Select(i => new ImageDto(i.Id, i.Category, i.OriginalFileName, 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) public sealed class UploadImageCommandHandler(IAppDbContext dbContext)
: ICommandHandler<UploadImageCommand, Result<Guid>> : 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); var image = Image.Create(command.Category, command.Extension, command.OriginalFileName);
dbContext.Images.Add(image); dbContext.Images.Add(image);
@@ -13,8 +13,8 @@ public sealed class AddEpisodeCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var show = await dbContext.Shows var show = await dbContext
.Include(s => s.Episodes) .Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null) if (show is null)
return Result.Failure<Guid>(ShowErrors.NotFound); return Result.Failure<Guid>(ShowErrors.NotFound);
@@ -22,8 +22,8 @@ public sealed class AddEpisodeCommandHandler(IAppDbContext dbContext)
if (!show.CanAddEpisode) if (!show.CanAddEpisode)
return Result.Failure<Guid>(ShowErrors.SingleAlreadyHasEpisode); return Result.Failure<Guid>(ShowErrors.SingleAlreadyHasEpisode);
var fileName = await dbContext.MediaAssets var fileName = await dbContext
.Where(a => a.Id == command.MediaAssetId) .MediaAssets.Where(a => a.Id == command.MediaAssetId)
.Select(a => a.OriginalFileName) .Select(a => a.OriginalFileName)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (fileName is null) if (fileName is null)
@@ -10,7 +10,12 @@ public sealed class CreateShowCommandHandler(IAppDbContext dbContext)
{ {
public Task<Result<Guid>> Handle(CreateShowCommand command, CancellationToken cancellationToken) 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); dbContext.Shows.Add(show);
return Task.FromResult(Result.Success(show.Id)); return Task.FromResult(Result.Success(show.Id));
} }
@@ -8,9 +8,13 @@ namespace TeleWave.Application.Library.GetShow;
public sealed class GetShowQueryHandler(IAppDbContext dbContext) public sealed class GetShowQueryHandler(IAppDbContext dbContext)
: IQueryHandler<GetShowQuery, Result<ShowDto>> : 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) .Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == query.Id, cancellationToken); .FirstOrDefaultAsync(s => s.Id == query.Id, cancellationToken);
if (show is null) if (show is null)
@@ -18,7 +22,8 @@ public sealed class GetShowQueryHandler(IAppDbContext dbContext)
var episodes = show.Episodes.OrderBy(e => e.Position).ToList(); var episodes = show.Episodes.OrderBy(e => e.Position).ToList();
var assetIds = episodes.Select(e => e.MediaAssetId).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)) .Where(a => assetIds.Contains(a.Id))
.Select(a => new .Select(a => new
{ {
@@ -12,14 +12,19 @@ public sealed class ListShowsQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var shows = await dbContext.Shows.AsNoTracking() var shows = await dbContext
.Shows.AsNoTracking()
.Include(s => s.Episodes) .Include(s => s.Episodes)
.OrderBy(s => s.Name) .OrderBy(s => s.Name)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// Имена ассетов нужны, чтобы распознать сезоны (номера в модели не хранятся). // Имена ассетов нужны, чтобы распознать сезоны (номера в модели не хранятся).
var assetIds = shows.SelectMany(s => s.Episodes.Select(e => e.MediaAssetId)).Distinct().ToList(); var assetIds = shows
var names = await dbContext.MediaAssets.AsNoTracking() .SelectMany(s => s.Episodes.Select(e => e.MediaAssetId))
.Distinct()
.ToList();
var names = await dbContext
.MediaAssets.AsNoTracking()
.Where(a => assetIds.Contains(a.Id)) .Where(a => assetIds.Contains(a.Id))
.Select(a => new { a.Id, a.OriginalFileName }) .Select(a => new { a.Id, a.OriginalFileName })
.ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken); .ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);
@@ -27,8 +32,12 @@ public sealed class ListShowsQueryHandler(IAppDbContext dbContext)
return shows return shows
.Select(s => .Select(s =>
{ {
var seasons = s.Episodes var seasons = s
.Select(e => names.TryGetValue(e.MediaAssetId, out var n) ? EpisodeName.ParseSeason(n) : null) .Episodes.Select(e =>
names.TryGetValue(e.MediaAssetId, out var n)
? EpisodeName.ParseSeason(n)
: null
)
.Where(season => season is not null) .Where(season => season is not null)
.Distinct() .Distinct()
.Count(); .Count();
@@ -13,8 +13,8 @@ public sealed class RemoveEpisodeCommandHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var show = await dbContext.Shows var show = await dbContext
.Include(s => s.Episodes) .Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null) if (show is null)
return Result.Failure(ShowErrors.NotFound); return Result.Failure(ShowErrors.NotFound);
@@ -13,8 +13,8 @@ public sealed class ClearAllMediaCommandHandler(IAppDbContext dbContext, IMediaS
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var assets = await dbContext.MediaAssets var assets = await dbContext
.Select(a => new { a.Id, a.OriginalExtension }) .MediaAssets.Select(a => new { a.Id, a.OriginalExtension })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
foreach (var asset in assets) foreach (var asset in assets)
@@ -14,8 +14,8 @@ public sealed class DeleteShowMediaCommandHandler(IAppDbContext dbContext, IMedi
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var show = await dbContext.Shows var show = await dbContext
.Include(s => s.Episodes) .Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null) if (show is null)
return Result.Failure<int>(ShowErrors.NotFound); return Result.Failure<int>(ShowErrors.NotFound);
@@ -23,19 +23,19 @@ public sealed class DeleteShowMediaCommandHandler(IAppDbContext dbContext, IMedi
var episodes = show.Episodes.ToList(); var episodes = show.Episodes.ToList();
var assetIds = episodes.Select(e => e.MediaAssetId).Distinct().ToList(); var assetIds = episodes.Select(e => e.MediaAssetId).Distinct().ToList();
var assets = await dbContext.MediaAssets var assets = await dbContext
.Where(a => assetIds.Contains(a.Id)) .MediaAssets.Where(a => assetIds.Contains(a.Id))
.Select(a => new { a.Id, a.OriginalExtension }) .Select(a => new { a.Id, a.OriginalExtension })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
foreach (var asset in assets) foreach (var asset in assets)
storage.DeleteAssetArtifacts(asset.Id, asset.OriginalExtension); storage.DeleteAssetArtifacts(asset.Id, asset.OriginalExtension);
await dbContext.ScheduleEntries await dbContext
.Where(e => assetIds.Contains(e.MediaAssetId)) .ScheduleEntries.Where(e => assetIds.Contains(e.MediaAssetId))
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
await dbContext.MediaAssets await dbContext
.Where(a => assetIds.Contains(a.Id)) .MediaAssets.Where(a => assetIds.Contains(a.Id))
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
// Серии шоу теперь указывают на удалённые ассеты — убираем их (сохранится через UnitOfWork). // Серии шоу теперь указывают на удалённые ассеты — убираем их (сохранится через UnitOfWork).
@@ -13,7 +13,8 @@ public sealed class GetMediaAssetQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var asset = await dbContext.MediaAssets.AsNoTracking() var asset = await dbContext
.MediaAssets.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == query.Id, cancellationToken); .FirstOrDefaultAsync(x => x.Id == query.Id, cancellationToken);
return asset is null return asset is null
@@ -15,8 +15,7 @@ public sealed class ListMediaAssetsQueryHandler(IAppDbContext dbContext)
) )
{ {
// Сгенерированные системой ассеты (ТВ-заставки) не показываем в библиотеке медиа. // Сгенерированные системой ассеты (ТВ-заставки) не показываем в библиотеке медиа.
var q = dbContext.MediaAssets.AsNoTracking() var q = dbContext.MediaAssets.AsNoTracking().Where(x => x.Source != MediaSource.Generated);
.Where(x => x.Source != MediaSource.Generated);
if (query.Statuses.Count > 0) if (query.Statuses.Count > 0)
q = q.Where(x => query.Statuses.Contains(x.Status)); q = q.Where(x => query.Statuses.Contains(x.Status));
@@ -6,10 +6,8 @@ using TeleWave.Domain.Media;
namespace TeleWave.Application.Media.Register; namespace TeleWave.Application.Media.Register;
public sealed class RegisterMediaAssetCommandHandler( public sealed class RegisterMediaAssetCommandHandler(IAppDbContext dbContext, IMediaStorage storage)
IAppDbContext dbContext, : ICommandHandler<RegisterMediaAssetCommand, Result<Guid>>
IMediaStorage storage
) : ICommandHandler<RegisterMediaAssetCommand, Result<Guid>>
{ {
public async Task<Result<Guid>> Handle( public async Task<Result<Guid>> Handle(
RegisterMediaAssetCommand command, RegisterMediaAssetCommand command,
@@ -2,7 +2,8 @@ using FluentValidation;
namespace TeleWave.Application.Media.Register; namespace TeleWave.Application.Media.Register;
public sealed class RegisterMediaAssetCommandValidator : AbstractValidator<RegisterMediaAssetCommand> public sealed class RegisterMediaAssetCommandValidator
: AbstractValidator<RegisterMediaAssetCommand>
{ {
public RegisterMediaAssetCommandValidator() 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(); return Result.Success();
} }
} }
@@ -19,8 +19,8 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var show = await dbContext.Shows var show = await dbContext
.Include(s => s.Episodes) .Shows.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
if (show is null) if (show is null)
return Result.Failure<int>(ShowErrors.NotFound); 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 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)) .Where(a => assetIds.Contains(a.Id))
.Select(a => new { a.Id, a.OriginalFileName }) .Select(a => new { a.Id, a.OriginalFileName })
.ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken); .ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);
@@ -14,7 +14,9 @@ public sealed class SearchShowMetadataQueryHandler(IMetadataProviderResolver res
{ {
var provider = resolver.Resolve(query.Provider); var provider = resolver.Resolve(query.Provider);
if (provider is null) if (provider is null)
return Result.Failure<IReadOnlyList<MetadataCandidate>>(MetadataErrors.ProviderNotAvailable); return Result.Failure<IReadOnlyList<MetadataCandidate>>(
MetadataErrors.ProviderNotAvailable
);
if (string.IsNullOrWhiteSpace(query.Query)) if (string.IsNullOrWhiteSpace(query.Query))
return Result.Success<IReadOnlyList<MetadataCandidate>>([]); return Result.Success<IReadOnlyList<MetadataCandidate>>([]);
@@ -13,7 +13,8 @@ public static class AppSettingsReader
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var value = await db.AppSettings.AsNoTracking() var value = await db
.AppSettings.AsNoTracking()
.Where(s => s.Key == key) .Where(s => s.Key == key)
.Select(s => s.Value) .Select(s => s.Value)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -13,7 +13,10 @@ public sealed class UpdateSiteSettingsCommandHandler(ISiteSettings siteSettings)
) )
{ {
// Сохранение выполняет UnitOfWorkBehavior команды. // Сохранение выполняет UnitOfWorkBehavior команды.
await siteSettings.SetRegistrationEnabledAsync(command.RegistrationEnabled, cancellationToken); await siteSettings.SetRegistrationEnabledAsync(
command.RegistrationEnabled,
cancellationToken
);
return Result.Success(); return Result.Success();
} }
} }
@@ -21,9 +21,15 @@ public sealed class GetLivePlaylistQueryHandler(
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channel = await dbContext.Channels.AsNoTracking() var channel = await dbContext
.Channels.AsNoTracking()
.Where(c => c.Slug == query.Slug && c.IsEnabled) .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); .FirstOrDefaultAsync(cancellationToken);
if (channel is null) if (channel is null)
return Result.Failure<LivePlaylistDto>(ChannelErrors.NotFound); return Result.Failure<LivePlaylistDto>(ChannelErrors.NotFound);
@@ -33,7 +39,8 @@ public sealed class GetLivePlaylistQueryHandler(
// Загружаем записи, пересекающиеся с окном (с небольшим запасом назад). // Загружаем записи, пересекающиеся с окном (с небольшим запасом назад).
var windowStartTime = query.Now.AddSeconds(-(window + 2) * seg); var windowStartTime = query.Now.AddSeconds(-(window + 2) * seg);
var rawEntries = await dbContext.ScheduleEntries.AsNoTracking() var rawEntries = await dbContext
.ScheduleEntries.AsNoTracking()
.Where(e => .Where(e =>
e.ChannelId == channel.Id e.ChannelId == channel.Id
&& e.StartsAtUtc < query.Now && e.StartsAtUtc < query.Now
@@ -52,7 +59,8 @@ public sealed class GetLivePlaylistQueryHandler(
if (channel.FillerAssetId is { } fillerId) if (channel.FillerAssetId is { } fillerId)
assetIds.Add(fillerId); assetIds.Add(fillerId);
var segmentCounts = await dbContext.MediaAssets.AsNoTracking() var segmentCounts = await dbContext
.MediaAssets.AsNoTracking()
.Where(a => .Where(a =>
assetIds.Contains(a.Id) assetIds.Contains(a.Id)
&& a.Status == MediaAssetStatus.Ready && a.Status == MediaAssetStatus.Ready
@@ -72,7 +80,10 @@ public sealed class GetLivePlaylistQueryHandler(
.ToList(); .ToList();
LiveFiller? filler = null; 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); filler = new LiveFiller(fid, fillerSegments);
var playlist = LiveWindowCalculator.Build( var playlist = LiveWindowCalculator.Build(
@@ -82,8 +93,8 @@ public sealed class GetLivePlaylistQueryHandler(
var dto = new LivePlaylistDto( var dto = new LivePlaylistDto(
playlist.MediaSequence, playlist.MediaSequence,
playlist.TargetDuration, playlist.TargetDuration,
playlist.Segments playlist
.Select(s => new LiveSegmentDto(s.AssetId, s.LocalIndex, s.Discontinuity)) .Segments.Select(s => new LiveSegmentDto(s.AssetId, s.LocalIndex, s.Discontinuity))
.ToList() .ToList()
); );
return Result.Success(dto); return Result.Success(dto);
@@ -14,14 +14,16 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channelId = await dbContext.Channels.AsNoTracking() var channelId = await dbContext
.Channels.AsNoTracking()
.Where(c => c.Slug == query.Slug && c.IsEnabled) .Where(c => c.Slug == query.Slug && c.IsEnabled)
.Select(c => (Guid?)c.Id) .Select(c => (Guid?)c.Id)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (channelId is null) if (channelId is null)
return Result.Failure<IReadOnlyList<PublicEpgEntryDto>>(ChannelErrors.NotFound); return Result.Failure<IReadOnlyList<PublicEpgEntryDto>>(ChannelErrors.NotFound);
var entries = await dbContext.ScheduleEntries.AsNoTracking() var entries = await dbContext
.ScheduleEntries.AsNoTracking()
.Where(e => .Where(e =>
e.ChannelId == channelId e.ChannelId == channelId
&& e.StartsAtUtc < query.ToUtc && e.StartsAtUtc < query.ToUtc
@@ -38,15 +40,26 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
}) })
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var showIds = entries.Where(e => e.ShowId != null).Select(e => e.ShowId!.Value).Distinct().ToList(); var showIds = entries
var shows = await dbContext.Shows.AsNoTracking() .Where(e => e.ShowId != null)
.Select(e => e.ShowId!.Value)
.Distinct()
.ToList();
var shows = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id)) .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); .ToDictionaryAsync(s => s.Id, cancellationToken);
// Метаданные серий: ключ — (шоу, ассет). // Метаданные серий: ключ — (шоу, ассет).
var assetIds = entries.Select(e => e.MediaAssetId).Distinct().ToList(); 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) .SelectMany(s => s.Episodes)
.Where(e => showIds.Contains(e.ShowId) && assetIds.Contains(e.MediaAssetId)) .Where(e => showIds.Contains(e.ShowId) && assetIds.Contains(e.MediaAssetId))
.Select(e => new .Select(e => new
@@ -13,10 +13,16 @@ public sealed class ListPublicChannelsQueryHandler(IAppDbContext dbContext)
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var channels = await dbContext.Channels.AsNoTracking() var channels = await dbContext
.Channels.AsNoTracking()
.Where(c => c.IsEnabled) .Where(c => c.IsEnabled)
.OrderBy(c => c.Name) .OrderBy(c => c.Name)
.Select(c => new { c.Id, c.Slug, c.Name }) .Select(c => new
{
c.Id,
c.Slug,
c.Name,
})
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (channels.Count == 0) if (channels.Count == 0)
return []; return [];
@@ -25,7 +31,8 @@ public sealed class ListPublicChannelsQueryHandler(IAppDbContext dbContext)
var channelIds = channels.Select(c => c.Id).ToList(); var channelIds = channels.Select(c => c.Id).ToList();
// Что идёт прямо сейчас на каждом канале (программа) — для постера-обложки плитки. // Что идёт прямо сейчас на каждом канале (программа) — для постера-обложки плитки.
var currentByChannel = await dbContext.ScheduleEntries.AsNoTracking() var currentByChannel = await dbContext
.ScheduleEntries.AsNoTracking()
.Where(e => .Where(e =>
channelIds.Contains(e.ChannelId) channelIds.Contains(e.ChannelId)
&& e.Kind == ScheduleEntryKind.Program && e.Kind == ScheduleEntryKind.Program
@@ -40,9 +47,15 @@ public sealed class ListPublicChannelsQueryHandler(IAppDbContext dbContext)
.ToDictionary(g => g.Key, g => g.First().ShowId); .ToDictionary(g => g.Key, g => g.First().ShowId);
var showIds = currentShowByChannel.Values.Distinct().ToList(); var showIds = currentShowByChannel.Values.Distinct().ToList();
var shows = await dbContext.Shows.AsNoTracking() var shows = await dbContext
.Shows.AsNoTracking()
.Where(s => showIds.Contains(s.Id)) .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); .ToDictionaryAsync(s => s.Id, cancellationToken);
return channels return channels
@@ -131,7 +131,8 @@ public class Channel
/// <summary>Добавить блок заставки в конец списка. Возвращает созданный блок.</summary> /// <summary>Добавить блок заставки в конец списка. Возвращает созданный блок.</summary>
public BumperTemplate AddBumperTemplate(string name) public BumperTemplate AddBumperTemplate(string name)
{ {
var nextPosition = _bumperTemplates.Count == 0 ? 0 : _bumperTemplates.Max(t => t.Position) + 1; var nextPosition =
_bumperTemplates.Count == 0 ? 0 : _bumperTemplates.Max(t => t.Position) + 1;
var template = BumperTemplate.Create(Id, nextPosition, name); var template = BumperTemplate.Create(Id, nextPosition, name);
_bumperTemplates.Add(template); _bumperTemplates.Add(template);
return template; return template;
@@ -153,7 +154,8 @@ public class Channel
/// <summary>Планировщик двигает курсор ротации блоков заставок по мере вставки.</summary> /// <summary>Планировщик двигает курсор ротации блоков заставок по мере вставки.</summary>
public void SetNextBumperIndex(int index) => NextBumperIndex = index; public void SetNextBumperIndex(int index) => NextBumperIndex = index;
public ChannelShow? FindShow(Guid channelShowId) => _shows.FirstOrDefault(s => s.Id == channelShowId); public ChannelShow? FindShow(Guid channelShowId) =>
_shows.FirstOrDefault(s => s.Id == channelShowId);
public ChannelShow AddShow(Guid showId, int weight, BlockMode blockMode, int blockValue) public ChannelShow AddShow(Guid showId, int weight, BlockMode blockMode, int blockValue)
{ {
@@ -69,9 +69,11 @@ public class ChannelShow
{ {
PreferredWeightMultiplier = Math.Max(1, multiplier); PreferredWeightMultiplier = Math.Max(1, multiplier);
_preferredHours.Clear(); _preferredHours.Clear();
foreach (var (start, end) in windows foreach (
.Where(w => w.StartHour >= 0 && w.EndHour <= 24 && w.StartHour < w.EndHour) var (start, end) in windows
.Distinct()) .Where(w => w.StartHour >= 0 && w.EndHour <= 24 && w.StartHour < w.EndHour)
.Distinct()
)
{ {
_preferredHours.Add(ChannelShowHour.Create(Id, start, end)); _preferredHours.Add(ChannelShowHour.Create(Id, start, end));
} }
@@ -62,7 +62,9 @@ public static class LiveWindowCalculator
LiveInput input LiveInput input
) )
{ {
var entry = input.Entries.FirstOrDefault(e => segTime >= e.StartsAtUtc && segTime < e.EndsAtUtc); var entry = input.Entries.FirstOrDefault(e =>
segTime >= e.StartsAtUtc && segTime < e.EndsAtUtc
);
if (entry is not null) if (entry is not null)
{ {
var local = (int)Math.Floor((segTime - entry.StartsAtUtc).TotalSeconds / seg); var local = (int)Math.Floor((segTime - entry.StartsAtUtc).TotalSeconds / seg);
@@ -72,7 +74,9 @@ public static class LiveWindowCalculator
if (input.Filler is { SegmentCount: > 0 } filler) if (input.Filler is { SegmentCount: > 0 } filler)
{ {
var local = (int)(((globalIndex % filler.SegmentCount) + filler.SegmentCount) % filler.SegmentCount); var local = (int)(
((globalIndex % filler.SegmentCount) + filler.SegmentCount) % filler.SegmentCount
);
return (filler.AssetId, local); return (filler.AssetId, local);
} }
@@ -57,7 +57,17 @@ public static class SchedulePlanner
if (RollChance(chance, random)) if (RollChance(chance, random))
{ {
var bumperStart = cursor; var bumperStart = cursor;
if (TryPlaceBumper(entries, bumper, prev, pick.ShowId, random, ref nextBumper, ref cursor)) if (
TryPlaceBumper(
entries,
bumper,
prev,
pick.ShowId,
random,
ref nextBumper,
ref cursor
)
)
lastBumperAt = bumperStart; lastBumperAt = bumperStart;
} }
} }
@@ -115,8 +125,10 @@ public static class SchedulePlanner
) )
{ {
var isShowChange = fromShowId != toShowId; var isShowChange = fromShowId != toShowId;
var eligible = bumper.Variants var eligible = bumper
.Where(v => v.Duration > TimeSpan.Zero && MatchesTrigger(v.Trigger, isShowChange)) .Variants.Where(v =>
v.Duration > TimeSpan.Zero && MatchesTrigger(v.Trigger, isShowChange)
)
.ToList(); .ToList();
if (eligible.Count == 0) if (eligible.Count == 0)
return false; return false;
@@ -211,7 +223,10 @@ public static class SchedulePlanner
var overridden = new List<(PlannerShow, int)>(); var overridden = new List<(PlannerShow, int)>();
foreach (var os in ovr.Shows) foreach (var os in ovr.Shows)
{ {
if (!byShowId.TryGetValue(os.ShowId, out var show) || show.EpisodeAssetIds.Count == 0) if (
!byShowId.TryGetValue(os.ShowId, out var show)
|| show.EpisodeAssetIds.Count == 0
)
continue; continue;
var weight = ovr.Mode == OverrideMode.Exclusive ? 1 : os.Weight; var weight = ovr.Mode == OverrideMode.Exclusive ? 1 : os.Weight;
if (weight > 0) if (weight > 0)
@@ -224,8 +239,8 @@ public static class SchedulePlanner
} }
var hour = moment.UtcDateTime.Hour; var hour = moment.UtcDateTime.Hour;
return input.Shows return input
.Where(s => s.Weight > 0 && s.EpisodeAssetIds.Count > 0) .Shows.Where(s => s.Weight > 0 && s.EpisodeAssetIds.Count > 0)
.Select(s => (s, EffectiveWeight(s, hour))) .Select(s => (s, EffectiveWeight(s, hour)))
.ToList(); .ToList();
} }
@@ -318,7 +333,9 @@ public static class SchedulePlanner
for (var i = 0; i < input.AdsPerBreak; i++) for (var i = 0; i < input.AdsPerBreak; i++)
{ {
var assetId = input.AdPool[((nextAd % input.AdPool.Count) + input.AdPool.Count) % input.AdPool.Count]; var assetId = input.AdPool[
((nextAd % input.AdPool.Count) + input.AdPool.Count) % input.AdPool.Count
];
nextAd++; nextAd++;
var end = cursor + DurationOf(assetId, input); var end = cursor + DurationOf(assetId, input);
entries.Add(new PlannedEntry(assetId, ScheduleEntryKind.Ad, cursor, end, null, null)); entries.Add(new PlannedEntry(assetId, ScheduleEntryKind.Ad, cursor, end, null, null));
+8 -2
View File
@@ -21,13 +21,19 @@ public class Image
private Image() { } private Image() { }
public static Image Create(ImageCategory category, string fileExtension, string? originalFileName) => public static Image Create(
ImageCategory category,
string fileExtension,
string? originalFileName
) =>
new() new()
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Category = category, Category = category,
FileExtension = Normalize(fileExtension), FileExtension = Normalize(fileExtension),
OriginalFileName = string.IsNullOrWhiteSpace(originalFileName) ? null : originalFileName.Trim(), OriginalFileName = string.IsNullOrWhiteSpace(originalFileName)
? null
: originalFileName.Trim(),
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
}; };
@@ -44,7 +44,12 @@ public class ShowEpisode
} }
/// <summary>Применить метаданные серии (кадр — уже зарегистрирован в реестре — может быть null).</summary> /// <summary>Применить метаданные серии (кадр — уже зарегистрирован в реестре — может быть null).</summary>
public void ApplyMetadata(string? title, string? overview, Guid? stillImageId, DateOnly? airDate) public void ApplyMetadata(
string? title,
string? overview,
Guid? stillImageId,
DateOnly? airDate
)
{ {
Title = title; Title = title;
Overview = overview; Overview = overview;
@@ -12,8 +12,7 @@ public class AppSetting
private AppSetting() { } private AppSetting() { }
public static AppSetting Create(string key, string value) => public static AppSetting Create(string key, string value) => new() { Key = key, Value = value };
new() { Key = key, Value = value };
public void SetValue(string value) => Value = value; public void SetValue(string value) => Value = value;
} }
@@ -59,15 +59,20 @@ public sealed class SchedulingBackgroundService(
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>(); var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
var generator = scope.ServiceProvider.GetRequiredService<ScheduleGenerator>(); var generator = scope.ServiceProvider.GetRequiredService<ScheduleGenerator>();
var channelIds = await db.Channels var channelIds = await db
.Where(c => c.IsEnabled) .Channels.Where(c => c.IsEnabled)
.Select(c => c.Id) .Select(c => c.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
foreach (var channelId in channelIds) foreach (var channelId in channelIds)
{ {
var added = await generator.GenerateAsync(channelId, now, regenerate: false, cancellationToken); var added = await generator.GenerateAsync(
channelId,
now,
regenerate: false,
cancellationToken
);
if (added > 0) if (added > 0)
logger.LogInformation( logger.LogInformation(
"Канал {ChannelId}: добавлено {Count} записей расписания", "Канал {ChannelId}: добавлено {Count} записей расписания",
@@ -115,8 +115,12 @@ public static class DependencyInjection
/// <summary>Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта.</summary> /// <summary>Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта.</summary>
private static void AddBroadcast(IServiceCollection services, IConfiguration configuration) private static void AddBroadcast(IServiceCollection services, IConfiguration configuration)
{ {
services.Configure<SchedulerOptions>(configuration.GetSection(SchedulerOptions.SectionName)); services.Configure<SchedulerOptions>(
services.Configure<StreamingOptions>(configuration.GetSection(StreamingOptions.SectionName)); configuration.GetSection(SchedulerOptions.SectionName)
);
services.Configure<StreamingOptions>(
configuration.GetSection(StreamingOptions.SectionName)
);
services.Configure<BumperOptions>(configuration.GetSection(BumperOptions.SectionName)); services.Configure<BumperOptions>(configuration.GetSection(BumperOptions.SectionName));
services.AddSingleton<IRandomSource, SystemRandomSource>(); services.AddSingleton<IRandomSource, SystemRandomSource>();
@@ -74,7 +74,13 @@ internal sealed class IdentityService(
var role = await GetPrimaryRoleAsync(user); var role = await GetPrimaryRoleAsync(user);
var roleEntity = await roleManager.FindByNameAsync(role); var roleEntity = await roleManager.FindByNameAsync(role);
return new CurrentUserProfile(user.Id, user.UserName!, roleEntity?.Id ?? Guid.Empty, role, user.IsBlocked); return new CurrentUserProfile(
user.Id,
user.UserName!,
roleEntity?.Id ?? Guid.Empty,
role,
user.IsBlocked
);
} }
public async Task<Result> ChangePasswordAsync( public async Task<Result> ChangePasswordAsync(
@@ -201,7 +207,12 @@ internal sealed class IdentityService(
from userRole in userRoles.DefaultIfEmpty() from userRole in userRoles.DefaultIfEmpty()
join role in dbContext.Roles on userRole.RoleId equals role.Id into roles join role in dbContext.Roles on userRole.RoleId equals role.Id into roles
from role in roles.DefaultIfEmpty() from role in roles.DefaultIfEmpty()
select new { user, RoleId = (Guid?)userRole.RoleId, RoleName = role != null ? role.Name : null }; select new
{
user,
RoleId = (Guid?)userRole.RoleId,
RoleName = role != null ? role.Name : null,
};
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
query = query.Where(x => x.user.UserName!.Contains(search)); query = query.Where(x => x.user.UserName!.Contains(search));
@@ -230,7 +241,10 @@ internal sealed class IdentityService(
return new PagedList<UserSummaryDto>(items, total, page, pageSize); return new PagedList<UserSummaryDto>(items, total, page, pageSize);
} }
public async Task<UserSummaryDto?> GetUserAsync(Guid userId, CancellationToken cancellationToken) public async Task<UserSummaryDto?> GetUserAsync(
Guid userId,
CancellationToken cancellationToken
)
{ {
var user = await userManager.FindByIdAsync(userId.ToString()); var user = await userManager.FindByIdAsync(userId.ToString());
if (user is null) if (user is null)
@@ -6,10 +6,15 @@ using TeleWave.Application.Common.Models;
namespace TeleWave.Infrastructure.Identity; namespace TeleWave.Infrastructure.Identity;
internal sealed class RoleService(RoleManager<AppRole> roleManager, UserManager<AppUser> userManager) internal sealed class RoleService(
: IRoleService RoleManager<AppRole> roleManager,
UserManager<AppUser> userManager
) : IRoleService
{ {
public async Task<Result<RoleDto>> CreateRoleAsync(string name, CancellationToken cancellationToken) public async Task<Result<RoleDto>> CreateRoleAsync(
string name,
CancellationToken cancellationToken
)
{ {
if (await roleManager.RoleExistsAsync(name)) if (await roleManager.RoleExistsAsync(name))
return Result.Failure<RoleDto>(RoleErrors.DuplicateName); return Result.Failure<RoleDto>(RoleErrors.DuplicateName);
@@ -42,10 +47,7 @@ internal sealed class RoleService(RoleManager<AppRole> roleManager, UserManager<
if (role.IsSystem) if (role.IsSystem)
return Result.Failure<RoleDto>(RoleErrors.CannotModifySystemRole); return Result.Failure<RoleDto>(RoleErrors.CannotModifySystemRole);
if ( if (await roleManager.FindByNameAsync(name) is { } existing && existing.Id != roleId)
await roleManager.FindByNameAsync(name) is { } existing
&& existing.Id != roleId
)
return Result.Failure<RoleDto>(RoleErrors.DuplicateName); return Result.Failure<RoleDto>(RoleErrors.DuplicateName);
role.Name = name; role.Name = name;
@@ -60,11 +60,15 @@ public sealed class FfmpegBumperRenderer(
var playlist = Path.Combine(assetDir, "index.m3u8"); var playlist = Path.Combine(assetDir, "index.m3u8");
if (!File.Exists(playlist)) if (!File.Exists(playlist))
throw new InvalidOperationException("ffmpeg не создал плейлист заставки index.m3u8."); throw new InvalidOperationException(
"ffmpeg не создал плейлист заставки index.m3u8."
);
var segmentCount = Directory.GetFiles(assetDir, "seg*.ts").Length; var segmentCount = Directory.GetFiles(assetDir, "seg*.ts").Length;
if (segmentCount == 0) if (segmentCount == 0)
throw new InvalidOperationException("ffmpeg не создал ни одного сегмента заставки."); throw new InvalidOperationException(
"ffmpeg не создал ни одного сегмента заставки."
);
return new BumperRenderResult( return new BumperRenderResult(
TimeSpan.FromSeconds(target), TimeSpan.FromSeconds(target),
@@ -168,17 +172,33 @@ public sealed class FfmpegBumperRenderer(
var line2Size = FitSize(spec.FreeLine2, titleSize, textWidth); var line2Size = FitSize(spec.FreeLine2, titleSize, textWidth);
var line1Y = (int)(h * 0.40); var line1Y = (int)(h * 0.40);
var line2Y = line1Y + (int)(line2Size * 1.2); var line2Y = line1Y + (int)(line2Size * 1.2);
vchain.Append(',').Append(DrawTitle(font, nowFile, spec.AccentColor, line1Size, line1Y, 0.2)); vchain
vchain.Append(',').Append(DrawTitle(font, nextFile, spec.TextColor, line2Size, line2Y, 0.5)); .Append(',')
.Append(DrawTitle(font, nowFile, spec.AccentColor, line1Size, line1Y, 0.2));
vchain
.Append(',')
.Append(DrawTitle(font, nextFile, spec.TextColor, line2Size, line2Y, 0.5));
} }
else else
{ {
var nowSize = FitSize(spec.NowTitle, titleSize, textWidth); var nowSize = FitSize(spec.NowTitle, titleSize, textWidth);
var nextSize = FitSize(spec.NextTitle, titleSize, textWidth); var nextSize = FitSize(spec.NextTitle, titleSize, textWidth);
vchain.Append(',').Append(DrawLabel(font, spec.NowLabel, spec.AccentColor, labelSize, nowLabelY, 0.2)); vchain
vchain.Append(',').Append(DrawTitle(font, nowFile, spec.TextColor, nowSize, nowTitleY, 0.3)); .Append(',')
vchain.Append(',').Append(DrawLabel(font, spec.NextLabel, spec.AccentColor, labelSize, nextLabelY, 1.0)); .Append(
vchain.Append(',').Append(DrawTitle(font, nextFile, spec.TextColor, nextSize, nextTitleY, 1.1)); DrawLabel(font, spec.NowLabel, spec.AccentColor, labelSize, nowLabelY, 0.2)
);
vchain
.Append(',')
.Append(DrawTitle(font, nowFile, spec.TextColor, nowSize, nowTitleY, 0.3));
vchain
.Append(',')
.Append(
DrawLabel(font, spec.NextLabel, spec.AccentColor, labelSize, nextLabelY, 1.0)
);
vchain
.Append(',')
.Append(DrawTitle(font, nextFile, spec.TextColor, nextSize, nextTitleY, 1.1));
} }
vchain.Append("[v]"); vchain.Append("[v]");
@@ -186,28 +206,47 @@ public sealed class FfmpegBumperRenderer(
var args = new List<string> { "-hide_banner", "-nostdin", "-y" }; var args = new List<string> { "-hide_banner", "-nostdin", "-y" };
args.AddRange(inputs); args.AddRange(inputs);
args.AddRange( args.AddRange([
[ "-filter_complex",
"-filter_complex", filterComplex, filterComplex,
"-map", "[v]", "-map",
"-map", "[a]", "[v]",
"-threads", _media.TranscodeThreads.ToString(CultureInfo.InvariantCulture), "-map",
"-c:v", "libx264", "[a]",
"-preset", "veryfast", "-threads",
"-crf", "21", _media.TranscodeThreads.ToString(CultureInfo.InvariantCulture),
"-pix_fmt", "yuv420p", "-c:v",
"-force_key_frames", $"expr:gte(t,n_forced*{seg.ToString(CultureInfo.InvariantCulture)})", "libx264",
"-sc_threshold", "0", "-preset",
"-c:a", "aac", "veryfast",
"-b:a", "128k", "-crf",
"-ac", "2", "21",
"-ar", "48000", "-pix_fmt",
"-t", Fmt(target), "yuv420p",
"-f", "hls", "-force_key_frames",
"-hls_time", seg.ToString(CultureInfo.InvariantCulture), $"expr:gte(t,n_forced*{seg.ToString(CultureInfo.InvariantCulture)})",
"-hls_playlist_type", "vod", "-sc_threshold",
"-hls_list_size", "0", "0",
"-hls_segment_filename", Path.Combine(assetDir, "seg%05d.ts"), "-c:a",
"aac",
"-b:a",
"128k",
"-ac",
"2",
"-ar",
"48000",
"-t",
Fmt(target),
"-f",
"hls",
"-hls_time",
seg.ToString(CultureInfo.InvariantCulture),
"-hls_playlist_type",
"vod",
"-hls_list_size",
"0",
"-hls_segment_filename",
Path.Combine(assetDir, "seg%05d.ts"),
Path.Combine(assetDir, "index.m3u8"), Path.Combine(assetDir, "index.m3u8"),
]); ]);
return args; return args;
@@ -261,8 +300,7 @@ public sealed class FfmpegBumperRenderer(
/// <summary>Экранирование пути для значения опции фильтра (Windows-разделители → прямые слэши, /// <summary>Экранирование пути для значения опции фильтра (Windows-разделители → прямые слэши,
/// двоеточие экранируется). На Linux (контейнере) — фактически no-op.</summary> /// двоеточие экранируется). На Linux (контейнере) — фактически no-op.</summary>
private static string EscapePath(string path) => private static string EscapePath(string path) => path.Replace('\\', '/').Replace(":", "\\:");
path.Replace('\\', '/').Replace(":", "\\:");
/// <summary>Экранирование литерального текста подписи внутри значения опции drawtext.</summary> /// <summary>Экранирование литерального текста подписи внутри значения опции drawtext.</summary>
private static string EscapeText(string text) => private static string EscapeText(string text) =>
@@ -146,21 +146,19 @@ public sealed class FfmpegMediaProcessor(
args.Add(Fmt(target)); args.Add(Fmt(target));
} }
args.AddRange( args.AddRange([
[ "-f",
"-f", "hls",
"hls", "-hls_time",
"-hls_time", segmentSeconds.ToString(CultureInfo.InvariantCulture),
segmentSeconds.ToString(CultureInfo.InvariantCulture), "-hls_playlist_type",
"-hls_playlist_type", "vod",
"vod", "-hls_list_size",
"-hls_list_size", "0",
"0", "-hls_segment_filename",
"-hls_segment_filename", Path.Combine(assetDir, "seg%05d.ts"),
Path.Combine(assetDir, "seg%05d.ts"), Path.Combine(assetDir, "index.m3u8"),
Path.Combine(assetDir, "index.m3u8"), ]);
]
);
return args; return args;
} }
@@ -169,15 +167,7 @@ public sealed class FfmpegMediaProcessor(
{ {
var result = await ProcessRunner.RunAsync( var result = await ProcessRunner.RunAsync(
_media.FfprobePath, _media.FfprobePath,
[ ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", path],
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
"-show_streams",
path,
],
lowPriority: false, lowPriority: false,
cancellationToken cancellationToken
); );
@@ -58,9 +58,10 @@ public sealed class FileSystemMediaStorage(MediaPathResolver paths) : IMediaStor
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var sourcePath = source == MediaSource.Inbox var sourcePath =
? paths.InboxPath(sourceToken) source == MediaSource.Inbox
: paths.UploadPath(sourceToken); ? paths.InboxPath(sourceToken)
: paths.UploadPath(sourceToken);
if (!File.Exists(sourcePath)) if (!File.Exists(sourcePath))
throw new FileNotFoundException("Исходный файл не найден в хранилище.", sourcePath); throw new FileNotFoundException("Исходный файл не найден в хранилище.", sourcePath);
@@ -5,7 +5,10 @@ namespace TeleWave.Infrastructure.Media;
/// <summary>Скачивает изображение по URL через HTTP-клиент «metadata» и определяет расширение.</summary> /// <summary>Скачивает изображение по URL через HTTP-клиент «metadata» и определяет расширение.</summary>
public sealed class ImageDownloader(IHttpClientFactory httpFactory) : IImageDownloader public sealed class ImageDownloader(IHttpClientFactory httpFactory) : IImageDownloader
{ {
public async Task<DownloadedImage?> DownloadAsync(string url, CancellationToken cancellationToken) public async Task<DownloadedImage?> DownloadAsync(
string url,
CancellationToken cancellationToken
)
{ {
try try
{ {
@@ -18,7 +21,8 @@ public sealed class ImageDownloader(IHttpClientFactory httpFactory) : IImageDown
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken); var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
return bytes.Length == 0 ? null : new DownloadedImage(bytes, ext); return bytes.Length == 0 ? null : new DownloadedImage(bytes, ext);
} }
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or IOException) catch (Exception ex)
when (ex is HttpRequestException or TaskCanceledException or IOException)
{ {
return null; return null;
} }
@@ -29,7 +29,9 @@ public sealed class InboxScannerBackgroundService(
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
paths.EnsureDirectories(); paths.EnsureDirectories();
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(Math.Max(1, _media.InboxScanSeconds))); using var timer = new PeriodicTimer(
TimeSpan.FromSeconds(Math.Max(1, _media.InboxScanSeconds))
);
while (await timer.WaitForNextTickAsync(stoppingToken)) while (await timer.WaitForNextTickAsync(stoppingToken))
{ {
@@ -53,7 +55,8 @@ public sealed class InboxScannerBackgroundService(
if (!Directory.Exists(paths.InboxDir)) if (!Directory.Exists(paths.InboxDir))
return; return;
var files = Directory.EnumerateFiles(paths.InboxDir) var files = Directory
.EnumerateFiles(paths.InboxDir)
.Where(f => MediaFormats.IsAllowed(f)) .Where(f => MediaFormats.IsAllowed(f))
.ToList(); .ToList();
@@ -108,11 +111,19 @@ public sealed class InboxScannerBackgroundService(
if (result.IsSuccess) if (result.IsSuccess)
{ {
queue.Enqueue(result.Value); queue.Enqueue(result.Value);
logger.LogInformation("Из inbox зарегистрирован ассет {AssetId} ({File})", result.Value, fileName); logger.LogInformation(
"Из inbox зарегистрирован ассет {AssetId} ({File})",
result.Value,
fileName
);
} }
else else
{ {
logger.LogWarning("Не удалось зарегистрировать {File} из inbox: {Error}", fileName, result.Error.Code); logger.LogWarning(
"Не удалось зарегистрировать {File} из inbox: {Error}",
fileName,
result.Error.Code
);
} }
} }
} }
@@ -72,10 +72,7 @@ public sealed class MediaProcessingBackgroundService(
CancellationToken.None CancellationToken.None
); );
inFlight[task] = 0; inFlight[task] = 0;
_ = task.ContinueWith( _ = task.ContinueWith(t => inFlight.TryRemove(t, out _), TaskScheduler.Default);
t => inFlight.TryRemove(t, out _),
TaskScheduler.Default
);
} }
// Работы нет — ждём сигнала о новой либо периодического опроса. // Работы нет — ждём сигнала о новой либо периодического опроса.
@@ -118,8 +115,8 @@ public sealed class MediaProcessingBackgroundService(
await using var scope = scopeFactory.CreateAsyncScope(); await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>(); var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
var interrupted = await db.MediaAssets var interrupted = await db
.Where(x => x.Status == MediaAssetStatus.Processing) .MediaAssets.Where(x => x.Status == MediaAssetStatus.Processing)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (interrupted.Count == 0) if (interrupted.Count == 0)
return; return;
@@ -134,13 +131,15 @@ public sealed class MediaProcessingBackgroundService(
/// либо null если работы нет. Вызывается только диспетчером последовательно, поэтому два транскода /// либо null если работы нет. Вызывается только диспетчером последовательно, поэтому два транскода
/// не возьмут один ассет. /// не возьмут один ассет.
/// </summary> /// </summary>
private async Task<(Guid Id, string Extension)?> ClaimNextAsync(CancellationToken cancellationToken) private async Task<(Guid Id, string Extension)?> ClaimNextAsync(
CancellationToken cancellationToken
)
{ {
await using var scope = scopeFactory.CreateAsyncScope(); await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>(); var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
var asset = await db.MediaAssets var asset = await db
.Where(x => x.Status == MediaAssetStatus.Pending) .MediaAssets.Where(x => x.Status == MediaAssetStatus.Pending)
.OrderBy(x => x.CreatedAt) .OrderBy(x => x.CreatedAt)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (asset is null) if (asset is null)
@@ -152,7 +151,11 @@ public sealed class MediaProcessingBackgroundService(
} }
/// <summary>Обрабатывает уже захваченный (Processing) ассет: транскод → Ready/Failed.</summary> /// <summary>Обрабатывает уже захваченный (Processing) ассет: транскод → Ready/Failed.</summary>
private async Task ProcessClaimedAsync(Guid assetId, string extension, CancellationToken cancellationToken) private async Task ProcessClaimedAsync(
Guid assetId,
string extension,
CancellationToken cancellationToken
)
{ {
try try
{ {
@@ -189,7 +192,10 @@ public sealed class MediaProcessingBackgroundService(
await using var scope = scopeFactory.CreateAsyncScope(); await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>(); var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
var asset = await db.MediaAssets.FirstOrDefaultAsync(x => x.Id == assetId, cancellationToken); var asset = await db.MediaAssets.FirstOrDefaultAsync(
x => x.Id == assetId,
cancellationToken
);
if (asset is null) if (asset is null)
return; return;
@@ -211,7 +217,10 @@ public sealed class MediaProcessingBackgroundService(
await using var scope = scopeFactory.CreateAsyncScope(); await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>(); var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
var asset = await db.MediaAssets.FirstOrDefaultAsync(x => x.Id == assetId, cancellationToken); var asset = await db.MediaAssets.FirstOrDefaultAsync(
x => x.Id == assetId,
cancellationToken
);
if (asset is null) if (asset is null)
return; return;
@@ -51,7 +51,8 @@ internal static class ProcessRunner
{ {
process.PriorityClass = ProcessPriorityClass.BelowNormal; process.PriorityClass = ProcessPriorityClass.BelowNormal;
} }
catch (Exception ex) when (ex is InvalidOperationException or PlatformNotSupportedException) catch (Exception ex)
when (ex is InvalidOperationException or PlatformNotSupportedException)
{ {
// Процесс мог завершиться мгновенно или платформа не поддерживает — не критично. // Процесс мог завершиться мгновенно или платформа не поддерживает — не критично.
} }
@@ -7,8 +7,10 @@ using TeleWave.Application.Metadata;
namespace TeleWave.Infrastructure.Metadata; namespace TeleWave.Infrastructure.Metadata;
/// <summary>Провайдер метаданных OMDb (omdbapi.com, данные IMDb). Требует API-ключ.</summary> /// <summary>Провайдер метаданных OMDb (omdbapi.com, данные IMDb). Требует API-ключ.</summary>
public sealed class OmdbMetadataProvider(IHttpClientFactory httpFactory, IOptions<MetadataOptions> options) public sealed class OmdbMetadataProvider(
: IMetadataProvider IHttpClientFactory httpFactory,
IOptions<MetadataOptions> options
) : IMetadataProvider
{ {
private readonly OmdbOptions _omdb = options.Value.Omdb; private readonly OmdbOptions _omdb = options.Value.Omdb;
@@ -19,7 +21,8 @@ public sealed class OmdbMetadataProvider(IHttpClientFactory httpFactory, IOption
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&type=series&s={Uri.EscapeDataString(query)}"; var url =
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&type=series&s={Uri.EscapeDataString(query)}";
using var doc = await GetJsonAsync(url, cancellationToken); using var doc = await GetJsonAsync(url, cancellationToken);
if (!doc.RootElement.TryGetProperty("Search", out var search)) if (!doc.RootElement.TryGetProperty("Search", out var search))
return []; return [];
@@ -43,7 +46,10 @@ public sealed class OmdbMetadataProvider(IHttpClientFactory httpFactory, IOption
return list; return list;
} }
public async Task<ShowMetadata?> GetShowAsync(string externalId, CancellationToken cancellationToken) public async Task<ShowMetadata?> GetShowAsync(
string externalId,
CancellationToken cancellationToken
)
{ {
var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}"; var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}";
using var doc = await TryGetJsonAsync(url, cancellationToken); using var doc = await TryGetJsonAsync(url, cancellationToken);
@@ -92,23 +98,30 @@ public sealed class OmdbMetadataProvider(IHttpClientFactory httpFactory, IOption
} }
/// <summary>Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко).</summary> /// <summary>Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко).</summary>
private async Task<JsonDocument?> TryGetJsonAsync(string url, CancellationToken cancellationToken) private async Task<JsonDocument?> TryGetJsonAsync(
string url,
CancellationToken cancellationToken
)
{ {
try try
{ {
return await GetJsonAsync(url, cancellationToken); return await GetJsonAsync(url, cancellationToken);
} }
catch (Exception ex) when (ex is HttpRequestException or JsonException or TaskCanceledException) catch (Exception ex)
when (ex is HttpRequestException or JsonException or TaskCanceledException)
{ {
return null; return null;
} }
} }
private static bool IsResponseTrue(JsonElement root) => private static bool IsResponseTrue(JsonElement root) =>
GetString(root, "Response") is { } r && r.Equals("True", StringComparison.OrdinalIgnoreCase); GetString(root, "Response") is { } r
&& r.Equals("True", StringComparison.OrdinalIgnoreCase);
private static string? GetString(JsonElement el, string name) => private static string? GetString(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
/// <summary>OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null.</summary> /// <summary>OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null.</summary>
private static string? Clean(string? value) => private static string? Clean(string? value) =>
@@ -7,8 +7,10 @@ using TeleWave.Application.Metadata;
namespace TeleWave.Infrastructure.Metadata; namespace TeleWave.Infrastructure.Metadata;
/// <summary>Провайдер метаданных TMDb (themoviedb.org). Требует API-ключ (v3).</summary> /// <summary>Провайдер метаданных TMDb (themoviedb.org). Требует API-ключ (v3).</summary>
public sealed class TmdbMetadataProvider(IHttpClientFactory httpFactory, IOptions<MetadataOptions> options) public sealed class TmdbMetadataProvider(
: IMetadataProvider IHttpClientFactory httpFactory,
IOptions<MetadataOptions> options
) : IMetadataProvider
{ {
private readonly MetadataOptions _options = options.Value; private readonly MetadataOptions _options = options.Value;
@@ -47,9 +49,13 @@ public sealed class TmdbMetadataProvider(IHttpClientFactory httpFactory, IOption
return list; return list;
} }
public async Task<ShowMetadata?> GetShowAsync(string externalId, CancellationToken cancellationToken) public async Task<ShowMetadata?> GetShowAsync(
string externalId,
CancellationToken cancellationToken
)
{ {
var url = $"{Tmdb.BaseUrl}/tv/{externalId}?api_key={Tmdb.ApiKey}&language={_options.Language}"; var url =
$"{Tmdb.BaseUrl}/tv/{externalId}?api_key={Tmdb.ApiKey}&language={_options.Language}";
using var doc = await TryGetJsonAsync(url, cancellationToken); using var doc = await TryGetJsonAsync(url, cancellationToken);
if (doc is null) if (doc is null)
return null; return null;
@@ -102,23 +108,31 @@ public sealed class TmdbMetadataProvider(IHttpClientFactory httpFactory, IOption
} }
/// <summary>Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко).</summary> /// <summary>Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко).</summary>
private async Task<JsonDocument?> TryGetJsonAsync(string url, CancellationToken cancellationToken) private async Task<JsonDocument?> TryGetJsonAsync(
string url,
CancellationToken cancellationToken
)
{ {
try try
{ {
return await GetJsonAsync(url, cancellationToken); return await GetJsonAsync(url, cancellationToken);
} }
catch (Exception ex) when (ex is HttpRequestException or JsonException or TaskCanceledException) catch (Exception ex)
when (ex is HttpRequestException or JsonException or TaskCanceledException)
{ {
return null; return null;
} }
} }
private static string? GetString(JsonElement el, string name) => private static string? GetString(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
private static int? GetInt(JsonElement el, string name) => private static int? GetInt(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetInt32() : null; el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number
? v.GetInt32()
: null;
private static int? YearFrom(string? date) => private static int? YearFrom(string? date) =>
date is { Length: >= 4 } && int.TryParse(date.AsSpan(0, 4), out var y) ? y : null; date is { Length: >= 4 } && int.TryParse(date.AsSpan(0, 4), out var y) ? y : null;
@@ -18,14 +18,23 @@ namespace TeleWave.Infrastructure.Migrations
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
IsSystem = table.Column<bool>(type: "boolean", nullable: false), IsSystem = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), Name = table.Column<string>(
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), type: "character varying(256)",
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true) maxLength: 256,
nullable: true
),
NormalizedName = table.Column<string>(
type: "character varying(256)",
maxLength: 256,
nullable: true
),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_AspNetRoles", x => x.Id); table.PrimaryKey("PK_AspNetRoles", x => x.Id);
}); }
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AspNetUsers", name: "AspNetUsers",
@@ -33,11 +42,30 @@ namespace TeleWave.Infrastructure.Migrations
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
IsBlocked = table.Column<bool>(type: "boolean", nullable: false), IsBlocked = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), CreatedAt = table.Column<DateTimeOffset>(
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), type: "timestamp with time zone",
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), nullable: false
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), ),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), UserName = table.Column<string>(
type: "character varying(256)",
maxLength: 256,
nullable: true
),
NormalizedUserName = table.Column<string>(
type: "character varying(256)",
maxLength: 256,
nullable: true
),
Email = table.Column<string>(
type: "character varying(256)",
maxLength: 256,
nullable: true
),
NormalizedEmail = table.Column<string>(
type: "character varying(256)",
maxLength: 256,
nullable: true
),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false), EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true), PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true), SecurityStamp = table.Column<string>(type: "text", nullable: true),
@@ -45,14 +73,18 @@ namespace TeleWave.Infrastructure.Migrations
PhoneNumber = table.Column<string>(type: "text", nullable: true), PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false), PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false), TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), LockoutEnd = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: true
),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false), LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false) AccessFailedCount = table.Column<int>(type: "integer", nullable: false),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_AspNetUsers", x => x.Id); table.PrimaryKey("PK_AspNetUsers", x => x.Id);
}); }
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "RefreshTokens", name: "RefreshTokens",
@@ -61,25 +93,39 @@ namespace TeleWave.Infrastructure.Migrations
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
TokenHash = table.Column<string>(type: "text", nullable: false), TokenHash = table.Column<string>(type: "text", nullable: false),
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), ExpiresAt = table.Column<DateTimeOffset>(
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), type: "timestamp with time zone",
RevokedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), nullable: false
ReplacedByTokenHash = table.Column<string>(type: "text", nullable: true) ),
CreatedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
RevokedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: true
),
ReplacedByTokenHash = table.Column<string>(type: "text", nullable: true),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_RefreshTokens", x => x.Id); table.PrimaryKey("PK_RefreshTokens", x => x.Id);
}); }
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AspNetRoleClaims", name: "AspNetRoleClaims",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(type: "integer", nullable: false) Id = table
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Column<int>(type: "integer", nullable: false)
.Annotation(
"Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
),
RoleId = table.Column<Guid>(type: "uuid", nullable: false), RoleId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true), ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true) ClaimValue = table.Column<string>(type: "text", nullable: true),
}, },
constraints: table => constraints: table =>
{ {
@@ -89,18 +135,24 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.RoleId, column: x => x.RoleId,
principalTable: "AspNetRoles", principalTable: "AspNetRoles",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AspNetUserClaims", name: "AspNetUserClaims",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(type: "integer", nullable: false) Id = table
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Column<int>(type: "integer", nullable: false)
.Annotation(
"Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true), ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true) ClaimValue = table.Column<string>(type: "text", nullable: true),
}, },
constraints: table => constraints: table =>
{ {
@@ -110,8 +162,10 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.UserId, column: x => x.UserId,
principalTable: "AspNetUsers", principalTable: "AspNetUsers",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AspNetUserLogins", name: "AspNetUserLogins",
@@ -120,25 +174,30 @@ namespace TeleWave.Infrastructure.Migrations
LoginProvider = table.Column<string>(type: "text", nullable: false), LoginProvider = table.Column<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false), ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true), ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<Guid>(type: "uuid", nullable: false) UserId = table.Column<Guid>(type: "uuid", nullable: false),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); table.PrimaryKey(
"PK_AspNetUserLogins",
x => new { x.LoginProvider, x.ProviderKey }
);
table.ForeignKey( table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId", name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId, column: x => x.UserId,
principalTable: "AspNetUsers", principalTable: "AspNetUsers",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AspNetUserRoles", name: "AspNetUserRoles",
columns: table => new columns: table => new
{ {
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
RoleId = table.Column<Guid>(type: "uuid", nullable: false) RoleId = table.Column<Guid>(type: "uuid", nullable: false),
}, },
constraints: table => constraints: table =>
{ {
@@ -148,14 +207,17 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.RoleId, column: x => x.RoleId,
principalTable: "AspNetRoles", principalTable: "AspNetRoles",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
);
table.ForeignKey( table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId", name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId, column: x => x.UserId,
principalTable: "AspNetUsers", principalTable: "AspNetUsers",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AspNetUserTokens", name: "AspNetUserTokens",
@@ -164,94 +226,105 @@ namespace TeleWave.Infrastructure.Migrations
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false), LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false), Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: true) Value = table.Column<string>(type: "text", nullable: true),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); table.PrimaryKey(
"PK_AspNetUserTokens",
x => new
{
x.UserId,
x.LoginProvider,
x.Name,
}
);
table.ForeignKey( table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId", name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId, column: x => x.UserId,
principalTable: "AspNetUsers", principalTable: "AspNetUsers",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId", name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims", table: "AspNetRoleClaims",
column: "RoleId"); column: "RoleId"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "RoleNameIndex", name: "RoleNameIndex",
table: "AspNetRoles", table: "AspNetRoles",
column: "NormalizedName", column: "NormalizedName",
unique: true); unique: true
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId", name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims", table: "AspNetUserClaims",
column: "UserId"); column: "UserId"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId", name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins", table: "AspNetUserLogins",
column: "UserId"); column: "UserId"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId", name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles", table: "AspNetUserRoles",
column: "RoleId"); column: "RoleId"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "EmailIndex", name: "EmailIndex",
table: "AspNetUsers", table: "AspNetUsers",
column: "NormalizedEmail"); column: "NormalizedEmail"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "UserNameIndex", name: "UserNameIndex",
table: "AspNetUsers", table: "AspNetUsers",
column: "NormalizedUserName", column: "NormalizedUserName",
unique: true); unique: true
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_RefreshTokens_TokenHash", name: "IX_RefreshTokens_TokenHash",
table: "RefreshTokens", table: "RefreshTokens",
column: "TokenHash", column: "TokenHash",
unique: true); unique: true
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_RefreshTokens_UserId", name: "IX_RefreshTokens_UserId",
table: "RefreshTokens", table: "RefreshTokens",
column: "UserId"); column: "UserId"
);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "AspNetRoleClaims");
name: "AspNetRoleClaims");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "AspNetUserClaims");
name: "AspNetUserClaims");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "AspNetUserLogins");
name: "AspNetUserLogins");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "AspNetUserRoles");
name: "AspNetUserRoles");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "AspNetUserTokens");
name: "AspNetUserTokens");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "RefreshTokens");
name: "RefreshTokens");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "AspNetRoles");
name: "AspNetRoles");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "AspNetUsers");
name: "AspNetUsers");
} }
} }
} }
@@ -16,8 +16,16 @@ namespace TeleWave.Infrastructure.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
OriginalFileName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false), OriginalFileName = table.Column<string>(
OriginalExtension = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false), type: "character varying(512)",
maxLength: 512,
nullable: false
),
OriginalExtension = table.Column<string>(
type: "character varying(16)",
maxLength: 16,
nullable: false
),
Source = table.Column<int>(type: "integer", nullable: false), Source = table.Column<int>(type: "integer", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false), Status = table.Column<int>(type: "integer", nullable: false),
Duration = table.Column<TimeSpan>(type: "interval", nullable: true), Duration = table.Column<TimeSpan>(type: "interval", nullable: true),
@@ -25,34 +33,58 @@ namespace TeleWave.Infrastructure.Migrations
SegmentCount = table.Column<int>(type: "integer", nullable: true), SegmentCount = table.Column<int>(type: "integer", nullable: true),
Width = table.Column<int>(type: "integer", nullable: true), Width = table.Column<int>(type: "integer", nullable: true),
Height = table.Column<int>(type: "integer", nullable: true), Height = table.Column<int>(type: "integer", nullable: true),
VideoCodec = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), VideoCodec = table.Column<string>(
AudioCodec = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), type: "character varying(32)",
RelativePath = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), maxLength: 32,
ErrorMessage = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true), nullable: true
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), ),
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false) AudioCodec = table.Column<string>(
type: "character varying(32)",
maxLength: 32,
nullable: true
),
RelativePath = table.Column<string>(
type: "character varying(256)",
maxLength: 256,
nullable: true
),
ErrorMessage = table.Column<string>(
type: "character varying(2048)",
maxLength: 2048,
nullable: true
),
CreatedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
UpdatedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_MediaAssets", x => x.Id); table.PrimaryKey("PK_MediaAssets", x => x.Id);
}); }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MediaAssets_CreatedAt", name: "IX_MediaAssets_CreatedAt",
table: "MediaAssets", table: "MediaAssets",
column: "CreatedAt"); column: "CreatedAt"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_MediaAssets_Status", name: "IX_MediaAssets_Status",
table: "MediaAssets", table: "MediaAssets",
column: "Status"); column: "Status"
);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "MediaAssets");
name: "MediaAssets");
} }
} }
} }
@@ -16,20 +16,35 @@ namespace TeleWave.Infrastructure.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Name = table.Column<string>(
Slug = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), type: "character varying(256)",
maxLength: 256,
nullable: false
),
Slug = table.Column<string>(
type: "character varying(128)",
maxLength: 128,
nullable: false
),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false), IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
EpochUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), EpochUtc = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
AdInsertion = table.Column<int>(type: "integer", nullable: false), AdInsertion = table.Column<int>(type: "integer", nullable: false),
AdsPerBreak = table.Column<int>(type: "integer", nullable: false), AdsPerBreak = table.Column<int>(type: "integer", nullable: false),
FillerAssetId = table.Column<Guid>(type: "uuid", nullable: true), FillerAssetId = table.Column<Guid>(type: "uuid", nullable: true),
NextAdIndex = table.Column<int>(type: "integer", nullable: false), NextAdIndex = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false) CreatedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Channels", x => x.Id); table.PrimaryKey("PK_Channels", x => x.Id);
}); }
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ScheduleEntries", name: "ScheduleEntries",
@@ -39,30 +54,49 @@ namespace TeleWave.Infrastructure.Migrations
ChannelId = table.Column<Guid>(type: "uuid", nullable: false), ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false), MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false),
Kind = table.Column<int>(type: "integer", nullable: false), Kind = table.Column<int>(type: "integer", nullable: false),
StartsAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), StartsAtUtc = table.Column<DateTimeOffset>(
EndsAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), type: "timestamp with time zone",
nullable: false
),
EndsAtUtc = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
ShowId = table.Column<Guid>(type: "uuid", nullable: true), ShowId = table.Column<Guid>(type: "uuid", nullable: true),
EpisodeIndex = table.Column<int>(type: "integer", nullable: true) EpisodeIndex = table.Column<int>(type: "integer", nullable: true),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_ScheduleEntries", x => x.Id); table.PrimaryKey("PK_ScheduleEntries", x => x.Id);
}); }
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Shows", name: "Shows",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Name = table.Column<string>(
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true), type: "character varying(256)",
maxLength: 256,
nullable: false
),
Description = table.Column<string>(
type: "character varying(2048)",
maxLength: 2048,
nullable: true
),
Kind = table.Column<int>(type: "integer", nullable: false), Kind = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false) CreatedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Shows", x => x.Id); table.PrimaryKey("PK_Shows", x => x.Id);
}); }
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ChannelAd", name: "ChannelAd",
@@ -71,7 +105,7 @@ namespace TeleWave.Infrastructure.Migrations
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
ChannelId = table.Column<Guid>(type: "uuid", nullable: false), ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false), MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false),
Position = table.Column<int>(type: "integer", nullable: false) Position = table.Column<int>(type: "integer", nullable: false),
}, },
constraints: table => constraints: table =>
{ {
@@ -81,8 +115,10 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.ChannelId, column: x => x.ChannelId,
principalTable: "Channels", principalTable: "Channels",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ChannelShow", name: "ChannelShow",
@@ -95,7 +131,7 @@ namespace TeleWave.Infrastructure.Migrations
BlockMode = table.Column<int>(type: "integer", nullable: false), BlockMode = table.Column<int>(type: "integer", nullable: false),
BlockValue = table.Column<int>(type: "integer", nullable: false), BlockValue = table.Column<int>(type: "integer", nullable: false),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false), IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
NextEpisodeIndex = table.Column<int>(type: "integer", nullable: false) NextEpisodeIndex = table.Column<int>(type: "integer", nullable: false),
}, },
constraints: table => constraints: table =>
{ {
@@ -105,8 +141,10 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.ChannelId, column: x => x.ChannelId,
principalTable: "Channels", principalTable: "Channels",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ProgrammingOverride", name: "ProgrammingOverride",
@@ -115,8 +153,14 @@ namespace TeleWave.Infrastructure.Migrations
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
ChannelId = table.Column<Guid>(type: "uuid", nullable: false), ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
Mode = table.Column<int>(type: "integer", nullable: false), Mode = table.Column<int>(type: "integer", nullable: false),
StartsAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), StartsAtUtc = table.Column<DateTimeOffset>(
EndsAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false) type: "timestamp with time zone",
nullable: false
),
EndsAtUtc = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
}, },
constraints: table => constraints: table =>
{ {
@@ -126,8 +170,10 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.ChannelId, column: x => x.ChannelId,
principalTable: "Channels", principalTable: "Channels",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ShowEpisode", name: "ShowEpisode",
@@ -137,7 +183,10 @@ namespace TeleWave.Infrastructure.Migrations
ShowId = table.Column<Guid>(type: "uuid", nullable: false), ShowId = table.Column<Guid>(type: "uuid", nullable: false),
MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false), MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false),
Position = table.Column<int>(type: "integer", nullable: false), Position = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false) CreatedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
}, },
constraints: table => constraints: table =>
{ {
@@ -147,8 +196,10 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.ShowId, column: x => x.ShowId,
principalTable: "Shows", principalTable: "Shows",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "OverrideShow", name: "OverrideShow",
@@ -157,7 +208,7 @@ namespace TeleWave.Infrastructure.Migrations
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
ProgrammingOverrideId = table.Column<Guid>(type: "uuid", nullable: false), ProgrammingOverrideId = table.Column<Guid>(type: "uuid", nullable: false),
ShowId = table.Column<Guid>(type: "uuid", nullable: false), ShowId = table.Column<Guid>(type: "uuid", nullable: false),
Weight = table.Column<int>(type: "integer", nullable: false) Weight = table.Column<int>(type: "integer", nullable: false),
}, },
constraints: table => constraints: table =>
{ {
@@ -167,87 +218,91 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.ProgrammingOverrideId, column: x => x.ProgrammingOverrideId,
principalTable: "ProgrammingOverride", principalTable: "ProgrammingOverride",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ChannelAd_ChannelId_Position", name: "IX_ChannelAd_ChannelId_Position",
table: "ChannelAd", table: "ChannelAd",
columns: new[] { "ChannelId", "Position" }); columns: new[] { "ChannelId", "Position" }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Channels_Slug", name: "IX_Channels_Slug",
table: "Channels", table: "Channels",
column: "Slug", column: "Slug",
unique: true); unique: true
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ChannelShow_ChannelId_ShowId", name: "IX_ChannelShow_ChannelId_ShowId",
table: "ChannelShow", table: "ChannelShow",
columns: new[] { "ChannelId", "ShowId" }); columns: new[] { "ChannelId", "ShowId" }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_OverrideShow_ProgrammingOverrideId", name: "IX_OverrideShow_ProgrammingOverrideId",
table: "OverrideShow", table: "OverrideShow",
column: "ProgrammingOverrideId"); column: "ProgrammingOverrideId"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ProgrammingOverride_ChannelId_StartsAtUtc_EndsAtUtc", name: "IX_ProgrammingOverride_ChannelId_StartsAtUtc_EndsAtUtc",
table: "ProgrammingOverride", table: "ProgrammingOverride",
columns: new[] { "ChannelId", "StartsAtUtc", "EndsAtUtc" }); columns: new[] { "ChannelId", "StartsAtUtc", "EndsAtUtc" }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ScheduleEntries_ChannelId_EndsAtUtc", name: "IX_ScheduleEntries_ChannelId_EndsAtUtc",
table: "ScheduleEntries", table: "ScheduleEntries",
columns: new[] { "ChannelId", "EndsAtUtc" }); columns: new[] { "ChannelId", "EndsAtUtc" }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ScheduleEntries_ChannelId_ShowId", name: "IX_ScheduleEntries_ChannelId_ShowId",
table: "ScheduleEntries", table: "ScheduleEntries",
columns: new[] { "ChannelId", "ShowId" }); columns: new[] { "ChannelId", "ShowId" }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ScheduleEntries_ChannelId_StartsAtUtc", name: "IX_ScheduleEntries_ChannelId_StartsAtUtc",
table: "ScheduleEntries", table: "ScheduleEntries",
columns: new[] { "ChannelId", "StartsAtUtc" }); columns: new[] { "ChannelId", "StartsAtUtc" }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ShowEpisode_MediaAssetId", name: "IX_ShowEpisode_MediaAssetId",
table: "ShowEpisode", table: "ShowEpisode",
column: "MediaAssetId"); column: "MediaAssetId"
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ShowEpisode_ShowId_Position", name: "IX_ShowEpisode_ShowId_Position",
table: "ShowEpisode", table: "ShowEpisode",
columns: new[] { "ShowId", "Position" }); columns: new[] { "ShowId", "Position" }
);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "ChannelAd");
name: "ChannelAd");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "ChannelShow");
name: "ChannelShow");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "OverrideShow");
name: "OverrideShow");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "ScheduleEntries");
name: "ScheduleEntries");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "ShowEpisode");
name: "ShowEpisode");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "ProgrammingOverride");
name: "ProgrammingOverride");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "Shows");
name: "Shows");
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "Channels");
name: "Channels");
} }
} }
} }
@@ -16,7 +16,8 @@ namespace TeleWave.Infrastructure.Migrations
table: "Channels", table: "Channels",
type: "boolean", type: "boolean",
nullable: false, nullable: false,
defaultValue: false); defaultValue: false
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "BumperAssets", name: "BumperAssets",
@@ -25,30 +26,36 @@ namespace TeleWave.Infrastructure.Migrations
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
FromShowId = table.Column<Guid>(type: "uuid", nullable: false), FromShowId = table.Column<Guid>(type: "uuid", nullable: false),
ToShowId = table.Column<Guid>(type: "uuid", nullable: false), ToShowId = table.Column<Guid>(type: "uuid", nullable: false),
Signature = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), Signature = table.Column<string>(
type: "character varying(128)",
maxLength: 128,
nullable: false
),
MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false), MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false) CreatedAt = table.Column<DateTimeOffset>(
type: "timestamp with time zone",
nullable: false
),
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_BumperAssets", x => x.Id); table.PrimaryKey("PK_BumperAssets", x => x.Id);
}); }
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_BumperAssets_FromShowId_ToShowId_Signature", name: "IX_BumperAssets_FromShowId_ToShowId_Signature",
table: "BumperAssets", table: "BumperAssets",
columns: new[] { "FromShowId", "ToShowId", "Signature" }); columns: new[] { "FromShowId", "ToShowId", "Signature" }
);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "BumperAssets");
name: "BumperAssets");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumpersEnabled", table: "Channels");
name: "BumpersEnabled",
table: "Channels");
} }
} }
} }
@@ -15,114 +15,104 @@ namespace TeleWave.Infrastructure.Migrations
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: false, nullable: false,
defaultValue: "0x38bdf8"); defaultValue: "0x38bdf8"
);
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "BumperBackgroundColor", name: "BumperBackgroundColor",
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: false, nullable: false,
defaultValue: "0x0b1020"); defaultValue: "0x0b1020"
);
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "BumperBackgroundColor2", name: "BumperBackgroundColor2",
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: false, nullable: false,
defaultValue: "0x1e293b"); defaultValue: "0x1e293b"
);
migrationBuilder.AddColumn<int>( migrationBuilder.AddColumn<int>(
name: "BumperDurationSeconds", name: "BumperDurationSeconds",
table: "Channels", table: "Channels",
type: "integer", type: "integer",
nullable: false, nullable: false,
defaultValue: 8); defaultValue: 8
);
migrationBuilder.AddColumn<int>( migrationBuilder.AddColumn<int>(
name: "BumperFont", name: "BumperFont",
table: "Channels", table: "Channels",
type: "integer", type: "integer",
nullable: false, nullable: false,
defaultValue: 0); defaultValue: 0
);
migrationBuilder.AddColumn<int>( migrationBuilder.AddColumn<int>(
name: "BumperMinIntervalMinutes", name: "BumperMinIntervalMinutes",
table: "Channels", table: "Channels",
type: "integer", type: "integer",
nullable: false, nullable: false,
defaultValue: 0); defaultValue: 0
);
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "BumperNextLabel", name: "BumperNextLabel",
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: false, nullable: false,
defaultValue: "ДАЛЕЕ"); defaultValue: "ДАЛЕЕ"
);
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "BumperNowLabel", name: "BumperNowLabel",
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: false, nullable: false,
defaultValue: "СЕЙЧАС"); defaultValue: "СЕЙЧАС"
);
migrationBuilder.AddColumn<bool>( migrationBuilder.AddColumn<bool>(
name: "BumperOnlyBetweenDifferentShows", name: "BumperOnlyBetweenDifferentShows",
table: "Channels", table: "Channels",
type: "boolean", type: "boolean",
nullable: false, nullable: false,
defaultValue: true); defaultValue: true
);
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "BumperTextColor", name: "BumperTextColor",
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: false, nullable: false,
defaultValue: "white"); defaultValue: "white"
);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperAccentColor", table: "Channels");
name: "BumperAccentColor",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperBackgroundColor", table: "Channels");
name: "BumperBackgroundColor",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperBackgroundColor2", table: "Channels");
name: "BumperBackgroundColor2",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperDurationSeconds", table: "Channels");
name: "BumperDurationSeconds",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperFont", table: "Channels");
name: "BumperFont",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperMinIntervalMinutes", table: "Channels");
name: "BumperMinIntervalMinutes",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperNextLabel", table: "Channels");
name: "BumperNextLabel",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperNowLabel", table: "Channels");
name: "BumperNowLabel",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperOnlyBetweenDifferentShows", table: "Channels");
name: "BumperOnlyBetweenDifferentShows",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperTextColor", table: "Channels");
name: "BumperTextColor",
table: "Channels");
} }
} }
} }
@@ -15,34 +15,39 @@ namespace TeleWave.Infrastructure.Migrations
name: "BumperBackgroundExtension", name: "BumperBackgroundExtension",
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: true); nullable: true
);
migrationBuilder.AddColumn<int>( migrationBuilder.AddColumn<int>(
name: "BumperMode", name: "BumperMode",
table: "Channels", table: "Channels",
type: "integer", type: "integer",
nullable: false, nullable: false,
defaultValue: 0); defaultValue: 0
);
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
name: "BumperMusicExtension", name: "BumperMusicExtension",
table: "Channels", table: "Channels",
type: "text", type: "text",
nullable: true); nullable: true
);
migrationBuilder.AddColumn<int>( migrationBuilder.AddColumn<int>(
name: "BumperRevision", name: "BumperRevision",
table: "Channels", table: "Channels",
type: "integer", type: "integer",
nullable: false, nullable: false,
defaultValue: 0); defaultValue: 0
);
migrationBuilder.AddColumn<int>( migrationBuilder.AddColumn<int>(
name: "NextJingleIndex", name: "NextJingleIndex",
table: "Channels", table: "Channels",
type: "integer", type: "integer",
nullable: false, nullable: false,
defaultValue: 0); defaultValue: 0
);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ChannelJingle", name: "ChannelJingle",
@@ -51,7 +56,7 @@ namespace TeleWave.Infrastructure.Migrations
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
ChannelId = table.Column<Guid>(type: "uuid", nullable: false), ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false), MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false),
Position = table.Column<int>(type: "integer", nullable: false) Position = table.Column<int>(type: "integer", nullable: false),
}, },
constraints: table => constraints: table =>
{ {
@@ -61,40 +66,32 @@ namespace TeleWave.Infrastructure.Migrations
column: x => x.ChannelId, column: x => x.ChannelId,
principalTable: "Channels", principalTable: "Channels",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade
}); );
}
);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_ChannelJingle_ChannelId_Position", name: "IX_ChannelJingle_ChannelId_Position",
table: "ChannelJingle", table: "ChannelJingle",
columns: new[] { "ChannelId", "Position" }); columns: new[] { "ChannelId", "Position" }
);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(name: "ChannelJingle");
name: "ChannelJingle");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperBackgroundExtension", table: "Channels");
name: "BumperBackgroundExtension",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperMode", table: "Channels");
name: "BumperMode",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperMusicExtension", table: "Channels");
name: "BumperMusicExtension",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "BumperRevision", table: "Channels");
name: "BumperRevision",
table: "Channels");
migrationBuilder.DropColumn( migrationBuilder.DropColumn(name: "NextJingleIndex", table: "Channels");
name: "NextJingleIndex",
table: "Channels");
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More