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