Add interstitial endpoints and enhance media and template functionalities: implement MapInterstitialEndpoints in Program.cs, add preview functionality for media assets in MediaEndpoints, and introduce template preview capabilities in TemplateEndpoints. Remove obsolete bumper settings from Channel and related classes to streamline configuration.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Api.Common;
|
||||
using TeleWave.Application.Library.Interstitials;
|
||||
using TeleWave.Application.Library.Interstitials.ImportInterstitials;
|
||||
using TeleWave.Application.Library.Interstitials.ListInterstitialBlocks;
|
||||
using TeleWave.Application.Library.Interstitials.ListInterstitials;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
|
||||
namespace TeleWave.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Ролики-врезки: тот же <c>Show(Kind = Interstitial)</c>, но со своим экраном (см. 6.7). Правка
|
||||
/// названия и удаление идут через обычные эндпоинты шоу — здесь только то, чего у библиотеки нет:
|
||||
/// список с длительностями, блоки и импорт загруженных файлов.
|
||||
/// </summary>
|
||||
public static class InterstitialEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapInterstitialEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var admin = app.MapGroup("/api/admin/interstitials")
|
||||
.WithTags("Admin.Interstitials")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("", List).Produces<IReadOnlyList<InterstitialDto>>();
|
||||
admin.MapGet("/blocks", ListBlocks).Produces<IReadOnlyList<InterstitialBlockDto>>();
|
||||
admin.MapPost("/import", Import).Produces<ImportInterstitialsResponse>();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<IResult> List(ISender sender, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await sender.Send(new ListInterstitialsQuery(), cancellationToken);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListBlocks(
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new ListInterstitialBlocksQuery(), cancellationToken);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
private static async Task<IResult> Import(
|
||||
ImportInterstitialsBody body,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(
|
||||
new ImportInterstitialsCommand(body.MediaAssetIds),
|
||||
cancellationToken
|
||||
);
|
||||
return result.IsSuccess
|
||||
? Results.Ok(new ImportInterstitialsResponse(result.Value))
|
||||
: result.ToHttpResult();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ImportInterstitialsBody(IReadOnlyList<Guid> MediaAssetIds);
|
||||
|
||||
public sealed record ImportInterstitialsResponse(int Imported);
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using LiteCqrs;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Api.Common;
|
||||
@@ -16,6 +18,8 @@ namespace TeleWave.Api.Endpoints;
|
||||
|
||||
public static class MediaEndpoints
|
||||
{
|
||||
private static readonly Regex SegmentFileName = new(@"^seg\d{1,6}\.ts$", RegexOptions.Compiled);
|
||||
|
||||
public static IEndpointRouteBuilder MapMediaEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var admin = app.MapGroup("/api/admin/media")
|
||||
@@ -27,6 +31,11 @@ public static class MediaEndpoints
|
||||
admin.MapGet("/stats", Stats).Produces<MediaStatsDto>();
|
||||
admin.MapDelete("/{id:guid}", Delete).Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
// Просмотр обработанного ассета в админке (ролики, проверка серии). Публичная раздача идёт
|
||||
// по stream-куке, здесь роут под JWT — плейлист и сегменты грузит hls.js с Bearer.
|
||||
admin.MapGet("/{id:guid}/preview/index.m3u8", PreviewPlaylist);
|
||||
admin.MapGet("/{id:guid}/preview/{file}", PreviewSegment);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -135,6 +144,56 @@ public static class MediaEndpoints
|
||||
var result = await sender.Send(new DeleteMediaAssetCommand(id), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
/// <summary>Плейлист ассета: переписываем ffmpeg-index.m3u8, направляя сегменты на admin-роут.</summary>
|
||||
private static IResult PreviewPlaylist(Guid id, MediaPathResolver paths)
|
||||
{
|
||||
string indexPath;
|
||||
try
|
||||
{
|
||||
indexPath = paths.SegmentPath(id, "index.m3u8");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
if (!File.Exists(indexPath))
|
||||
return Results.NotFound();
|
||||
|
||||
var baseUrl = $"/api/admin/media/{id}/preview/";
|
||||
var sb = new StringBuilder();
|
||||
foreach (var line in File.ReadLines(indexPath))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
continue;
|
||||
// Директивы — как есть; строки-сегменты (абсолютный путь от ffmpeg) → admin-URL.
|
||||
sb.Append(trimmed.StartsWith('#') ? trimmed : baseUrl + Path.GetFileName(trimmed))
|
||||
.Append('\n');
|
||||
}
|
||||
|
||||
return Results.Text(sb.ToString(), "application/vnd.apple.mpegurl");
|
||||
}
|
||||
|
||||
private static IResult PreviewSegment(Guid id, string file, MediaPathResolver paths)
|
||||
{
|
||||
if (!SegmentFileName.IsMatch(file))
|
||||
return Results.NotFound();
|
||||
|
||||
string path;
|
||||
try
|
||||
{
|
||||
path = paths.SegmentPath(id, file);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
if (!File.Exists(path))
|
||||
return Results.NotFound();
|
||||
|
||||
return Results.File(path, "video/mp2t", enableRangeProcessing: true);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record UploadMediaResponse(Guid Id);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Api.Common;
|
||||
using TeleWave.Application.Programming.Planning.ApplyTemplate;
|
||||
using TeleWave.Application.Programming.Planning.Preview;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Programming.Templates.CreateSlot;
|
||||
using TeleWave.Application.Programming.Templates.DeleteSlot;
|
||||
@@ -34,6 +35,10 @@ public static class TemplateEndpoints
|
||||
admin
|
||||
.MapPost("/channels/{channelId:guid}/template/apply", ApplyTemplate)
|
||||
.Produces<ApplyResultDto>();
|
||||
// Предпросмотр — тот же генератор, но без записи и без продвижения курсоров.
|
||||
admin
|
||||
.MapGet("/channels/{channelId:guid}/template/preview", PreviewTemplate)
|
||||
.Produces<SchedulePreviewDto>();
|
||||
|
||||
admin
|
||||
.MapPost("/templates/{templateId:guid}/layers", CreateLayer)
|
||||
@@ -75,6 +80,20 @@ public static class TemplateEndpoints
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> PreviewTemplate(
|
||||
Guid channelId,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken,
|
||||
int days = 1
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(
|
||||
new PreviewScheduleQuery(channelId, days),
|
||||
cancellationToken
|
||||
);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateTemplate(
|
||||
Guid templateId,
|
||||
UpdateTemplateBody body,
|
||||
|
||||
@@ -125,6 +125,7 @@ app.MapAdminUserEndpoints();
|
||||
app.MapMediaEndpoints();
|
||||
app.MapShowEndpoints();
|
||||
app.MapGenreEndpoints();
|
||||
app.MapInterstitialEndpoints();
|
||||
app.MapCollectionEndpoints();
|
||||
app.MapGroupEndpoints();
|
||||
app.MapTemplateEndpoints();
|
||||
|
||||
Reference in New Issue
Block a user