Add bumper variant management: implement API endpoints for adding, updating, and removing bumper text variants, enhance data models to support variant details, and update scheduling logic to utilize variants. Refactor related components for improved bumper template handling and ensure proper error management for variant operations.
This commit is contained in:
@@ -93,6 +93,22 @@ public static class ChannelEndpoints
|
|||||||
PreviewSegment
|
PreviewSegment
|
||||||
);
|
);
|
||||||
|
|
||||||
|
admin
|
||||||
|
.MapPost("/{id:guid}/bumper/templates/{templateId:guid}/variants", AddBumperVariant)
|
||||||
|
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
||||||
|
admin
|
||||||
|
.MapPut(
|
||||||
|
"/{id:guid}/bumper/templates/{templateId:guid}/variants/{variantId:guid}",
|
||||||
|
UpdateBumperVariant
|
||||||
|
)
|
||||||
|
.Produces(StatusCodes.Status204NoContent);
|
||||||
|
admin
|
||||||
|
.MapDelete(
|
||||||
|
"/{id:guid}/bumper/templates/{templateId:guid}/variants/{variantId:guid}",
|
||||||
|
RemoveBumperVariant
|
||||||
|
)
|
||||||
|
.Produces(StatusCodes.Status204NoContent);
|
||||||
|
|
||||||
admin
|
admin
|
||||||
.MapPost("/{id:guid}/overrides", CreateOverride)
|
.MapPost("/{id:guid}/overrides", CreateOverride)
|
||||||
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
||||||
@@ -348,6 +364,65 @@ public static class ChannelEndpoints
|
|||||||
return result.ToHttpResult();
|
return result.ToHttpResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> AddBumperVariant(
|
||||||
|
Guid id,
|
||||||
|
Guid templateId,
|
||||||
|
AddBumperVariantBody body,
|
||||||
|
ISender sender,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var result = await sender.Send(
|
||||||
|
new AddBumperTextVariantCommand(id, templateId, body.Name),
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Created($"/api/admin/channels/{id}", new CreatedIdResponse(result.Value))
|
||||||
|
: result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> UpdateBumperVariant(
|
||||||
|
Guid id,
|
||||||
|
Guid templateId,
|
||||||
|
Guid variantId,
|
||||||
|
UpdateBumperVariantBody body,
|
||||||
|
ISender sender,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var result = await sender.Send(
|
||||||
|
new UpdateBumperTextVariantCommand(
|
||||||
|
id,
|
||||||
|
templateId,
|
||||||
|
variantId,
|
||||||
|
body.Name,
|
||||||
|
body.Kind,
|
||||||
|
body.NowLabel,
|
||||||
|
body.NextLabel,
|
||||||
|
body.Line1,
|
||||||
|
body.Line2,
|
||||||
|
body.Trigger
|
||||||
|
),
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> RemoveBumperVariant(
|
||||||
|
Guid id,
|
||||||
|
Guid templateId,
|
||||||
|
Guid variantId,
|
||||||
|
ISender sender,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var result = await sender.Send(
|
||||||
|
new RemoveBumperTextVariantCommand(id, templateId, variantId),
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<IResult> ClearTemplateBackground(
|
private static async Task<IResult> ClearTemplateBackground(
|
||||||
Guid id,
|
Guid id,
|
||||||
Guid templateId,
|
Guid templateId,
|
||||||
@@ -532,6 +607,18 @@ public sealed record AddBumperTemplateBody(string Name);
|
|||||||
|
|
||||||
public sealed record SetBumperTemplateBackgroundBody(Guid ImageId);
|
public sealed record SetBumperTemplateBackgroundBody(Guid ImageId);
|
||||||
|
|
||||||
|
public sealed record AddBumperVariantBody(string Name);
|
||||||
|
|
||||||
|
public sealed record UpdateBumperVariantBody(
|
||||||
|
string Name,
|
||||||
|
BumperTextKind Kind,
|
||||||
|
string NowLabel,
|
||||||
|
string NextLabel,
|
||||||
|
string Line1,
|
||||||
|
string Line2,
|
||||||
|
BumperTrigger Trigger
|
||||||
|
);
|
||||||
|
|
||||||
public sealed record UpdateBumperTemplateBody(
|
public sealed record UpdateBumperTemplateBody(
|
||||||
string Name,
|
string Name,
|
||||||
string BackgroundColor,
|
string BackgroundColor,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using LiteCqrs;
|
||||||
|
using TeleWave.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
|
/// <summary>Добавить подблок (текст-вариант) в блок заставки.</summary>
|
||||||
|
public sealed record AddBumperTextVariantCommand(Guid ChannelId, Guid TemplateId, string Name)
|
||||||
|
: ICommand<Result<Guid>>;
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
using LiteCqrs;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TeleWave.Application.Common.Interfaces;
|
||||||
|
using TeleWave.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
|
public sealed class AddBumperTextVariantCommandHandler(IAppDbContext dbContext)
|
||||||
|
: ICommandHandler<AddBumperTextVariantCommand, Result<Guid>>
|
||||||
|
{
|
||||||
|
public async Task<Result<Guid>> Handle(
|
||||||
|
AddBumperTextVariantCommand command,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
var template = channel.FindBumperTemplate(command.TemplateId);
|
||||||
|
if (template is null)
|
||||||
|
return Result.Failure<Guid>(ChannelErrors.BumperTemplateNotFound);
|
||||||
|
|
||||||
|
var name = string.IsNullOrWhiteSpace(command.Name)
|
||||||
|
? $"Текст {template.Variants.Count + 1}"
|
||||||
|
: command.Name.Trim();
|
||||||
|
var variant = template.AddVariant(name);
|
||||||
|
return Result.Success(variant.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using LiteCqrs;
|
||||||
|
using TeleWave.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
|
/// <summary>Удалить подблок (кроме последнего) из блока заставки.</summary>
|
||||||
|
public sealed record RemoveBumperTextVariantCommand(Guid ChannelId, Guid TemplateId, Guid VariantId)
|
||||||
|
: ICommand<Result>;
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
using LiteCqrs;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TeleWave.Application.Common.Interfaces;
|
||||||
|
using TeleWave.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
|
public sealed class RemoveBumperTextVariantCommandHandler(IAppDbContext dbContext)
|
||||||
|
: ICommandHandler<RemoveBumperTextVariantCommand, Result>
|
||||||
|
{
|
||||||
|
public async Task<Result> Handle(
|
||||||
|
RemoveBumperTextVariantCommand command,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
var template = channel.FindBumperTemplate(command.TemplateId);
|
||||||
|
if (template is null)
|
||||||
|
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
||||||
|
if (template.FindVariant(command.VariantId) is null)
|
||||||
|
return Result.Failure(ChannelErrors.BumperTextVariantNotFound);
|
||||||
|
|
||||||
|
return template.RemoveVariant(command.VariantId)
|
||||||
|
? Result.Success()
|
||||||
|
: Result.Failure(ChannelErrors.CannotRemoveLastBumperTextVariant);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-5
@@ -30,7 +30,9 @@ public sealed class RenderBumperPreviewQueryHandler(
|
|||||||
{
|
{
|
||||||
var channel = await dbContext.Channels.AsNoTracking()
|
var channel = await dbContext.Channels.AsNoTracking()
|
||||||
.Include(c => c.BumperTemplates)
|
.Include(c => c.BumperTemplates)
|
||||||
|
.ThenInclude(t => t.Variants)
|
||||||
.Include(c => c.Shows)
|
.Include(c => c.Shows)
|
||||||
|
.AsSplitQuery()
|
||||||
.FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken);
|
.FirstOrDefaultAsync(c => c.Id == query.ChannelId, cancellationToken);
|
||||||
if (channel is null)
|
if (channel is null)
|
||||||
return Result.Failure<Guid>(ChannelErrors.NotFound);
|
return Result.Failure<Guid>(ChannelErrors.NotFound);
|
||||||
@@ -39,7 +41,13 @@ public sealed class RenderBumperPreviewQueryHandler(
|
|||||||
if (template is null)
|
if (template is null)
|
||||||
return Result.Failure<Guid>(ChannelErrors.BumperTemplateNotFound);
|
return Result.Failure<Guid>(ChannelErrors.BumperTemplateNotFound);
|
||||||
|
|
||||||
|
// Превью показываем по первому подблоку (стиль/звук блока + его текст).
|
||||||
|
var variant = template.Variants.OrderBy(v => v.Position).FirstOrDefault();
|
||||||
|
if (variant is null)
|
||||||
|
return Result.Failure<Guid>(ChannelErrors.BumperTemplateNotFound);
|
||||||
|
|
||||||
var (fromName, toName) = await SampleNamesAsync(channel, cancellationToken);
|
var (fromName, toName) = await SampleNamesAsync(channel, cancellationToken);
|
||||||
|
var free = variant.Kind == BumperTextKind.Free;
|
||||||
|
|
||||||
// Фон блока — из общего реестра по id.
|
// Фон блока — из общего реестра по id.
|
||||||
string? backgroundPath = null;
|
string? backgroundPath = null;
|
||||||
@@ -68,14 +76,17 @@ public sealed class RenderBumperPreviewQueryHandler(
|
|||||||
template.AccentColor,
|
template.AccentColor,
|
||||||
template.TextColor,
|
template.TextColor,
|
||||||
channel.BumperFont == BumperFont.Serif ? _bumper.FontFileSerif : _bumper.FontFileSans,
|
channel.BumperFont == BumperFont.Serif ? _bumper.FontFileSerif : _bumper.FontFileSans,
|
||||||
channel.BumperNowLabel,
|
free ? "" : variant.NowLabel,
|
||||||
fromName,
|
free ? "" : fromName,
|
||||||
channel.BumperNextLabel,
|
free ? "" : variant.NextLabel,
|
||||||
toName,
|
free ? "" : toName,
|
||||||
backgroundPath,
|
backgroundPath,
|
||||||
storage.AudioPath(template.Id, template.AudioExtension),
|
storage.AudioPath(template.Id, template.AudioExtension),
|
||||||
// Постер зависит от конкретного «следующего» шоу — в превью не подставляем.
|
// Постер зависит от конкретного «следующего» шоу — в превью не подставляем.
|
||||||
null
|
null,
|
||||||
|
free,
|
||||||
|
variant.Line1,
|
||||||
|
variant.Line2
|
||||||
);
|
);
|
||||||
|
|
||||||
var previewId = BumperPreview.AssetId(template.Id);
|
var previewId = BumperPreview.AssetId(template.Id);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using LiteCqrs;
|
||||||
|
using TeleWave.Application.Common.Models;
|
||||||
|
using TeleWave.Domain.Broadcast;
|
||||||
|
|
||||||
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
|
/// <summary>Обновить подблок: имя, режим текста, текст и правило показа.</summary>
|
||||||
|
public sealed record UpdateBumperTextVariantCommand(
|
||||||
|
Guid ChannelId,
|
||||||
|
Guid TemplateId,
|
||||||
|
Guid VariantId,
|
||||||
|
string Name,
|
||||||
|
BumperTextKind Kind,
|
||||||
|
string NowLabel,
|
||||||
|
string NextLabel,
|
||||||
|
string Line1,
|
||||||
|
string Line2,
|
||||||
|
BumperTrigger Trigger
|
||||||
|
) : ICommand<Result>;
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
using LiteCqrs;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TeleWave.Application.Common.Interfaces;
|
||||||
|
using TeleWave.Application.Common.Models;
|
||||||
|
|
||||||
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
|
public sealed class UpdateBumperTextVariantCommandHandler(IAppDbContext dbContext)
|
||||||
|
: ICommandHandler<UpdateBumperTextVariantCommand, Result>
|
||||||
|
{
|
||||||
|
public async Task<Result> Handle(
|
||||||
|
UpdateBumperTextVariantCommand command,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
var template = channel.FindBumperTemplate(command.TemplateId);
|
||||||
|
if (template is null)
|
||||||
|
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
||||||
|
|
||||||
|
var variant = template.FindVariant(command.VariantId);
|
||||||
|
if (variant is null)
|
||||||
|
return Result.Failure(ChannelErrors.BumperTextVariantNotFound);
|
||||||
|
|
||||||
|
variant.Update(
|
||||||
|
command.Name.Trim(),
|
||||||
|
command.Kind,
|
||||||
|
command.NowLabel,
|
||||||
|
command.NextLabel,
|
||||||
|
command.Line1,
|
||||||
|
command.Line2,
|
||||||
|
command.Trigger
|
||||||
|
);
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
|
public sealed class UpdateBumperTextVariantCommandValidator
|
||||||
|
: AbstractValidator<UpdateBumperTextVariantCommand>
|
||||||
|
{
|
||||||
|
public UpdateBumperTextVariantCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(64);
|
||||||
|
RuleFor(x => x.NowLabel).MaximumLength(64);
|
||||||
|
RuleFor(x => x.NextLabel).MaximumLength(64);
|
||||||
|
RuleFor(x => x.Line1).MaximumLength(120);
|
||||||
|
RuleFor(x => x.Line2).MaximumLength(120);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,17 +27,27 @@ public sealed record ProgrammingOverrideDto(
|
|||||||
IReadOnlyList<OverrideShowDto> Shows
|
IReadOnlyList<OverrideShowDto> Shows
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>Общие для канала настройки ТВ-заставок (стиль/звук — на каждом блоке, см. <see cref="BumperTemplateDto"/>).</summary>
|
/// <summary>Общие для канала настройки ТВ-заставок (стиль/звук/текст — на блоках/подблоках).</summary>
|
||||||
public sealed record BumperSettingsDto(
|
public sealed record BumperSettingsDto(
|
||||||
BumperFont Font,
|
BumperFont Font,
|
||||||
string NowLabel,
|
|
||||||
string NextLabel,
|
|
||||||
int MinIntervalMinutes,
|
int MinIntervalMinutes,
|
||||||
bool OnlyBetweenDifferentShows,
|
|
||||||
BumperSelection Selection
|
BumperSelection Selection
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>Блок заставки: своё оформление + звук. <see cref="AudioDurationSeconds"/> — длина звука (сек).</summary>
|
/// <summary>Подблок (текст-вариант): свой текст + правило показа поверх стиля/звука блока.</summary>
|
||||||
|
public sealed record BumperTextVariantDto(
|
||||||
|
Guid Id,
|
||||||
|
int Position,
|
||||||
|
string Name,
|
||||||
|
BumperTextKind Kind,
|
||||||
|
string NowLabel,
|
||||||
|
string NextLabel,
|
||||||
|
string Line1,
|
||||||
|
string Line2,
|
||||||
|
BumperTrigger Trigger
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>Блок заставки: своё оформление + звук + подблоки. <see cref="AudioDurationSeconds"/> — длина звука (сек).</summary>
|
||||||
public sealed record BumperTemplateDto(
|
public sealed record BumperTemplateDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
int Position,
|
int Position,
|
||||||
@@ -49,7 +59,8 @@ public sealed record BumperTemplateDto(
|
|||||||
string TextColor,
|
string TextColor,
|
||||||
Guid? BackgroundImageId,
|
Guid? BackgroundImageId,
|
||||||
bool HasAudio,
|
bool HasAudio,
|
||||||
double? AudioDurationSeconds
|
double? AudioDurationSeconds,
|
||||||
|
IReadOnlyList<BumperTextVariantDto> Variants
|
||||||
);
|
);
|
||||||
|
|
||||||
public sealed record ChannelDto(
|
public sealed record ChannelDto(
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ public static class ChannelErrors
|
|||||||
"Дефолтный блок заставки удалить нельзя."
|
"Дефолтный блок заставки удалить нельзя."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static readonly Error BumperTextVariantNotFound = Error.NotFound(
|
||||||
|
"Channels.BumperTextVariantNotFound",
|
||||||
|
"Подблок заставки не найден."
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Error CannotRemoveLastBumperTextVariant = Error.Validation(
|
||||||
|
"Channels.CannotRemoveLastBumperTextVariant",
|
||||||
|
"Нельзя удалить последний подблок — нужен хотя бы один."
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly Error AssetNotFound = Error.NotFound(
|
public static readonly Error AssetNotFound = Error.NotFound(
|
||||||
"Channels.AssetNotFound",
|
"Channels.AssetNotFound",
|
||||||
"Медиа-ассет не найден."
|
"Медиа-ассет не найден."
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
|||||||
.Include(c => c.Shows)
|
.Include(c => c.Shows)
|
||||||
.Include(c => c.Ads)
|
.Include(c => c.Ads)
|
||||||
.Include(c => c.BumperTemplates)
|
.Include(c => c.BumperTemplates)
|
||||||
|
.ThenInclude(t => t.Variants)
|
||||||
.Include(c => c.Overrides)
|
.Include(c => c.Overrides)
|
||||||
.ThenInclude(o => o.Shows)
|
.ThenInclude(o => o.Shows)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
@@ -77,7 +78,21 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
|||||||
t.TextColor,
|
t.TextColor,
|
||||||
t.BackgroundImageId,
|
t.BackgroundImageId,
|
||||||
t.AudioExtension is not null,
|
t.AudioExtension is not null,
|
||||||
t.AudioDurationSeconds
|
t.AudioDurationSeconds,
|
||||||
|
t.Variants
|
||||||
|
.OrderBy(v => v.Position)
|
||||||
|
.Select(v => new BumperTextVariantDto(
|
||||||
|
v.Id,
|
||||||
|
v.Position,
|
||||||
|
v.Name,
|
||||||
|
v.Kind,
|
||||||
|
v.NowLabel,
|
||||||
|
v.NextLabel,
|
||||||
|
v.Line1,
|
||||||
|
v.Line2,
|
||||||
|
v.Trigger
|
||||||
|
))
|
||||||
|
.ToList()
|
||||||
))
|
))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -105,10 +120,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
|||||||
channel.BumpersEnabled,
|
channel.BumpersEnabled,
|
||||||
new BumperSettingsDto(
|
new BumperSettingsDto(
|
||||||
channel.BumperFont,
|
channel.BumperFont,
|
||||||
channel.BumperNowLabel,
|
|
||||||
channel.BumperNextLabel,
|
|
||||||
channel.BumperMinIntervalMinutes,
|
channel.BumperMinIntervalMinutes,
|
||||||
channel.BumperOnlyBetweenDifferentShows,
|
|
||||||
channel.BumperSelection
|
channel.BumperSelection
|
||||||
),
|
),
|
||||||
bumperTemplates,
|
bumperTemplates,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public sealed class ScheduleGenerator(
|
|||||||
.Include(c => c.Shows)
|
.Include(c => c.Shows)
|
||||||
.Include(c => c.Ads)
|
.Include(c => c.Ads)
|
||||||
.Include(c => c.BumperTemplates)
|
.Include(c => c.BumperTemplates)
|
||||||
|
.ThenInclude(t => t.Variants)
|
||||||
.Include(c => c.Overrides)
|
.Include(c => c.Overrides)
|
||||||
.ThenInclude(o => o.Shows)
|
.ThenInclude(o => o.Shows)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
@@ -147,16 +148,16 @@ public sealed class ScheduleGenerator(
|
|||||||
private static ScheduleEntry? BuildBumperEntry(
|
private static ScheduleEntry? BuildBumperEntry(
|
||||||
Guid channelId,
|
Guid channelId,
|
||||||
PlannedEntry entry,
|
PlannedEntry entry,
|
||||||
IReadOnlyDictionary<(Guid From, Guid To, Guid Template), Guid> bumperAssets
|
IReadOnlyDictionary<(Guid From, Guid To, Guid Variant), Guid> bumperAssets
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Заставка резолвится по паре шоу + выбранному блоку (отрендерена/из кэша). Если рендер не
|
// Заставка резолвится по паре шоу + выбранному подблоку (отрендерена/из кэша). Если рендер не
|
||||||
// удался — пропускаем запись (слот заполнит филлер/следующая программа; длину планировщик учёл).
|
// удался — пропускаем запись (слот заполнит филлер/следующая программа; длину планировщик учёл).
|
||||||
if (
|
if (
|
||||||
entry.FromShowId is not { } from
|
entry.FromShowId is not { } from
|
||||||
|| entry.ToShowId is not { } to
|
|| entry.ToShowId is not { } to
|
||||||
|| entry.BumperTemplateId is not { } template
|
|| entry.BumperVariantId is not { } variant
|
||||||
|| !bumperAssets.TryGetValue((from, to, template), out var assetId)
|
|| !bumperAssets.TryGetValue((from, to, variant), out var assetId)
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -164,10 +165,10 @@ public sealed class ScheduleGenerator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Для каждой уникальной тройки «из→в→блок» из запланированных заставок возвращает id готового
|
/// Для каждой уникальной тройки «из→в→подблок» из запланированных заставок возвращает id готового
|
||||||
/// ассета-заставки: из кэша (<see cref="BumperAsset"/>) либо свежесгенерированного.
|
/// ассета-заставки: из кэша (<see cref="BumperAsset"/>) либо свежесгенерированного.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<Dictionary<(Guid From, Guid To, Guid Template), Guid>> ResolveBumperAssetsAsync(
|
private async Task<Dictionary<(Guid From, Guid To, Guid Variant), Guid>> ResolveBumperAssetsAsync(
|
||||||
Channel channel,
|
Channel channel,
|
||||||
IReadOnlyList<PlannedEntry> entries,
|
IReadOnlyList<PlannedEntry> entries,
|
||||||
IReadOnlyDictionary<Guid, string> showNames,
|
IReadOnlyDictionary<Guid, string> showNames,
|
||||||
@@ -176,17 +177,20 @@ public sealed class ScheduleGenerator(
|
|||||||
{
|
{
|
||||||
var result = new Dictionary<(Guid, Guid, Guid), Guid>();
|
var result = new Dictionary<(Guid, Guid, Guid), Guid>();
|
||||||
var templatesById = channel.BumperTemplates.ToDictionary(t => t.Id);
|
var templatesById = channel.BumperTemplates.ToDictionary(t => t.Id);
|
||||||
|
var variantsById = channel.BumperTemplates
|
||||||
|
.SelectMany(t => t.Variants.Select(v => (Variant: v, Template: t)))
|
||||||
|
.ToDictionary(x => x.Variant.Id);
|
||||||
var combos = entries
|
var combos = entries
|
||||||
.Where(e =>
|
.Where(e =>
|
||||||
e.Kind == ScheduleEntryKind.Bumper
|
e.Kind == ScheduleEntryKind.Bumper
|
||||||
&& e.FromShowId is not null
|
&& e.FromShowId is not null
|
||||||
&& e.ToShowId is not null
|
&& e.ToShowId is not null
|
||||||
&& e.BumperTemplateId is not null
|
&& e.BumperVariantId is not null
|
||||||
)
|
)
|
||||||
.Select(e => (
|
.Select(e => (
|
||||||
From: e.FromShowId!.Value,
|
From: e.FromShowId!.Value,
|
||||||
To: e.ToShowId!.Value,
|
To: e.ToShowId!.Value,
|
||||||
Template: e.BumperTemplateId!.Value
|
Variant: e.BumperVariantId!.Value
|
||||||
))
|
))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -256,17 +260,20 @@ public sealed class ScheduleGenerator(
|
|||||||
|
|
||||||
foreach (var combo in combos)
|
foreach (var combo in combos)
|
||||||
{
|
{
|
||||||
if (!templatesById.TryGetValue(combo.Template, out var template))
|
if (!variantsById.TryGetValue(combo.Variant, out var pair))
|
||||||
continue;
|
continue;
|
||||||
|
var (variant, template) = pair;
|
||||||
|
|
||||||
var fromName = showNames.GetValueOrDefault(combo.From, "…");
|
var fromName = showNames.GetValueOrDefault(combo.From, "…");
|
||||||
var toName = showNames.GetValueOrDefault(combo.To, "…");
|
var toName = showNames.GetValueOrDefault(combo.To, "…");
|
||||||
var poster = posterByShow.TryGetValue(combo.To, out var pr) ? pr : default;
|
// Постер шоу-получателя как фон — только для «Сейчас/Далее» (свободный текст шоу не упоминает).
|
||||||
|
var usePoster = variant.Kind == BumperTextKind.NowNext;
|
||||||
|
var poster = usePoster && posterByShow.TryGetValue(combo.To, out var pr) ? pr : default;
|
||||||
var posterToken = poster.ImageId == Guid.Empty ? "-" : poster.ImageId.ToString();
|
var posterToken = poster.ImageId == Guid.Empty ? "-" : poster.ImageId.ToString();
|
||||||
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
|
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
|
||||||
var bgAbs = bgByTemplate.GetValueOrDefault(template.Id);
|
var bgAbs = bgByTemplate.GetValueOrDefault(template.Id);
|
||||||
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template));
|
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template));
|
||||||
var signature = ComputeSignature(channel, template, fromName, toName, aligned, posterToken);
|
var signature = ComputeSignature(channel, template, variant, fromName, toName, aligned, posterToken);
|
||||||
|
|
||||||
var hit = cached.FirstOrDefault(c =>
|
var hit = cached.FirstOrDefault(c =>
|
||||||
c.FromShowId == combo.From
|
c.FromShowId == combo.From
|
||||||
@@ -285,6 +292,7 @@ public sealed class ScheduleGenerator(
|
|||||||
var assetId = await RenderBumperAsync(
|
var assetId = await RenderBumperAsync(
|
||||||
channel,
|
channel,
|
||||||
template,
|
template,
|
||||||
|
variant,
|
||||||
combo.From,
|
combo.From,
|
||||||
combo.To,
|
combo.To,
|
||||||
fromName,
|
fromName,
|
||||||
@@ -314,6 +322,7 @@ public sealed class ScheduleGenerator(
|
|||||||
private async Task<Guid> RenderBumperAsync(
|
private async Task<Guid> RenderBumperAsync(
|
||||||
Channel channel,
|
Channel channel,
|
||||||
BumperTemplate template,
|
BumperTemplate template,
|
||||||
|
BumperTextVariant variant,
|
||||||
Guid fromShowId,
|
Guid fromShowId,
|
||||||
Guid toShowId,
|
Guid toShowId,
|
||||||
string fromName,
|
string fromName,
|
||||||
@@ -331,6 +340,7 @@ public sealed class ScheduleGenerator(
|
|||||||
BuildSpec(
|
BuildSpec(
|
||||||
channel,
|
channel,
|
||||||
template,
|
template,
|
||||||
|
variant,
|
||||||
alignedDurationSeconds,
|
alignedDurationSeconds,
|
||||||
fromName,
|
fromName,
|
||||||
toName,
|
toName,
|
||||||
@@ -361,13 +371,16 @@ public sealed class ScheduleGenerator(
|
|||||||
private BumperRenderSpec BuildSpec(
|
private BumperRenderSpec BuildSpec(
|
||||||
Channel channel,
|
Channel channel,
|
||||||
BumperTemplate template,
|
BumperTemplate template,
|
||||||
|
BumperTextVariant variant,
|
||||||
int alignedDurationSeconds,
|
int alignedDurationSeconds,
|
||||||
string fromName,
|
string fromName,
|
||||||
string toName,
|
string toName,
|
||||||
string? posterAbsolutePath,
|
string? posterAbsolutePath,
|
||||||
string? backgroundAbsolutePath
|
string? backgroundAbsolutePath
|
||||||
) =>
|
)
|
||||||
new(
|
{
|
||||||
|
var free = variant.Kind == BumperTextKind.Free;
|
||||||
|
return new BumperRenderSpec(
|
||||||
alignedDurationSeconds,
|
alignedDurationSeconds,
|
||||||
_bumper.Width,
|
_bumper.Width,
|
||||||
_bumper.Height,
|
_bumper.Height,
|
||||||
@@ -376,14 +389,18 @@ public sealed class ScheduleGenerator(
|
|||||||
template.AccentColor,
|
template.AccentColor,
|
||||||
template.TextColor,
|
template.TextColor,
|
||||||
FontPath(channel.BumperFont),
|
FontPath(channel.BumperFont),
|
||||||
channel.BumperNowLabel,
|
free ? "" : variant.NowLabel,
|
||||||
fromName,
|
free ? "" : fromName,
|
||||||
channel.BumperNextLabel,
|
free ? "" : variant.NextLabel,
|
||||||
toName,
|
free ? "" : toName,
|
||||||
backgroundAbsolutePath,
|
backgroundAbsolutePath,
|
||||||
bumperStorage.AudioPath(template.Id, template.AudioExtension),
|
bumperStorage.AudioPath(template.Id, template.AudioExtension),
|
||||||
posterAbsolutePath
|
posterAbsolutePath,
|
||||||
|
free,
|
||||||
|
variant.Line1,
|
||||||
|
variant.Line2
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private string FontPath(BumperFont font) =>
|
private string FontPath(BumperFont font) =>
|
||||||
font == BumperFont.Serif ? _bumper.FontFileSerif : _bumper.FontFileSans;
|
font == BumperFont.Serif ? _bumper.FontFileSerif : _bumper.FontFileSans;
|
||||||
@@ -407,6 +424,7 @@ public sealed class ScheduleGenerator(
|
|||||||
private string ComputeSignature(
|
private string ComputeSignature(
|
||||||
Channel channel,
|
Channel channel,
|
||||||
BumperTemplate template,
|
BumperTemplate template,
|
||||||
|
BumperTextVariant variant,
|
||||||
string fromName,
|
string fromName,
|
||||||
string toName,
|
string toName,
|
||||||
int alignedDurationSeconds,
|
int alignedDurationSeconds,
|
||||||
@@ -420,8 +438,11 @@ public sealed class ScheduleGenerator(
|
|||||||
_bumper.Height,
|
_bumper.Height,
|
||||||
alignedDurationSeconds,
|
alignedDurationSeconds,
|
||||||
channel.BumperFont,
|
channel.BumperFont,
|
||||||
channel.BumperNowLabel,
|
variant.Kind,
|
||||||
channel.BumperNextLabel,
|
variant.NowLabel,
|
||||||
|
variant.NextLabel,
|
||||||
|
variant.Line1,
|
||||||
|
variant.Line2,
|
||||||
template.BackgroundColor,
|
template.BackgroundColor,
|
||||||
template.BackgroundColor2,
|
template.BackgroundColor2,
|
||||||
template.AccentColor,
|
template.AccentColor,
|
||||||
@@ -515,13 +536,16 @@ public sealed class ScheduleGenerator(
|
|||||||
.Where(durations.ContainsKey)
|
.Where(durations.ContainsKey)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Блоки заставок: длительность слота — по звуку (или дефолт), выровнена на сегмент.
|
// Подблоки заставок (плоский список): длительность слота — по звуку блока, выровнена на сегмент.
|
||||||
var bumperTemplates = channel.BumperTemplates
|
var bumperVariants = channel.BumperTemplates
|
||||||
.OrderBy(t => t.Position)
|
.OrderBy(t => t.Position)
|
||||||
.Select(t => new PlannerBumperTemplate(
|
.SelectMany(t =>
|
||||||
t.Id,
|
{
|
||||||
TimeSpan.FromSeconds(AlignedDurationSeconds(TemplateDurationSeconds(t)))
|
var dur = TimeSpan.FromSeconds(AlignedDurationSeconds(TemplateDurationSeconds(t)));
|
||||||
))
|
return t.Variants
|
||||||
|
.OrderBy(v => v.Position)
|
||||||
|
.Select(v => new PlannerBumperVariant(v.Id, t.Id, dur, v.Trigger));
|
||||||
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var overrides = channel.Overrides
|
var overrides = channel.Overrides
|
||||||
@@ -535,10 +559,9 @@ public sealed class ScheduleGenerator(
|
|||||||
|
|
||||||
var bumpers = new PlannerBumperConfig(
|
var bumpers = new PlannerBumperConfig(
|
||||||
channel.BumpersEnabled,
|
channel.BumpersEnabled,
|
||||||
channel.BumperOnlyBetweenDifferentShows,
|
|
||||||
TimeSpan.FromMinutes(channel.BumperMinIntervalMinutes),
|
TimeSpan.FromMinutes(channel.BumperMinIntervalMinutes),
|
||||||
channel.BumperSelection,
|
channel.BumperSelection,
|
||||||
bumperTemplates
|
bumperVariants
|
||||||
);
|
);
|
||||||
|
|
||||||
return new PlannerInput(
|
return new PlannerInput(
|
||||||
|
|||||||
-3
@@ -18,9 +18,6 @@ public sealed record UpdateChannelSettingsCommand(
|
|||||||
/// <summary>Общие настройки ТВ-заставок канала (см. <c>Channel.UpdateBumperSettings</c>).</summary>
|
/// <summary>Общие настройки ТВ-заставок канала (см. <c>Channel.UpdateBumperSettings</c>).</summary>
|
||||||
public sealed record BumperSettingsInput(
|
public sealed record BumperSettingsInput(
|
||||||
BumperFont Font,
|
BumperFont Font,
|
||||||
string NowLabel,
|
|
||||||
string NextLabel,
|
|
||||||
int MinIntervalMinutes,
|
int MinIntervalMinutes,
|
||||||
bool OnlyBetweenDifferentShows,
|
|
||||||
BumperSelection Selection
|
BumperSelection Selection
|
||||||
);
|
);
|
||||||
|
|||||||
-3
@@ -37,10 +37,7 @@ public sealed class UpdateChannelSettingsCommandHandler(IAppDbContext dbContext)
|
|||||||
);
|
);
|
||||||
channel.UpdateBumperSettings(
|
channel.UpdateBumperSettings(
|
||||||
command.Bumper.Font,
|
command.Bumper.Font,
|
||||||
command.Bumper.NowLabel,
|
|
||||||
command.Bumper.NextLabel,
|
|
||||||
command.Bumper.MinIntervalMinutes,
|
command.Bumper.MinIntervalMinutes,
|
||||||
command.Bumper.OnlyBetweenDifferentShows,
|
|
||||||
command.Bumper.Selection
|
command.Bumper.Selection
|
||||||
);
|
);
|
||||||
return Result.Success();
|
return Result.Success();
|
||||||
|
|||||||
-2
@@ -11,7 +11,5 @@ public sealed class UpdateChannelSettingsCommandValidator
|
|||||||
RuleFor(x => x.AdsPerBreak).InclusiveBetween(0, 10);
|
RuleFor(x => x.AdsPerBreak).InclusiveBetween(0, 10);
|
||||||
|
|
||||||
RuleFor(x => x.Bumper.MinIntervalMinutes).InclusiveBetween(0, 1440);
|
RuleFor(x => x.Bumper.MinIntervalMinutes).InclusiveBetween(0, 1440);
|
||||||
RuleFor(x => x.Bumper.NowLabel).MaximumLength(64);
|
|
||||||
RuleFor(x => x.Bumper.NextLabel).MaximumLength(64);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ public sealed record BumperRenderSpec(
|
|||||||
string? BackgroundFile = null,
|
string? BackgroundFile = null,
|
||||||
string? MusicFile = null,
|
string? MusicFile = null,
|
||||||
/// <summary>Постер шоу как фон (используется, если нет загруженного фона канала; затемняется).</summary>
|
/// <summary>Постер шоу как фон (используется, если нет загруженного фона канала; затемняется).</summary>
|
||||||
string? PosterFile = null
|
string? PosterFile = null,
|
||||||
|
/// <summary>Режим свободного текста: вместо «Сейчас/Далее» рисуются <see cref="FreeLine1"/>/<see cref="FreeLine2"/>.</summary>
|
||||||
|
bool FreeText = false,
|
||||||
|
string FreeLine1 = "",
|
||||||
|
string FreeLine2 = ""
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>Итог рендера заставки — та же форма метаданных, что у обычного ассета после нарезки.</summary>
|
/// <summary>Итог рендера заставки — та же форма метаданных, что у обычного ассета после нарезки.</summary>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
namespace TeleWave.Domain.Broadcast;
|
namespace TeleWave.Domain.Broadcast;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Блок ТВ-заставки канала: свой звук + своё оформление (цвета, опциональная фон-картинка). На
|
/// Блок ТВ-заставки канала: свой звук + своё оформление (цвета, опциональная фон-картинка) + набор
|
||||||
/// переходе между шоу генератор рендерит «Сейчас/Далее» стилем блока поверх его звука; длительность
|
/// подблоков (<see cref="Variants"/>) с разным текстом и правилом показа. Длительность заставки — по
|
||||||
/// заставки определяется длиной звука (выравнивается на сегмент при рендере). Общие для канала шрифт,
|
/// длине звука (выравнивается на сегмент при рендере). Общий для канала — только шрифт.
|
||||||
/// подписи и правила показа живут на <see cref="Channel"/>.
|
|
||||||
///
|
///
|
||||||
/// Первый блок (<see cref="Position"/> == 0) — дефолтный, не удаляется; если звук в нём не загружен,
|
/// Первый блок (<see cref="Position"/> == 0) — дефолтный, не удаляется; если звук в нём не загружен,
|
||||||
/// рендер синтезирует джингл по умолчанию.
|
/// рендер синтезирует джингл по умолчанию.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BumperTemplate
|
public class BumperTemplate
|
||||||
{
|
{
|
||||||
|
private readonly List<BumperTextVariant> _variants = new();
|
||||||
|
|
||||||
public Guid Id { get; private set; }
|
public Guid Id { get; private set; }
|
||||||
public Guid ChannelId { get; private set; }
|
public Guid ChannelId { get; private set; }
|
||||||
|
|
||||||
@@ -46,10 +47,16 @@ public class BumperTemplate
|
|||||||
|
|
||||||
public bool IsDefault => Position == 0;
|
public bool IsDefault => Position == 0;
|
||||||
|
|
||||||
|
/// <summary>Подблоки (текст-варианты); порядок — по <see cref="BumperTextVariant.Position"/>.</summary>
|
||||||
|
public IReadOnlyList<BumperTextVariant> Variants => _variants;
|
||||||
|
|
||||||
|
private const string DefaultVariantName = "Текст 1";
|
||||||
|
|
||||||
private BumperTemplate() { }
|
private BumperTemplate() { }
|
||||||
|
|
||||||
internal static BumperTemplate Create(Guid channelId, int position, string name) =>
|
internal static BumperTemplate Create(Guid channelId, int position, string name)
|
||||||
new()
|
{
|
||||||
|
var template = new BumperTemplate
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
ChannelId = channelId,
|
ChannelId = channelId,
|
||||||
@@ -65,6 +72,35 @@ public class BumperTemplate
|
|||||||
Revision = 0,
|
Revision = 0,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
// Дефолтный подблок «Сейчас/Далее», показывается на смене шоу.
|
||||||
|
template._variants.Add(
|
||||||
|
BumperTextVariant.Create(template.Id, 0, DefaultVariantName, BumperTrigger.OnShowChange)
|
||||||
|
);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BumperTextVariant AddVariant(string name)
|
||||||
|
{
|
||||||
|
var nextPosition = _variants.Count == 0 ? 0 : _variants.Max(v => v.Position) + 1;
|
||||||
|
var variant = BumperTextVariant.Create(Id, nextPosition, name, BumperTrigger.OnShowChange);
|
||||||
|
_variants.Add(variant);
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BumperTextVariant? FindVariant(Guid variantId) =>
|
||||||
|
_variants.FirstOrDefault(v => v.Id == variantId);
|
||||||
|
|
||||||
|
/// <summary>Удалить подблок. Последний подблок удалить нельзя (нужен хотя бы один) — вернёт false.</summary>
|
||||||
|
public bool RemoveVariant(Guid variantId)
|
||||||
|
{
|
||||||
|
if (_variants.Count <= 1)
|
||||||
|
return false;
|
||||||
|
var variant = _variants.FirstOrDefault(v => v.Id == variantId);
|
||||||
|
if (variant is null)
|
||||||
|
return false;
|
||||||
|
_variants.Remove(variant);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Обновить имя и цвета блока. Цвета — в нотации ffmpeg (0xRRGGBB или имя).</summary>
|
/// <summary>Обновить имя и цвета блока. Цвета — в нотации ffmpeg (0xRRGGBB или имя).</summary>
|
||||||
public void UpdateStyle(
|
public void UpdateStyle(
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace TeleWave.Domain.Broadcast;
|
||||||
|
|
||||||
|
/// <summary>Как формируется текст подблока заставки.</summary>
|
||||||
|
public enum BumperTextKind
|
||||||
|
{
|
||||||
|
/// <summary>«Сейчас/Далее»: две подписи + названия текущего и следующего шоу.</summary>
|
||||||
|
NowNext,
|
||||||
|
|
||||||
|
/// <summary>Произвольные строки (без названий шоу) — например название канала и совет.</summary>
|
||||||
|
Free,
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
namespace TeleWave.Domain.Broadcast;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подблок заставки (текст-вариант) внутри <see cref="BumperTemplate"/>. Наследует от блока звук,
|
||||||
|
/// стиль и фон, но задаёт собственный текст и правило показа (<see cref="Trigger"/>). Позволяет иметь
|
||||||
|
/// несколько текстов на одной музыке/оформлении, не дублируя блок.
|
||||||
|
/// </summary>
|
||||||
|
public class BumperTextVariant
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public Guid BumperTemplateId { get; private set; }
|
||||||
|
public int Position { get; private set; }
|
||||||
|
public string Name { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public BumperTextKind Kind { get; private set; }
|
||||||
|
|
||||||
|
// ── Режим NowNext: подписи (названия шоу подставляет генератор) ──
|
||||||
|
public string NowLabel { get; private set; } = DefaultNowLabel;
|
||||||
|
public string NextLabel { get; private set; } = DefaultNextLabel;
|
||||||
|
|
||||||
|
// ── Режим Free: произвольные строки (например название канала и совет) ──
|
||||||
|
public string Line1 { get; private set; } = string.Empty;
|
||||||
|
public string Line2 { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public BumperTrigger Trigger { get; private set; }
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedAt { get; private set; }
|
||||||
|
|
||||||
|
public const string DefaultNowLabel = "СЕЙЧАС";
|
||||||
|
public const string DefaultNextLabel = "ДАЛЕЕ";
|
||||||
|
|
||||||
|
private BumperTextVariant() { }
|
||||||
|
|
||||||
|
internal static BumperTextVariant Create(
|
||||||
|
Guid bumperTemplateId,
|
||||||
|
int position,
|
||||||
|
string name,
|
||||||
|
BumperTrigger trigger
|
||||||
|
) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
BumperTemplateId = bumperTemplateId,
|
||||||
|
Position = position,
|
||||||
|
Name = name,
|
||||||
|
Kind = BumperTextKind.NowNext,
|
||||||
|
NowLabel = DefaultNowLabel,
|
||||||
|
NextLabel = DefaultNextLabel,
|
||||||
|
Line1 = string.Empty,
|
||||||
|
Line2 = string.Empty,
|
||||||
|
Trigger = trigger,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
public void Update(
|
||||||
|
string name,
|
||||||
|
BumperTextKind kind,
|
||||||
|
string nowLabel,
|
||||||
|
string nextLabel,
|
||||||
|
string line1,
|
||||||
|
string line2,
|
||||||
|
BumperTrigger trigger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Kind = kind;
|
||||||
|
NowLabel = nowLabel;
|
||||||
|
NextLabel = nextLabel;
|
||||||
|
Line1 = line1;
|
||||||
|
Line2 = line2;
|
||||||
|
Trigger = trigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Подходит ли подблок для перехода: <paramref name="isShowChange"/> — сменилось ли шоу.</summary>
|
||||||
|
public bool Matches(bool isShowChange) =>
|
||||||
|
Trigger switch
|
||||||
|
{
|
||||||
|
BumperTrigger.OnShowChange => isShowChange,
|
||||||
|
BumperTrigger.BetweenEpisodes => !isShowChange,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace TeleWave.Domain.Broadcast;
|
||||||
|
|
||||||
|
/// <summary>На каких переходах показывать подблок заставки.</summary>
|
||||||
|
public enum BumperTrigger
|
||||||
|
{
|
||||||
|
/// <summary>Только при смене шоу (следующее шоу отличается от текущего).</summary>
|
||||||
|
OnShowChange,
|
||||||
|
|
||||||
|
/// <summary>Только между блоками одного шоу (шоу не меняется).</summary>
|
||||||
|
BetweenEpisodes,
|
||||||
|
|
||||||
|
/// <summary>И на смене шоу, и между блоками одного шоу.</summary>
|
||||||
|
Both,
|
||||||
|
}
|
||||||
@@ -35,17 +35,10 @@ public class Channel
|
|||||||
public int NextBumperIndex { get; private set; }
|
public int NextBumperIndex { get; private set; }
|
||||||
|
|
||||||
public BumperFont BumperFont { get; private set; }
|
public BumperFont BumperFont { get; private set; }
|
||||||
public string BumperNowLabel { get; private set; } = DefaultNowLabel;
|
|
||||||
public string BumperNextLabel { get; private set; } = DefaultNextLabel;
|
|
||||||
|
|
||||||
/// <summary>Не вставлять заставку чаще, чем раз в N минут (0 — на каждом подходящем переходе).</summary>
|
/// <summary>Не вставлять заставку чаще, чем раз в N минут (0 — на каждом подходящем переходе).</summary>
|
||||||
public int BumperMinIntervalMinutes { get; private set; }
|
public int BumperMinIntervalMinutes { get; private set; }
|
||||||
|
|
||||||
/// <summary>Ставить заставку только на смене шоу (иначе — и внутри марафона одного шоу).</summary>
|
|
||||||
public bool BumperOnlyBetweenDifferentShows { get; private set; }
|
|
||||||
|
|
||||||
private const string DefaultNowLabel = "СЕЙЧАС";
|
|
||||||
private const string DefaultNextLabel = "ДАЛЕЕ";
|
|
||||||
private const string DefaultTemplateName = "Заставка 1";
|
private const string DefaultTemplateName = "Заставка 1";
|
||||||
|
|
||||||
/// <summary>Ассет-заглушка на случай пустого расписания (аварийная подстраховка).</summary>
|
/// <summary>Ассет-заглушка на случай пустого расписания (аварийная подстраховка).</summary>
|
||||||
@@ -82,10 +75,7 @@ public class Channel
|
|||||||
BumperSelection = BumperSelection.Rotation,
|
BumperSelection = BumperSelection.Rotation,
|
||||||
NextBumperIndex = 0,
|
NextBumperIndex = 0,
|
||||||
BumperFont = BumperFont.Sans,
|
BumperFont = BumperFont.Sans,
|
||||||
BumperNowLabel = DefaultNowLabel,
|
|
||||||
BumperNextLabel = DefaultNextLabel,
|
|
||||||
BumperMinIntervalMinutes = 0,
|
BumperMinIntervalMinutes = 0,
|
||||||
BumperOnlyBetweenDifferentShows = true,
|
|
||||||
NextAdIndex = 0,
|
NextAdIndex = 0,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
@@ -111,21 +101,15 @@ public class Channel
|
|||||||
FillerAssetId = fillerAssetId;
|
FillerAssetId = fillerAssetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Общие настройки ТВ-заставок канала: шрифт, подписи, правила показа и стратегия выбора блока.</summary>
|
/// <summary>Общие настройки ТВ-заставок канала: шрифт, мин. интервал и стратегия выбора подблока.</summary>
|
||||||
public void UpdateBumperSettings(
|
public void UpdateBumperSettings(
|
||||||
BumperFont font,
|
BumperFont font,
|
||||||
string nowLabel,
|
|
||||||
string nextLabel,
|
|
||||||
int minIntervalMinutes,
|
int minIntervalMinutes,
|
||||||
bool onlyBetweenDifferentShows,
|
|
||||||
BumperSelection selection
|
BumperSelection selection
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
BumperFont = font;
|
BumperFont = font;
|
||||||
BumperNowLabel = nowLabel;
|
|
||||||
BumperNextLabel = nextLabel;
|
|
||||||
BumperMinIntervalMinutes = Math.Max(0, minIntervalMinutes);
|
BumperMinIntervalMinutes = Math.Max(0, minIntervalMinutes);
|
||||||
BumperOnlyBetweenDifferentShows = onlyBetweenDifferentShows;
|
|
||||||
BumperSelection = selection;
|
BumperSelection = selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ public static class SchedulePlanner
|
|||||||
|
|
||||||
var pick = WeightedPick(candidates, random);
|
var pick = WeightedPick(candidates, random);
|
||||||
|
|
||||||
// ТВ-заставка на переходе. Резервируем слот выбранного блока фикс. длины — конкретный
|
// ТВ-заставка на переходе. Из подходящих подблоков (по правилу показа vs контексту)
|
||||||
// отрендеренный ассет («Сейчас/Далее» стилем блока поверх его звука) подставит оркестратор.
|
// резервируем слот выбранного блока — ассет подставит оркестратор.
|
||||||
if (
|
if (
|
||||||
prevShowId is { } prev
|
prevShowId is { } prev
|
||||||
&& input.Bumpers is { Enabled: true } bumper
|
&& input.Bumpers is { Enabled: true } bumper
|
||||||
&& (!bumper.OnlyBetweenDifferentShows || prev != pick.ShowId)
|
|
||||||
&& (
|
&& (
|
||||||
bumper.MinInterval <= TimeSpan.Zero
|
bumper.MinInterval <= TimeSpan.Zero
|
||||||
|| lastBumperAt is not { } last
|
|| lastBumperAt is not { } last
|
||||||
@@ -94,9 +93,10 @@ public static class SchedulePlanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ставит на переходе заставку выбранного блока: резервирует слот его длины и оставляет
|
/// Ставит на переходе заставку выбранного подблока: из подходящих по правилу показа (контекст —
|
||||||
/// плейсхолдер с парой шоу + id блока (ассет отрендерит оркестратор). Выбор блока — по стратегии
|
/// сменилось ли шоу) выбирает один по стратегии канала и резервирует слот длины его блока. Оставляет
|
||||||
/// канала (ротация двигает курсор). Возвращает true, если заставка добавлена (курсор сдвинут).
|
/// плейсхолдер с парой шоу + id блока/варианта (ассет отрендерит оркестратор). Возвращает true, если
|
||||||
|
/// заставка добавлена (курсор сдвинут).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool TryPlaceBumper(
|
private static bool TryPlaceBumper(
|
||||||
List<PlannedEntry> entries,
|
List<PlannedEntry> entries,
|
||||||
@@ -108,30 +108,30 @@ public static class SchedulePlanner
|
|||||||
ref DateTimeOffset cursor
|
ref DateTimeOffset cursor
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var templates = bumper.Templates;
|
var isShowChange = fromShowId != toShowId;
|
||||||
if (templates is not { Count: > 0 })
|
var eligible = bumper.Variants
|
||||||
|
.Where(v => v.Duration > TimeSpan.Zero && MatchesTrigger(v.Trigger, isShowChange))
|
||||||
|
.ToList();
|
||||||
|
if (eligible.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
PlannerBumperTemplate template;
|
PlannerBumperVariant variant;
|
||||||
switch (bumper.Selection)
|
switch (bumper.Selection)
|
||||||
{
|
{
|
||||||
case BumperSelection.Random:
|
case BumperSelection.Random:
|
||||||
template = templates[random.Next(templates.Count)];
|
variant = eligible[random.Next(eligible.Count)];
|
||||||
break;
|
break;
|
||||||
case BumperSelection.AlwaysFirst:
|
case BumperSelection.AlwaysFirst:
|
||||||
template = templates[0];
|
variant = eligible[0];
|
||||||
break;
|
break;
|
||||||
default: // Rotation
|
default: // Rotation
|
||||||
var idx = ((nextBumper % templates.Count) + templates.Count) % templates.Count;
|
var idx = ((nextBumper % eligible.Count) + eligible.Count) % eligible.Count;
|
||||||
template = templates[idx];
|
variant = eligible[idx];
|
||||||
nextBumper++;
|
nextBumper++;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (template.Duration <= TimeSpan.Zero)
|
var end = cursor + variant.Duration;
|
||||||
return false;
|
|
||||||
|
|
||||||
var end = cursor + template.Duration;
|
|
||||||
entries.Add(
|
entries.Add(
|
||||||
new PlannedEntry(
|
new PlannedEntry(
|
||||||
Guid.Empty,
|
Guid.Empty,
|
||||||
@@ -142,13 +142,22 @@ public static class SchedulePlanner
|
|||||||
null,
|
null,
|
||||||
fromShowId,
|
fromShowId,
|
||||||
toShowId,
|
toShowId,
|
||||||
template.TemplateId
|
variant.TemplateId,
|
||||||
|
variant.VariantId
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
cursor = end;
|
cursor = end;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool MatchesTrigger(BumperTrigger trigger, bool isShowChange) =>
|
||||||
|
trigger switch
|
||||||
|
{
|
||||||
|
BumperTrigger.OnShowChange => isShowChange,
|
||||||
|
BumperTrigger.BetweenEpisodes => !isShowChange,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
|
||||||
private static List<(PlannerShow Show, int Weight)> ResolvePolicy(
|
private static List<(PlannerShow Show, int Weight)> ResolvePolicy(
|
||||||
DateTimeOffset moment,
|
DateTimeOffset moment,
|
||||||
PlannerInput input,
|
PlannerInput input,
|
||||||
|
|||||||
@@ -22,21 +22,27 @@ public sealed record PlannerOverride(
|
|||||||
public sealed record PlannerOverrideShow(Guid ShowId, int Weight);
|
public sealed record PlannerOverrideShow(Guid ShowId, int Weight);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Политика ТВ-заставок на переходах. Планировщик выбирает блок (<see cref="Templates"/>) по
|
/// Политика ТВ-заставок на переходах. Планировщик из подходящих подблоков (<see cref="Variants"/>,
|
||||||
/// стратегии <see cref="Selection"/> и резервирует слот его длины (<see cref="PlannerBumperTemplate.Duration"/>,
|
/// фильтр по <see cref="PlannerBumperVariant.Trigger"/> и контексту перехода) выбирает один по стратегии
|
||||||
/// уже выровнена генератором на сегмент). Конкретный отрендеренный ассет подставляет оркестратор
|
/// <see cref="Selection"/> и резервирует слот длины его блока. Ассет подставляет оркестратор.
|
||||||
/// по паре шоу + выбранному блоку.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record PlannerBumperConfig(
|
public sealed record PlannerBumperConfig(
|
||||||
bool Enabled,
|
bool Enabled,
|
||||||
bool OnlyBetweenDifferentShows,
|
|
||||||
TimeSpan MinInterval,
|
TimeSpan MinInterval,
|
||||||
BumperSelection Selection,
|
BumperSelection Selection,
|
||||||
IReadOnlyList<PlannerBumperTemplate> Templates
|
IReadOnlyList<PlannerBumperVariant> Variants
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>Блок заставки в терминах планировщика: id + длительность слота (кратна сегменту).</summary>
|
/// <summary>
|
||||||
public sealed record PlannerBumperTemplate(Guid TemplateId, TimeSpan Duration);
|
/// Подблок заставки в терминах планировщика: id варианта + id родительского блока (стиль/звук) +
|
||||||
|
/// длительность слота (кратна сегменту) + правило показа.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record PlannerBumperVariant(
|
||||||
|
Guid VariantId,
|
||||||
|
Guid TemplateId,
|
||||||
|
TimeSpan Duration,
|
||||||
|
BumperTrigger Trigger
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>Полный вход планировщика для одного прогона по каналу.</summary>
|
/// <summary>Полный вход планировщика для одного прогона по каналу.</summary>
|
||||||
public sealed record PlannerInput(
|
public sealed record PlannerInput(
|
||||||
@@ -69,7 +75,8 @@ public sealed record PlannedEntry(
|
|||||||
int? EpisodeIndex,
|
int? EpisodeIndex,
|
||||||
Guid? FromShowId = null,
|
Guid? FromShowId = null,
|
||||||
Guid? ToShowId = null,
|
Guid? ToShowId = null,
|
||||||
Guid? BumperTemplateId = null
|
Guid? BumperTemplateId = null,
|
||||||
|
Guid? BumperVariantId = null
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>Результат прогона: новые записи + обновлённые курсоры (серий по каждому ChannelShow, рекламы, заставок).</summary>
|
/// <summary>Результат прогона: новые записи + обновлённые курсоры (серий по каждому ChannelShow, рекламы, заставок).</summary>
|
||||||
|
|||||||
@@ -35,12 +35,14 @@ public sealed class FfmpegBumperRenderer(
|
|||||||
Directory.Delete(assetDir, recursive: true);
|
Directory.Delete(assetDir, recursive: true);
|
||||||
Directory.CreateDirectory(assetDir);
|
Directory.CreateDirectory(assetDir);
|
||||||
|
|
||||||
// Динамический текст (названия шоу) пишем в файлы и читаем через textfile= с expansion=none —
|
// Динамический текст (названия шоу / свободные строки) пишем в файлы и читаем через textfile=
|
||||||
// так произвольные символы/кириллица не ломают синтаксис фильтра.
|
// с expansion=none — так произвольные символы/кириллица не ломают синтаксис фильтра.
|
||||||
var nowFile = Path.Combine(assetDir, "now.txt");
|
var nowFile = Path.Combine(assetDir, "now.txt");
|
||||||
var nextFile = Path.Combine(assetDir, "next.txt");
|
var nextFile = Path.Combine(assetDir, "next.txt");
|
||||||
await File.WriteAllTextAsync(nowFile, spec.NowTitle, new UTF8Encoding(false), cancellationToken);
|
var line1 = spec.FreeText ? spec.FreeLine1 : spec.NowTitle;
|
||||||
await File.WriteAllTextAsync(nextFile, spec.NextTitle, new UTF8Encoding(false), cancellationToken);
|
var line2 = spec.FreeText ? spec.FreeLine2 : spec.NextTitle;
|
||||||
|
await File.WriteAllTextAsync(nowFile, line1, new UTF8Encoding(false), cancellationToken);
|
||||||
|
await File.WriteAllTextAsync(nextFile, line2, new UTF8Encoding(false), cancellationToken);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -156,10 +158,21 @@ public sealed class FfmpegBumperRenderer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var vchain = new StringBuilder(videoPrefix);
|
var vchain = new StringBuilder(videoPrefix);
|
||||||
vchain.Append(',').Append(DrawLabel(font, spec.NowLabel, spec.AccentColor, labelSize, nowLabelY, 0.2));
|
if (spec.FreeText)
|
||||||
vchain.Append(',').Append(DrawTitle(font, nowFile, spec.TextColor, titleSize, nowTitleY, 0.3));
|
{
|
||||||
vchain.Append(',').Append(DrawLabel(font, spec.NextLabel, spec.AccentColor, labelSize, nextLabelY, 1.0));
|
// Свободный текст: две центрированные строки (акцентная + основная).
|
||||||
vchain.Append(',').Append(DrawTitle(font, nextFile, spec.TextColor, titleSize, nextTitleY, 1.1));
|
var line1Y = (int)(h * 0.40);
|
||||||
|
var line2Y = line1Y + (int)(titleSize * 1.2);
|
||||||
|
vchain.Append(',').Append(DrawTitle(font, nowFile, spec.AccentColor, labelSize + 4, line1Y, 0.2));
|
||||||
|
vchain.Append(',').Append(DrawTitle(font, nextFile, spec.TextColor, titleSize, line2Y, 0.5));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
vchain.Append(',').Append(DrawLabel(font, spec.NowLabel, spec.AccentColor, labelSize, nowLabelY, 0.2));
|
||||||
|
vchain.Append(',').Append(DrawTitle(font, nowFile, spec.TextColor, titleSize, nowTitleY, 0.3));
|
||||||
|
vchain.Append(',').Append(DrawLabel(font, spec.NextLabel, spec.AccentColor, labelSize, nextLabelY, 1.0));
|
||||||
|
vchain.Append(',').Append(DrawTitle(font, nextFile, spec.TextColor, titleSize, nextTitleY, 1.1));
|
||||||
|
}
|
||||||
vchain.Append("[v]");
|
vchain.Append("[v]");
|
||||||
|
|
||||||
var filterComplex = $"{vchain};{audioChain}";
|
var filterComplex = $"{vchain};{audioChain}";
|
||||||
|
|||||||
Generated
+942
@@ -0,0 +1,942 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using TeleWave.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TeleWave.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260725101820_BumperTextVariants")]
|
||||||
|
partial class BumperTextVariants
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Auth.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperAsset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("FromShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Signature")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ToShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FromShowId", "ToShowId", "Signature");
|
||||||
|
|
||||||
|
b.ToTable("BumperAssets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AccentColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<double?>("AudioDurationSeconds")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<string>("AudioExtension")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundColor2")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("BackgroundImageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Revision")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TextColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("BumperTemplate");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTextVariant", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("BumperTemplateId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Line1")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<string>("Line2")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("NextLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("NowLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Trigger")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BumperTemplateId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("BumperTextVariant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AdInsertion")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("AdsPerBreak")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BumperFont")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BumperMinIntervalMinutes")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BumperSelection")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("BumpersEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("EpochUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FillerAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<int>("NextAdIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("NextBumperIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Channels");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelAd", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("ChannelAd");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelShow", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("BlockMode")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BlockValue")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("NextEpisodeIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "ShowId");
|
||||||
|
|
||||||
|
b.ToTable("ChannelShow");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.OverrideShow", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProgrammingOverrideId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProgrammingOverrideId");
|
||||||
|
|
||||||
|
b.ToTable("OverrideShow");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ProgrammingOverride", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("EndsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Mode")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "StartsAtUtc", "EndsAtUtc");
|
||||||
|
|
||||||
|
b.ToTable("ProgrammingOverride");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ScheduleEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("EndsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "EndsAtUtc");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "ShowId");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "StartsAtUtc");
|
||||||
|
|
||||||
|
b.ToTable("ScheduleEntries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Images.Image", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Category")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("FileExtension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFileName")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Category", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("Images");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.Show", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("MetadataExternalId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("MetadataProvider")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PosterImageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("Year")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.ShowEpisode", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("AirDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int?>("Episode")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("Season")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("StillImageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MediaAssetId");
|
||||||
|
|
||||||
|
b.HasIndex("ShowId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("ShowEpisode");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Media.MediaAsset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AudioCodec")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<TimeSpan?>("Duration")
|
||||||
|
.HasColumnType("interval");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<int?>("Height")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalExtension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<int?>("SegmentCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SegmentSeconds")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Source")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("VideoCodec")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<int?>("Width")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("MediaAssets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Settings.AppSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("AppSettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Infrastructure.Identity.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSystem")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Infrastructure.Identity.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBlocked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("BumperTemplates")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTextVariant", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.BumperTemplate", null)
|
||||||
|
.WithMany("Variants")
|
||||||
|
.HasForeignKey("BumperTemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelAd", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("Ads")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelShow", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("Shows")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.OverrideShow", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.ProgrammingOverride", null)
|
||||||
|
.WithMany("Shows")
|
||||||
|
.HasForeignKey("ProgrammingOverrideId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ProgrammingOverride", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("Overrides")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.ShowEpisode", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Library.Show", null)
|
||||||
|
.WithMany("Episodes")
|
||||||
|
.HasForeignKey("ShowId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Variants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Ads");
|
||||||
|
|
||||||
|
b.Navigation("BumperTemplates");
|
||||||
|
|
||||||
|
b.Navigation("Overrides");
|
||||||
|
|
||||||
|
b.Navigation("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ProgrammingOverride", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.Show", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Episodes");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TeleWave.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class BumperTextVariants : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "BumperTextVariant",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
BumperTemplateId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Position = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
Kind = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
NowLabel = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
NextLabel = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
Line1 = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||||
|
Line2 = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||||
|
Trigger = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_BumperTextVariant", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_BumperTextVariant_BumperTemplate_BumperTemplateId",
|
||||||
|
column: x => x.BumperTemplateId,
|
||||||
|
principalTable: "BumperTemplate",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BumperTextVariant_BumperTemplateId_Position",
|
||||||
|
table: "BumperTextVariant",
|
||||||
|
columns: new[] { "BumperTemplateId", "Position" });
|
||||||
|
|
||||||
|
// Каждому блоку — дефолтный подблок «Сейчас/Далее» с прежними подписями канала; правило
|
||||||
|
// показа переносим из старой галочки (только на смене шоу → OnShowChange=0, иначе Both=2).
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
DECLARE r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT t."Id" AS tid, c."BumperNowLabel" AS nl, c."BumperNextLabel" AS xl,
|
||||||
|
c."BumperOnlyBetweenDifferentShows" AS only_diff
|
||||||
|
FROM "BumperTemplate" t
|
||||||
|
JOIN "Channels" c ON c."Id" = t."ChannelId"
|
||||||
|
LOOP
|
||||||
|
INSERT INTO "BumperTextVariant"
|
||||||
|
("Id", "BumperTemplateId", "Position", "Name", "Kind",
|
||||||
|
"NowLabel", "NextLabel", "Line1", "Line2", "Trigger", "CreatedAt")
|
||||||
|
VALUES (gen_random_uuid(), r.tid, 0, 'Текст 1', 0,
|
||||||
|
COALESCE(NULLIF(r.nl, ''), 'СЕЙЧАС'), COALESCE(NULLIF(r.xl, ''), 'ДАЛЕЕ'),
|
||||||
|
'', '', CASE WHEN r.only_diff THEN 0 ELSE 2 END, now());
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(name: "BumperNextLabel", table: "Channels");
|
||||||
|
migrationBuilder.DropColumn(name: "BumperNowLabel", table: "Channels");
|
||||||
|
migrationBuilder.DropColumn(name: "BumperOnlyBetweenDifferentShows", table: "Channels");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BumperTextVariant");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BumperNextLabel",
|
||||||
|
table: "Channels",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BumperNowLabel",
|
||||||
|
table: "Channels",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "BumperOnlyBetweenDifferentShows",
|
||||||
|
table: "Channels",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -247,6 +247,58 @@ namespace TeleWave.Infrastructure.Migrations
|
|||||||
b.ToTable("BumperTemplate");
|
b.ToTable("BumperTemplate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTextVariant", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("BumperTemplateId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Line1")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<string>("Line2")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("character varying(120)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("NextLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("NowLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Trigger")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BumperTemplateId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("BumperTextVariant");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -264,17 +316,6 @@ namespace TeleWave.Infrastructure.Migrations
|
|||||||
b.Property<int>("BumperMinIntervalMinutes")
|
b.Property<int>("BumperMinIntervalMinutes")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<string>("BumperNextLabel")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("BumperNowLabel")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<bool>("BumperOnlyBetweenDifferentShows")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<int>("BumperSelection")
|
b.Property<int>("BumperSelection")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
@@ -813,6 +854,15 @@ namespace TeleWave.Infrastructure.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTextVariant", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.BumperTemplate", null)
|
||||||
|
.WithMany("Variants")
|
||||||
|
.HasForeignKey("BumperTemplateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelAd", b =>
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelAd", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
@@ -858,6 +908,11 @@ namespace TeleWave.Infrastructure.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Variants");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Ads");
|
b.Navigation("Ads");
|
||||||
|
|||||||
+20
@@ -70,6 +70,26 @@ public class BumperTemplateConfiguration : IEntityTypeConfiguration<BumperTempla
|
|||||||
builder.Property(x => x.AccentColor).IsRequired().HasMaxLength(32);
|
builder.Property(x => x.AccentColor).IsRequired().HasMaxLength(32);
|
||||||
builder.Property(x => x.TextColor).IsRequired().HasMaxLength(32);
|
builder.Property(x => x.TextColor).IsRequired().HasMaxLength(32);
|
||||||
builder.Property(x => x.AudioExtension).HasMaxLength(16);
|
builder.Property(x => x.AudioExtension).HasMaxLength(16);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasMany(x => x.Variants)
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey(v => v.BumperTemplateId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
builder.Navigation(x => x.Variants).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BumperTextVariantConfiguration : IEntityTypeConfiguration<BumperTextVariant>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<BumperTextVariant> builder)
|
||||||
|
{
|
||||||
|
builder.HasIndex(x => new { x.BumperTemplateId, x.Position });
|
||||||
|
builder.Property(x => x.Name).IsRequired().HasMaxLength(64);
|
||||||
|
builder.Property(x => x.NowLabel).IsRequired().HasMaxLength(64);
|
||||||
|
builder.Property(x => x.NextLabel).IsRequired().HasMaxLength(64);
|
||||||
|
builder.Property(x => x.Line1).IsRequired().HasMaxLength(120);
|
||||||
|
builder.Property(x => x.Line2).IsRequired().HasMaxLength(120);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,23 +21,23 @@ public class SchedulePlannerTests
|
|||||||
items.ToDictionary(x => x.Id, x => TimeSpan.FromMinutes(x.Minutes));
|
items.ToDictionary(x => x.Id, x => TimeSpan.FromMinutes(x.Minutes));
|
||||||
|
|
||||||
private static readonly Guid DefaultTemplate = Guid.NewGuid();
|
private static readonly Guid DefaultTemplate = Guid.NewGuid();
|
||||||
|
private static readonly Guid DefaultVariant = Guid.NewGuid();
|
||||||
|
|
||||||
/// <summary>Конфиг заставок с одним дефолтным блоком (8с), если явно не заданы блоки.</summary>
|
/// <summary>Конфиг заставок с одним дефолтным подблоком (8с) заданного триггера, если варианты не заданы.</summary>
|
||||||
private static PlannerBumperConfig Bumper(
|
private static PlannerBumperConfig Bumper(
|
||||||
bool enabled,
|
bool enabled,
|
||||||
bool onlyBetweenDifferentShows,
|
BumperTrigger trigger,
|
||||||
TimeSpan minInterval,
|
TimeSpan minInterval,
|
||||||
BumperSelection selection = BumperSelection.Rotation,
|
BumperSelection selection = BumperSelection.Rotation,
|
||||||
params PlannerBumperTemplate[] templates
|
params PlannerBumperVariant[] variants
|
||||||
) =>
|
) =>
|
||||||
new(
|
new(
|
||||||
enabled,
|
enabled,
|
||||||
onlyBetweenDifferentShows,
|
|
||||||
minInterval,
|
minInterval,
|
||||||
selection,
|
selection,
|
||||||
templates.Length == 0
|
variants.Length == 0
|
||||||
? [new PlannerBumperTemplate(DefaultTemplate, TimeSpan.FromSeconds(8))]
|
? [new PlannerBumperVariant(DefaultVariant, DefaultTemplate, TimeSpan.FromSeconds(8), trigger)]
|
||||||
: templates
|
: variants
|
||||||
);
|
);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -232,7 +232,7 @@ public class SchedulePlannerTests
|
|||||||
|
|
||||||
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
|
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
|
||||||
{
|
{
|
||||||
Bumpers = Bumper(true, onlyBetweenDifferentShows: true, TimeSpan.Zero),
|
Bumpers = Bumper(true, BumperTrigger.OnShowChange, TimeSpan.Zero),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Чередуем выбор: roll 0 → a, roll 1 → b (веса 1/1, total 2).
|
// Чередуем выбор: roll 0 → a, roll 1 → b (веса 1/1, total 2).
|
||||||
@@ -267,7 +267,7 @@ public class SchedulePlannerTests
|
|||||||
horizonEnd: Start.AddMinutes(50)
|
horizonEnd: Start.AddMinutes(50)
|
||||||
) with
|
) with
|
||||||
{
|
{
|
||||||
Bumpers = Bumper(true, onlyBetweenDifferentShows: true, TimeSpan.Zero),
|
Bumpers = Bumper(true, BumperTrigger.OnShowChange, TimeSpan.Zero),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||||
@@ -287,7 +287,7 @@ public class SchedulePlannerTests
|
|||||||
horizonEnd: Start.AddMinutes(50)
|
horizonEnd: Start.AddMinutes(50)
|
||||||
) with
|
) with
|
||||||
{
|
{
|
||||||
Bumpers = Bumper(true, onlyBetweenDifferentShows: false, TimeSpan.Zero),
|
Bumpers = Bumper(true, BumperTrigger.Both, TimeSpan.Zero),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||||
@@ -305,7 +305,7 @@ public class SchedulePlannerTests
|
|||||||
// Два перехода в горизонте (~на 20-й и ~40-й минуте), но интервал 30 мин пропускает второй.
|
// Два перехода в горизонте (~на 20-й и ~40-й минуте), но интервал 30 мин пропускает второй.
|
||||||
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
|
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
|
||||||
{
|
{
|
||||||
Bumpers = Bumper(true, onlyBetweenDifferentShows: true, TimeSpan.FromMinutes(30)),
|
Bumpers = Bumper(true, BumperTrigger.OnShowChange, TimeSpan.FromMinutes(30)),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
|
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
|
||||||
@@ -326,11 +326,11 @@ public class SchedulePlannerTests
|
|||||||
{
|
{
|
||||||
Bumpers = Bumper(
|
Bumpers = Bumper(
|
||||||
true,
|
true,
|
||||||
onlyBetweenDifferentShows: true,
|
BumperTrigger.Both,
|
||||||
TimeSpan.Zero,
|
TimeSpan.Zero,
|
||||||
BumperSelection.Rotation,
|
BumperSelection.Rotation,
|
||||||
new PlannerBumperTemplate(t0, TimeSpan.FromSeconds(8)),
|
new PlannerBumperVariant(Guid.NewGuid(), t0, TimeSpan.FromSeconds(8), BumperTrigger.Both),
|
||||||
new PlannerBumperTemplate(t1, TimeSpan.FromSeconds(4))
|
new PlannerBumperVariant(Guid.NewGuid(), t1, TimeSpan.FromSeconds(4), BumperTrigger.Both)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ public class SchedulePlannerTests
|
|||||||
|
|
||||||
var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList();
|
var bumpers = result.Entries.Where(e => e.Kind == ScheduleEntryKind.Bumper).ToList();
|
||||||
Assert.True(bumpers.Count >= 2);
|
Assert.True(bumpers.Count >= 2);
|
||||||
// Ротация: первый блок → t0 (8с), второй → t1 (4с). Все — плейсхолдеры по паре шоу.
|
// Ротация: первый подблок → t0 (8с), второй → t1 (4с). Все — плейсхолдеры по паре шоу.
|
||||||
Assert.Equal(t0, bumpers[0].BumperTemplateId);
|
Assert.Equal(t0, bumpers[0].BumperTemplateId);
|
||||||
Assert.Equal(TimeSpan.FromSeconds(8), bumpers[0].EndsAtUtc - bumpers[0].StartsAtUtc);
|
Assert.Equal(TimeSpan.FromSeconds(8), bumpers[0].EndsAtUtc - bumpers[0].StartsAtUtc);
|
||||||
Assert.Equal(t1, bumpers[1].BumperTemplateId);
|
Assert.Equal(t1, bumpers[1].BumperTemplateId);
|
||||||
@@ -360,11 +360,11 @@ public class SchedulePlannerTests
|
|||||||
{
|
{
|
||||||
Bumpers = Bumper(
|
Bumpers = Bumper(
|
||||||
true,
|
true,
|
||||||
onlyBetweenDifferentShows: true,
|
BumperTrigger.Both,
|
||||||
TimeSpan.Zero,
|
TimeSpan.Zero,
|
||||||
BumperSelection.AlwaysFirst,
|
BumperSelection.AlwaysFirst,
|
||||||
new PlannerBumperTemplate(t0, TimeSpan.FromSeconds(8)),
|
new PlannerBumperVariant(Guid.NewGuid(), t0, TimeSpan.FromSeconds(8), BumperTrigger.Both),
|
||||||
new PlannerBumperTemplate(t1, TimeSpan.FromSeconds(4))
|
new PlannerBumperVariant(Guid.NewGuid(), t1, TimeSpan.FromSeconds(4), BumperTrigger.Both)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -387,10 +387,9 @@ public class SchedulePlannerTests
|
|||||||
{
|
{
|
||||||
Bumpers = new PlannerBumperConfig(
|
Bumpers = new PlannerBumperConfig(
|
||||||
true,
|
true,
|
||||||
OnlyBetweenDifferentShows: true,
|
|
||||||
MinInterval: TimeSpan.Zero,
|
MinInterval: TimeSpan.Zero,
|
||||||
Selection: BumperSelection.Rotation,
|
Selection: BumperSelection.Rotation,
|
||||||
Templates: []
|
Variants: []
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -408,7 +407,7 @@ public class SchedulePlannerTests
|
|||||||
|
|
||||||
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
|
var input = BaseInput([a, b], durations, Start.AddMinutes(50)) with
|
||||||
{
|
{
|
||||||
Bumpers = Bumper(false, onlyBetweenDifferentShows: true, TimeSpan.Zero),
|
Bumpers = Bumper(false, BumperTrigger.OnShowChange, TimeSpan.Zero),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
|
var result = SchedulePlanner.Plan(input, new FixedRandom(0, 1));
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import type {
|
|||||||
BumperSelection,
|
BumperSelection,
|
||||||
BumperSettings,
|
BumperSettings,
|
||||||
BumperTemplateDto,
|
BumperTemplateDto,
|
||||||
|
BumperTextKind,
|
||||||
|
BumperTextVariantDto,
|
||||||
|
BumperTrigger,
|
||||||
ChannelShowDto,
|
ChannelShowDto,
|
||||||
OverrideMode,
|
OverrideMode,
|
||||||
ScheduleEntryDto,
|
ScheduleEntryDto,
|
||||||
@@ -29,6 +32,7 @@ import { listMedia } from '@/features/admin/media/api'
|
|||||||
import { listShows } from '@/features/admin/shows/api'
|
import { listShows } from '@/features/admin/shows/api'
|
||||||
import {
|
import {
|
||||||
addBumperTemplate,
|
addBumperTemplate,
|
||||||
|
addBumperVariant,
|
||||||
addChannelAd,
|
addChannelAd,
|
||||||
addChannelShow,
|
addChannelShow,
|
||||||
bumperPreviewPlaylistUrl,
|
bumperPreviewPlaylistUrl,
|
||||||
@@ -40,11 +44,13 @@ import {
|
|||||||
getSchedule,
|
getSchedule,
|
||||||
regenerateSchedule,
|
regenerateSchedule,
|
||||||
removeBumperTemplate,
|
removeBumperTemplate,
|
||||||
|
removeBumperVariant,
|
||||||
removeChannelAd,
|
removeChannelAd,
|
||||||
removeChannelShow,
|
removeChannelShow,
|
||||||
renderBumperPreview,
|
renderBumperPreview,
|
||||||
setBumperTemplateBackground,
|
setBumperTemplateBackground,
|
||||||
updateBumperTemplate,
|
updateBumperTemplate,
|
||||||
|
updateBumperVariant,
|
||||||
updateChannelSettings,
|
updateChannelSettings,
|
||||||
updateChannelShow,
|
updateChannelShow,
|
||||||
uploadBumperTemplateAudio,
|
uploadBumperTemplateAudio,
|
||||||
@@ -483,31 +489,7 @@ function BumperCard({
|
|||||||
onChange={(e) => setField('minIntervalMinutes', Number(e.target.value))}
|
onChange={(e) => setField('minIntervalMinutes', Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<Label>{t('admin.channels.bumperNowLabel')}</Label>
|
|
||||||
<Input
|
|
||||||
value={bumper.nowLabel}
|
|
||||||
maxLength={64}
|
|
||||||
onChange={(e) => setField('nowLabel', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<Label>{t('admin.channels.bumperNextLabel')}</Label>
|
|
||||||
<Input
|
|
||||||
value={bumper.nextLabel}
|
|
||||||
maxLength={64}
|
|
||||||
onChange={(e) => setField('nextLabel', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={bumper.onlyBetweenDifferentShows}
|
|
||||||
onChange={(e) => setField('onlyBetweenDifferentShows', e.target.checked)}
|
|
||||||
/>
|
|
||||||
{t('admin.channels.bumperOnlyDifferent')}
|
|
||||||
</label>
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button size="sm" disabled={save.isPending} onClick={() => save.mutate()}>
|
<Button size="sm" disabled={save.isPending} onClick={() => save.mutate()}>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
@@ -589,6 +571,11 @@ function BumperTemplateEditor({
|
|||||||
onSuccess: onChanged,
|
onSuccess: onChanged,
|
||||||
onError,
|
onError,
|
||||||
})
|
})
|
||||||
|
const addVariant = useMutation({
|
||||||
|
mutationFn: () => addBumperVariant(channelId, template.id, ''),
|
||||||
|
onSuccess: onChanged,
|
||||||
|
onError,
|
||||||
|
})
|
||||||
|
|
||||||
const colorFields: { key: keyof typeof colors; label: string }[] = [
|
const colorFields: { key: keyof typeof colors; label: string }[] = [
|
||||||
{ key: 'backgroundColor', label: t('admin.channels.bumperBg') },
|
{ key: 'backgroundColor', label: t('admin.channels.bumperBg') },
|
||||||
@@ -677,6 +664,35 @@ function BumperTemplateEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Подблоки (текст-варианты) */}
|
||||||
|
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||||
|
<p className="text-sm font-medium">{t('admin.channels.bumperVariants')}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{t('admin.channels.bumperVariantsHint')}</p>
|
||||||
|
{[...template.variants]
|
||||||
|
.sort((a, b) => a.position - b.position)
|
||||||
|
.map((variant) => (
|
||||||
|
<BumperVariantEditor
|
||||||
|
key={variant.id}
|
||||||
|
channelId={channelId}
|
||||||
|
templateId={template.id}
|
||||||
|
variant={variant}
|
||||||
|
canRemove={template.variants.length > 1}
|
||||||
|
onChanged={onChanged}
|
||||||
|
onError={onError}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={addVariant.isPending}
|
||||||
|
onClick={() => addVariant.mutate()}
|
||||||
|
>
|
||||||
|
{t('admin.channels.bumperAddVariant')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||||
<BumperPreviewPlayer channelId={channelId} templateId={template.id} onError={onError} />
|
<BumperPreviewPlayer channelId={channelId} templateId={template.id} onError={onError} />
|
||||||
</div>
|
</div>
|
||||||
@@ -692,6 +708,166 @@ function BumperTemplateEditor({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BumperVariantEditor({
|
||||||
|
channelId,
|
||||||
|
templateId,
|
||||||
|
variant,
|
||||||
|
canRemove,
|
||||||
|
onChanged,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
channelId: string
|
||||||
|
templateId: string
|
||||||
|
variant: BumperTextVariantDto
|
||||||
|
canRemove: boolean
|
||||||
|
onChanged: () => void
|
||||||
|
onError: (e: unknown) => void
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
name: variant.name,
|
||||||
|
kind: variant.kind,
|
||||||
|
nowLabel: variant.nowLabel,
|
||||||
|
nextLabel: variant.nextLabel,
|
||||||
|
line1: variant.line1,
|
||||||
|
line2: variant.line2,
|
||||||
|
trigger: variant.trigger,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setForm({
|
||||||
|
name: variant.name,
|
||||||
|
kind: variant.kind,
|
||||||
|
nowLabel: variant.nowLabel,
|
||||||
|
nextLabel: variant.nextLabel,
|
||||||
|
line1: variant.line1,
|
||||||
|
line2: variant.line2,
|
||||||
|
trigger: variant.trigger,
|
||||||
|
})
|
||||||
|
}, [variant])
|
||||||
|
|
||||||
|
const set = <K extends keyof typeof form>(key: K, value: (typeof form)[K]) =>
|
||||||
|
setForm((f) => ({ ...f, [key]: value }))
|
||||||
|
|
||||||
|
const save = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
updateBumperVariant(channelId, templateId, variant.id, { ...form, name: form.name.trim() }),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('settings.saved'))
|
||||||
|
onChanged()
|
||||||
|
},
|
||||||
|
onError,
|
||||||
|
})
|
||||||
|
const remove = useMutation({
|
||||||
|
mutationFn: () => removeBumperVariant(channelId, templateId, variant.id),
|
||||||
|
onSuccess: onChanged,
|
||||||
|
onError,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 rounded-md border border-border bg-background/40 p-3">
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>{t('admin.channels.bumperVariantName')}</Label>
|
||||||
|
<Input value={form.name} maxLength={64} onChange={(e) => set('name', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>{t('admin.channels.bumperTextKind')}</Label>
|
||||||
|
<Select value={form.kind} onValueChange={(v) => set('kind', v as BumperTextKind)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="NowNext">{t('admin.channels.bumperKindNowNext')}</SelectItem>
|
||||||
|
<SelectItem value="Free">{t('admin.channels.bumperKindFree')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>{t('admin.channels.bumperTrigger')}</Label>
|
||||||
|
<Select value={form.trigger} onValueChange={(v) => set('trigger', v as BumperTrigger)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="OnShowChange">
|
||||||
|
{t('admin.channels.bumperTriggerOnShowChange')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="BetweenEpisodes">
|
||||||
|
{t('admin.channels.bumperTriggerBetweenEpisodes')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="Both">{t('admin.channels.bumperTriggerBoth')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
|
{form.kind === 'NowNext' ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>{t('admin.channels.bumperNowLabel')}</Label>
|
||||||
|
<Input
|
||||||
|
value={form.nowLabel}
|
||||||
|
maxLength={64}
|
||||||
|
onChange={(e) => set('nowLabel', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>{t('admin.channels.bumperNextLabel')}</Label>
|
||||||
|
<Input
|
||||||
|
value={form.nextLabel}
|
||||||
|
maxLength={64}
|
||||||
|
onChange={(e) => set('nextLabel', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>{t('admin.channels.bumperLine1')}</Label>
|
||||||
|
<Input
|
||||||
|
value={form.line1}
|
||||||
|
maxLength={120}
|
||||||
|
onChange={(e) => set('line1', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>{t('admin.channels.bumperLine2')}</Label>
|
||||||
|
<Input
|
||||||
|
value={form.line2}
|
||||||
|
maxLength={120}
|
||||||
|
onChange={(e) => set('line2', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
{canRemove && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={remove.isPending}
|
||||||
|
onClick={() => remove.mutate()}
|
||||||
|
>
|
||||||
|
{t('common.delete')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={save.isPending || !form.name.trim()}
|
||||||
|
onClick={() => save.mutate()}
|
||||||
|
>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function BumperPreviewPlayer({
|
function BumperPreviewPlayer({
|
||||||
channelId,
|
channelId,
|
||||||
templateId,
|
templateId,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type {
|
|||||||
AdInsertion,
|
AdInsertion,
|
||||||
BlockMode,
|
BlockMode,
|
||||||
BumperSettings,
|
BumperSettings,
|
||||||
|
BumperTextKind,
|
||||||
|
BumperTrigger,
|
||||||
ChannelDto,
|
ChannelDto,
|
||||||
ChannelSummaryDto,
|
ChannelSummaryDto,
|
||||||
CreatedIdResponse,
|
CreatedIdResponse,
|
||||||
@@ -137,6 +139,42 @@ export function uploadBumperTemplateAudio(id: string, templateId: string, file:
|
|||||||
return uploadBumperTemplateFile(id, templateId, 'audio', file)
|
return uploadBumperTemplateFile(id, templateId, 'audio', file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BumperVariantBody = {
|
||||||
|
name: string
|
||||||
|
kind: BumperTextKind
|
||||||
|
nowLabel: string
|
||||||
|
nextLabel: string
|
||||||
|
line1: string
|
||||||
|
line2: string
|
||||||
|
trigger: BumperTrigger
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addBumperVariant(id: string, templateId: string, name: string) {
|
||||||
|
return apiRequest<CreatedIdResponse>(
|
||||||
|
`/admin/channels/${id}/bumper/templates/${templateId}/variants`,
|
||||||
|
{ method: 'POST', body: { name } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateBumperVariant(
|
||||||
|
id: string,
|
||||||
|
templateId: string,
|
||||||
|
variantId: string,
|
||||||
|
body: BumperVariantBody,
|
||||||
|
) {
|
||||||
|
return apiRequest<void>(
|
||||||
|
`/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`,
|
||||||
|
{ method: 'PUT', body },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeBumperVariant(id: string, templateId: string, variantId: string) {
|
||||||
|
return apiRequest<void>(
|
||||||
|
`/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Привязать фон-картинку блока по ссылке на изображение из реестра (галерея). */
|
/** Привязать фон-картинку блока по ссылке на изображение из реестра (галерея). */
|
||||||
export function setBumperTemplateBackground(id: string, templateId: string, imageId: string) {
|
export function setBumperTemplateBackground(id: string, templateId: string, imageId: string) {
|
||||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
||||||
|
|||||||
@@ -128,15 +128,27 @@ export type OverrideMode = 'Exclusive' | 'Boost'
|
|||||||
export type ScheduleEntryKind = 'Program' | 'Ad' | 'Bumper'
|
export type ScheduleEntryKind = 'Program' | 'Ad' | 'Bumper'
|
||||||
export type BumperFont = 'Sans' | 'Serif'
|
export type BumperFont = 'Sans' | 'Serif'
|
||||||
export type BumperSelection = 'Rotation' | 'Random' | 'AlwaysFirst'
|
export type BumperSelection = 'Rotation' | 'Random' | 'AlwaysFirst'
|
||||||
|
export type BumperTextKind = 'NowNext' | 'Free'
|
||||||
|
export type BumperTrigger = 'OnShowChange' | 'BetweenEpisodes' | 'Both'
|
||||||
|
|
||||||
/** Общие для канала настройки заставок (стиль/звук — на каждом блоке, см. BumperTemplateDto). */
|
/** Общие для канала настройки заставок (стиль/звук/текст — на блоках и подблоках). */
|
||||||
export type BumperSettings = {
|
export type BumperSettings = {
|
||||||
font: BumperFont
|
font: BumperFont
|
||||||
|
minIntervalMinutes: number
|
||||||
|
selection: BumperSelection
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Подблок (текст-вариант): свой текст + правило показа поверх стиля/звука блока. */
|
||||||
|
export type BumperTextVariantDto = {
|
||||||
|
id: string
|
||||||
|
position: number
|
||||||
|
name: string
|
||||||
|
kind: BumperTextKind
|
||||||
nowLabel: string
|
nowLabel: string
|
||||||
nextLabel: string
|
nextLabel: string
|
||||||
minIntervalMinutes: number
|
line1: string
|
||||||
onlyBetweenDifferentShows: boolean
|
line2: string
|
||||||
selection: BumperSelection
|
trigger: BumperTrigger
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BumperTemplateDto = {
|
export type BumperTemplateDto = {
|
||||||
@@ -151,6 +163,7 @@ export type BumperTemplateDto = {
|
|||||||
backgroundImageId: string | null
|
backgroundImageId: string | null
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
audioDurationSeconds: number | null
|
audioDurationSeconds: number | null
|
||||||
|
variants: BumperTextVariantDto[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChannelSummaryDto = {
|
export type ChannelSummaryDto = {
|
||||||
|
|||||||
@@ -220,6 +220,20 @@ const resources = {
|
|||||||
'Каждый блок — свой звук и оформление. Первый блок дефолтный, его нельзя удалить. Длительность заставки — по длине звука.',
|
'Каждый блок — свой звук и оформление. Первый блок дефолтный, его нельзя удалить. Длительность заставки — по длине звука.',
|
||||||
bumperAddTemplate: 'Добавить блок',
|
bumperAddTemplate: 'Добавить блок',
|
||||||
bumperTemplateName: 'Название',
|
bumperTemplateName: 'Название',
|
||||||
|
bumperVariants: 'Подблоки (текст)',
|
||||||
|
bumperVariantsHint:
|
||||||
|
'Разный текст на одной музыке и оформлении блока. Правило показа — у каждого подблока своё.',
|
||||||
|
bumperAddVariant: 'Добавить текст',
|
||||||
|
bumperVariantName: 'Название',
|
||||||
|
bumperTextKind: 'Режим текста',
|
||||||
|
bumperKindNowNext: 'Сейчас / Далее',
|
||||||
|
bumperKindFree: 'Свободный текст',
|
||||||
|
bumperLine1: 'Строка 1',
|
||||||
|
bumperLine2: 'Строка 2',
|
||||||
|
bumperTrigger: 'Показывать',
|
||||||
|
bumperTriggerOnShowChange: 'При смене шоу',
|
||||||
|
bumperTriggerBetweenEpisodes: 'Между сериями',
|
||||||
|
bumperTriggerBoth: 'Оба',
|
||||||
bumperDefault: 'по умолчанию',
|
bumperDefault: 'по умолчанию',
|
||||||
bumperSeconds: 'с',
|
bumperSeconds: 'с',
|
||||||
bumperDefaultDuration: '≈8 с (джингл)',
|
bumperDefaultDuration: '≈8 с (джингл)',
|
||||||
@@ -526,6 +540,20 @@ const resources = {
|
|||||||
'Each block has its own sound and style. The first block is the default and cannot be removed. Bumper length follows the sound length.',
|
'Each block has its own sound and style. The first block is the default and cannot be removed. Bumper length follows the sound length.',
|
||||||
bumperAddTemplate: 'Add block',
|
bumperAddTemplate: 'Add block',
|
||||||
bumperTemplateName: 'Name',
|
bumperTemplateName: 'Name',
|
||||||
|
bumperVariants: 'Sub-blocks (text)',
|
||||||
|
bumperVariantsHint:
|
||||||
|
'Different text over the same music and style. Each sub-block has its own show rule.',
|
||||||
|
bumperAddVariant: 'Add text',
|
||||||
|
bumperVariantName: 'Name',
|
||||||
|
bumperTextKind: 'Text mode',
|
||||||
|
bumperKindNowNext: 'Now / Next',
|
||||||
|
bumperKindFree: 'Free text',
|
||||||
|
bumperLine1: 'Line 1',
|
||||||
|
bumperLine2: 'Line 2',
|
||||||
|
bumperTrigger: 'Show on',
|
||||||
|
bumperTriggerOnShowChange: 'Show change',
|
||||||
|
bumperTriggerBetweenEpisodes: 'Between episodes',
|
||||||
|
bumperTriggerBoth: 'Both',
|
||||||
bumperDefault: 'default',
|
bumperDefault: 'default',
|
||||||
bumperSeconds: 's',
|
bumperSeconds: 's',
|
||||||
bumperDefaultDuration: '≈8 s (jingle)',
|
bumperDefaultDuration: '≈8 s (jingle)',
|
||||||
|
|||||||
Reference in New Issue
Block a user