Add broadcast scheduling features: implement Show and Channel entities, enhance AppDbContext and DependencyInjection for broadcasting, and update API routing. Include migration for new database schema and update documentation for broadcast-related functionalities.

This commit is contained in:
Leonid Pershin
2026-07-24 08:57:08 +03:00
parent e15ecbdb29
commit 4fa9dae37f
86 changed files with 4094 additions and 15 deletions
@@ -0,0 +1,280 @@
using LiteCqrs;
using TeleWave.Api.Common;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Broadcast.AddChannelAd;
using TeleWave.Application.Broadcast.AddChannelShow;
using TeleWave.Application.Broadcast.CreateChannel;
using TeleWave.Application.Broadcast.CreateOverride;
using TeleWave.Application.Broadcast.DeleteOverride;
using TeleWave.Application.Broadcast.GetChannel;
using TeleWave.Application.Broadcast.GetSchedule;
using TeleWave.Application.Broadcast.ListChannels;
using TeleWave.Application.Broadcast.RegenerateSchedule;
using TeleWave.Application.Broadcast.RemoveChannelAd;
using TeleWave.Application.Broadcast.RemoveChannelShow;
using TeleWave.Application.Broadcast.UpdateChannelSettings;
using TeleWave.Application.Broadcast.UpdateChannelShow;
using TeleWave.Domain.Broadcast;
using TeleWave.Infrastructure.Identity;
namespace TeleWave.Api.Endpoints;
public static class ChannelEndpoints
{
public static IEndpointRouteBuilder MapChannelEndpoints(this IEndpointRouteBuilder app)
{
var admin = app.MapGroup("/api/admin/channels")
.WithTags("Admin.Channels")
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
admin.MapPost("", CreateChannel).Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin.MapGet("", ListChannels).Produces<IReadOnlyList<ChannelSummaryDto>>();
admin.MapGet("/{id:guid}", GetChannel).Produces<ChannelDto>();
admin.MapPut("/{id:guid}/settings", UpdateSettings).Produces(StatusCodes.Status204NoContent);
admin
.MapPost("/{id:guid}/shows", AddShow)
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin
.MapPut("/{id:guid}/shows/{channelShowId:guid}", UpdateShow)
.Produces(StatusCodes.Status204NoContent);
admin
.MapDelete("/{id:guid}/shows/{channelShowId:guid}", RemoveShow)
.Produces(StatusCodes.Status204NoContent);
admin
.MapPost("/{id:guid}/ads", AddAd)
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin
.MapDelete("/{id:guid}/ads/{channelAdId:guid}", RemoveAd)
.Produces(StatusCodes.Status204NoContent);
admin
.MapPost("/{id:guid}/overrides", CreateOverride)
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin
.MapDelete("/{id:guid}/overrides/{overrideId:guid}", DeleteOverride)
.Produces(StatusCodes.Status204NoContent);
admin.MapPost("/{id:guid}/regenerate", Regenerate).Produces(StatusCodes.Status204NoContent);
admin
.MapGet("/{id:guid}/schedule", GetSchedule)
.Produces<IReadOnlyList<ScheduleEntryDto>>();
return app;
}
private static async Task<IResult> CreateChannel(
CreateChannelCommand command,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(command, cancellationToken);
return result.IsSuccess
? Results.Created(
$"/api/admin/channels/{result.Value}",
new CreatedIdResponse(result.Value)
)
: result.ToHttpResult();
}
private static async Task<IResult> ListChannels(ISender sender, CancellationToken cancellationToken)
{
var result = await sender.Send(new ListChannelsQuery(), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetChannel(
Guid id,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new GetChannelQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> UpdateSettings(
Guid id,
UpdateChannelSettingsBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new UpdateChannelSettingsCommand(
id,
body.Name,
body.IsEnabled,
body.AdInsertion,
body.AdsPerBreak,
body.FillerAssetId
),
cancellationToken
);
return result.ToHttpResult();
}
private static async Task<IResult> AddShow(
Guid id,
AddChannelShowBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new AddChannelShowCommand(id, body.ShowId, body.Weight, body.BlockMode, body.BlockValue),
cancellationToken
);
return result.IsSuccess
? Results.Created($"/api/admin/channels/{id}", new CreatedIdResponse(result.Value))
: result.ToHttpResult();
}
private static async Task<IResult> UpdateShow(
Guid id,
Guid channelShowId,
UpdateChannelShowBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new UpdateChannelShowCommand(
id,
channelShowId,
body.Weight,
body.BlockMode,
body.BlockValue,
body.IsEnabled
),
cancellationToken
);
return result.ToHttpResult();
}
private static async Task<IResult> RemoveShow(
Guid id,
Guid channelShowId,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new RemoveChannelShowCommand(id, channelShowId), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> AddAd(
Guid id,
AddChannelAdBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new AddChannelAdCommand(id, body.MediaAssetId), cancellationToken);
return result.IsSuccess
? Results.Created($"/api/admin/channels/{id}", new CreatedIdResponse(result.Value))
: result.ToHttpResult();
}
private static async Task<IResult> RemoveAd(
Guid id,
Guid channelAdId,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new RemoveChannelAdCommand(id, channelAdId), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> CreateOverride(
Guid id,
CreateOverrideBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new CreateProgrammingOverrideCommand(
id,
body.Mode,
body.StartsAtUtc,
body.EndsAtUtc,
body.Shows
),
cancellationToken
);
return result.IsSuccess
? Results.Created($"/api/admin/channels/{id}", new CreatedIdResponse(result.Value))
: result.ToHttpResult();
}
private static async Task<IResult> DeleteOverride(
Guid id,
Guid overrideId,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(
new DeleteProgrammingOverrideCommand(id, overrideId),
cancellationToken
);
return result.ToHttpResult();
}
private static async Task<IResult> Regenerate(
Guid id,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new RegenerateChannelScheduleCommand(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> GetSchedule(
Guid id,
DateTimeOffset? from,
DateTimeOffset? to,
ISender sender,
CancellationToken cancellationToken
)
{
var fromUtc = from ?? DateTimeOffset.UtcNow;
var toUtc = to ?? fromUtc.AddDays(1);
var result = await sender.Send(
new GetChannelScheduleQuery(id, fromUtc, toUtc),
cancellationToken
);
return result.ToHttpResult();
}
}
public sealed record UpdateChannelSettingsBody(
string Name,
bool IsEnabled,
AdInsertion AdInsertion,
int AdsPerBreak,
Guid? FillerAssetId
);
public sealed record AddChannelShowBody(Guid ShowId, int Weight, BlockMode BlockMode, int BlockValue);
public sealed record UpdateChannelShowBody(
int Weight,
BlockMode BlockMode,
int BlockValue,
bool IsEnabled
);
public sealed record AddChannelAdBody(Guid MediaAssetId);
public sealed record CreateOverrideBody(
OverrideMode Mode,
DateTimeOffset StartsAtUtc,
DateTimeOffset EndsAtUtc,
IReadOnlyList<OverrideShowInput> Shows
);
@@ -0,0 +1,99 @@
using LiteCqrs;
using TeleWave.Api.Common;
using TeleWave.Application.Library;
using TeleWave.Application.Library.AddEpisode;
using TeleWave.Application.Library.CreateShow;
using TeleWave.Application.Library.DeleteShow;
using TeleWave.Application.Library.GetShow;
using TeleWave.Application.Library.ListShows;
using TeleWave.Application.Library.RemoveEpisode;
using TeleWave.Infrastructure.Identity;
namespace TeleWave.Api.Endpoints;
public static class ShowEndpoints
{
public static IEndpointRouteBuilder MapShowEndpoints(this IEndpointRouteBuilder app)
{
var admin = app.MapGroup("/api/admin/shows")
.WithTags("Admin.Shows")
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
admin.MapPost("", CreateShow).Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin.MapGet("", ListShows).Produces<IReadOnlyList<ShowSummaryDto>>();
admin.MapGet("/{id:guid}", GetShow).Produces<ShowDto>();
admin.MapDelete("/{id:guid}", DeleteShow).Produces(StatusCodes.Status204NoContent);
admin
.MapPost("/{id:guid}/episodes", AddEpisode)
.Produces<CreatedIdResponse>(StatusCodes.Status201Created);
admin
.MapDelete("/{id:guid}/episodes/{episodeId:guid}", RemoveEpisode)
.Produces(StatusCodes.Status204NoContent);
return app;
}
private static async Task<IResult> CreateShow(
CreateShowCommand command,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(command, cancellationToken);
return result.IsSuccess
? Results.Created($"/api/admin/shows/{result.Value}", new CreatedIdResponse(result.Value))
: result.ToHttpResult();
}
private static async Task<IResult> ListShows(ISender sender, CancellationToken cancellationToken)
{
var result = await sender.Send(new ListShowsQuery(), cancellationToken);
return Results.Ok(result);
}
private static async Task<IResult> GetShow(
Guid id,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new GetShowQuery(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> DeleteShow(
Guid id,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new DeleteShowCommand(id), cancellationToken);
return result.ToHttpResult();
}
private static async Task<IResult> AddEpisode(
Guid id,
AddEpisodeBody body,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new AddEpisodeCommand(id, body.MediaAssetId), cancellationToken);
return result.IsSuccess
? Results.Created($"/api/admin/shows/{id}", new CreatedIdResponse(result.Value))
: result.ToHttpResult();
}
private static async Task<IResult> RemoveEpisode(
Guid id,
Guid episodeId,
ISender sender,
CancellationToken cancellationToken
)
{
var result = await sender.Send(new RemoveEpisodeCommand(id, episodeId), cancellationToken);
return result.ToHttpResult();
}
}
public sealed record AddEpisodeBody(Guid MediaAssetId);