Add TV bumpers functionality: introduce configuration options for bumpers in .env.example and appsettings.json, enhance ChannelEndpoints to manage jingles and bumper assets, and update Channel and ScheduleEntry models to support bumper logic. Implement validation for bumper settings and integrate bumper handling in scheduling logic.
This commit is contained in:
@@ -2,7 +2,10 @@ using LiteCqrs;
|
||||
using TeleWave.Api.Common;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.AddChannelAd;
|
||||
using TeleWave.Application.Broadcast.AddChannelJingle;
|
||||
using TeleWave.Application.Broadcast.AddChannelShow;
|
||||
using TeleWave.Application.Broadcast.BumperBackground;
|
||||
using TeleWave.Application.Broadcast.BumperMusic;
|
||||
using TeleWave.Application.Broadcast.CreateChannel;
|
||||
using TeleWave.Application.Broadcast.CreateOverride;
|
||||
using TeleWave.Application.Broadcast.DeleteOverride;
|
||||
@@ -11,9 +14,11 @@ using TeleWave.Application.Broadcast.GetSchedule;
|
||||
using TeleWave.Application.Broadcast.ListChannels;
|
||||
using TeleWave.Application.Broadcast.RegenerateSchedule;
|
||||
using TeleWave.Application.Broadcast.RemoveChannelAd;
|
||||
using TeleWave.Application.Broadcast.RemoveChannelJingle;
|
||||
using TeleWave.Application.Broadcast.RemoveChannelShow;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelSettings;
|
||||
using TeleWave.Application.Broadcast.UpdateChannelShow;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
|
||||
@@ -49,6 +54,26 @@ public static class ChannelEndpoints
|
||||
.MapDelete("/{id:guid}/ads/{channelAdId:guid}", RemoveAd)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
admin
|
||||
.MapPost("/{id:guid}/jingles", AddJingle)
|
||||
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
||||
admin
|
||||
.MapDelete("/{id:guid}/jingles/{channelJingleId:guid}", RemoveJingle)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
admin
|
||||
.MapPut("/{id:guid}/bumper/background", UploadBackground)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
admin
|
||||
.MapDelete("/{id:guid}/bumper/background", ClearBackground)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
admin
|
||||
.MapPut("/{id:guid}/bumper/music", UploadMusic)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
admin
|
||||
.MapDelete("/{id:guid}/bumper/music", ClearMusic)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
admin
|
||||
.MapPost("/{id:guid}/overrides", CreateOverride)
|
||||
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
||||
@@ -109,6 +134,8 @@ public static class ChannelEndpoints
|
||||
body.IsEnabled,
|
||||
body.AdInsertion,
|
||||
body.AdsPerBreak,
|
||||
body.BumpersEnabled,
|
||||
body.Bumper,
|
||||
body.FillerAssetId
|
||||
),
|
||||
cancellationToken
|
||||
@@ -189,6 +216,110 @@ public static class ChannelEndpoints
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> AddJingle(
|
||||
Guid id,
|
||||
AddChannelJingleBody body,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(
|
||||
new AddChannelJingleCommand(id, body.MediaAssetId),
|
||||
cancellationToken
|
||||
);
|
||||
return result.IsSuccess
|
||||
? Results.Created($"/api/admin/channels/{id}", new CreatedIdResponse(result.Value))
|
||||
: result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> RemoveJingle(
|
||||
Guid id,
|
||||
Guid channelJingleId,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(
|
||||
new RemoveChannelJingleCommand(id, channelJingleId),
|
||||
cancellationToken
|
||||
);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> UploadBackground(
|
||||
Guid id,
|
||||
string fileName,
|
||||
HttpRequest request,
|
||||
IBumperTemplateStorage storage,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (ResolveBumperExtension(fileName, request, BumperFiles.BackgroundExtensions) is not { } ext)
|
||||
return ChannelErrors.InvalidBumperFile.ToProblem();
|
||||
|
||||
await storage.SaveBackgroundAsync(id, ext, request.Body, cancellationToken);
|
||||
|
||||
var result = await sender.Send(new SetBumperBackgroundCommand(id, ext), cancellationToken);
|
||||
if (!result.IsSuccess)
|
||||
storage.DeleteBackground(id);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> ClearBackground(
|
||||
Guid id,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new ClearBumperBackgroundCommand(id), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> UploadMusic(
|
||||
Guid id,
|
||||
string fileName,
|
||||
HttpRequest request,
|
||||
IBumperTemplateStorage storage,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (ResolveBumperExtension(fileName, request, BumperFiles.MusicExtensions) is not { } ext)
|
||||
return ChannelErrors.InvalidBumperFile.ToProblem();
|
||||
|
||||
await storage.SaveMusicAsync(id, ext, request.Body, cancellationToken);
|
||||
|
||||
var result = await sender.Send(new SetBumperMusicCommand(id, ext), cancellationToken);
|
||||
if (!result.IsSuccess)
|
||||
storage.DeleteMusic(id);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> ClearMusic(
|
||||
Guid id,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new ClearBumperMusicCommand(id), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
/// <summary>Проверяет расширение файла (по allowlist) и размер (Content-Length). Возвращает
|
||||
/// нормализованное расширение (с точкой, нижний регистр) или null при отказе.</summary>
|
||||
private static string? ResolveBumperExtension(
|
||||
string fileName,
|
||||
HttpRequest request,
|
||||
IReadOnlySet<string> allowedExtensions
|
||||
)
|
||||
{
|
||||
if (request.ContentLength is > BumperFiles.MaxBytes or 0 or null)
|
||||
return null;
|
||||
var ext = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return allowedExtensions.Contains(ext) ? ext : null;
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateOverride(
|
||||
Guid id,
|
||||
CreateOverrideBody body,
|
||||
@@ -258,6 +389,8 @@ public sealed record UpdateChannelSettingsBody(
|
||||
bool IsEnabled,
|
||||
AdInsertion AdInsertion,
|
||||
int AdsPerBreak,
|
||||
bool BumpersEnabled,
|
||||
BumperSettingsInput Bumper,
|
||||
Guid? FillerAssetId
|
||||
);
|
||||
|
||||
@@ -272,6 +405,42 @@ public sealed record UpdateChannelShowBody(
|
||||
|
||||
public sealed record AddChannelAdBody(Guid MediaAssetId);
|
||||
|
||||
public sealed record AddChannelJingleBody(Guid MediaAssetId);
|
||||
|
||||
/// <summary>Ограничения на загружаемые файлы заставки (фон/музыка).</summary>
|
||||
internal static class BumperFiles
|
||||
{
|
||||
public const long MaxBytes = 200L * 1024 * 1024; // 200 МБ
|
||||
|
||||
public static readonly IReadOnlySet<string> BackgroundExtensions = new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
)
|
||||
{
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".webp",
|
||||
".bmp",
|
||||
".gif",
|
||||
".mp4",
|
||||
".mov",
|
||||
".mkv",
|
||||
".webm",
|
||||
};
|
||||
|
||||
public static readonly IReadOnlySet<string> MusicExtensions = new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
)
|
||||
{
|
||||
".mp3",
|
||||
".m4a",
|
||||
".aac",
|
||||
".ogg",
|
||||
".opus",
|
||||
".wav",
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record CreateOverrideBody(
|
||||
OverrideMode Mode,
|
||||
DateTimeOffset StartsAtUtc,
|
||||
|
||||
Reference in New Issue
Block a user