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:
+12
@@ -0,0 +1,12 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials.ImportInterstitials;
|
||||
|
||||
/// <summary>
|
||||
/// Превращает готовые медиа-ассеты в ролики: по одному <c>Show(Kind = Interstitial)</c> на файл,
|
||||
/// имя — имя файла без расширения. Ассеты, уже привязанные к какому-нибудь шоу, пропускаются —
|
||||
/// так повторный импорт того же выделения не плодит дубли.
|
||||
/// </summary>
|
||||
public sealed record ImportInterstitialsCommand(IReadOnlyList<Guid> MediaAssetIds)
|
||||
: ICommand<Result<int>>;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials.ImportInterstitials;
|
||||
|
||||
public sealed class ImportInterstitialsCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<ImportInterstitialsCommand, Result<int>>
|
||||
{
|
||||
public async Task<Result<int>> Handle(
|
||||
ImportInterstitialsCommand command,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var ids = command.MediaAssetIds.Distinct().ToList();
|
||||
|
||||
var assets = await dbContext
|
||||
.MediaAssets.AsNoTracking()
|
||||
.Where(a => ids.Contains(a.Id))
|
||||
.Select(a => new { a.Id, a.OriginalFileName })
|
||||
.ToListAsync(cancellationToken);
|
||||
if (assets.Count == 0)
|
||||
return Result.Failure<int>(ShowErrors.AssetNotFound);
|
||||
|
||||
var alreadyUsed = await dbContext
|
||||
.Shows.SelectMany(s => s.Episodes)
|
||||
.Where(e => ids.Contains(e.MediaAssetId))
|
||||
.Select(e => e.MediaAssetId)
|
||||
.ToListAsync(cancellationToken);
|
||||
var used = alreadyUsed.ToHashSet();
|
||||
|
||||
var created = 0;
|
||||
foreach (var asset in assets.Where(a => !used.Contains(a.Id)))
|
||||
{
|
||||
var show = Show.Create(ClipName(asset.OriginalFileName), ShowKind.Interstitial);
|
||||
show.AddEpisode(asset.Id);
|
||||
dbContext.Shows.Add(show);
|
||||
created++;
|
||||
}
|
||||
|
||||
return Result.Success(created);
|
||||
}
|
||||
|
||||
/// <summary>Имя ролика — имя файла без расширения; пустое (файл вида «.mp4») заменяем самим файлом.</summary>
|
||||
private static string ClipName(string fileName)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(fileName);
|
||||
return string.IsNullOrWhiteSpace(name) ? fileName : name;
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials.ImportInterstitials;
|
||||
|
||||
public sealed class ImportInterstitialsCommandValidator
|
||||
: AbstractValidator<ImportInterstitialsCommand>
|
||||
{
|
||||
public ImportInterstitialsCommandValidator() =>
|
||||
RuleFor(x => x.MediaAssetIds).NotEmpty().Must(ids => ids.Count <= 500);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using TeleWave.Domain.Media;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials;
|
||||
|
||||
/// <summary>
|
||||
/// Ролик на экране «Ролики». Это то же <c>Show(Kind = Interstitial)</c>, но показывается по-другому:
|
||||
/// у ролика нет ни года, ни постера, ни серий — важна длительность, поэтому она приходит сразу,
|
||||
/// а не вторым запросом за медиа-ассетом.
|
||||
/// </summary>
|
||||
public sealed record InterstitialDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
Guid? MediaAssetId,
|
||||
MediaAssetStatus? AssetStatus,
|
||||
double? DurationSeconds,
|
||||
DateTimeOffset CreatedAt
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Рекламный блок — коллекция, целиком собранная из роликов. Отдельного типа под блок нет
|
||||
/// (см. 3.7): блок и есть коллекция, здесь она показывается со своей суммарной длительностью.
|
||||
/// </summary>
|
||||
public sealed record InterstitialBlockDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
int ItemCount,
|
||||
double DurationSeconds
|
||||
);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
using LiteCqrs;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials.ListInterstitialBlocks;
|
||||
|
||||
/// <summary>Коллекции, целиком собранные из роликов, — рекламные блоки экрана «Ролики».</summary>
|
||||
public sealed record ListInterstitialBlocksQuery : IQuery<IReadOnlyList<InterstitialBlockDto>>;
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials.ListInterstitialBlocks;
|
||||
|
||||
public sealed class ListInterstitialBlocksQueryHandler(IAppDbContext dbContext)
|
||||
: IQueryHandler<ListInterstitialBlocksQuery, IReadOnlyList<InterstitialBlockDto>>
|
||||
{
|
||||
public async Task<IReadOnlyList<InterstitialBlockDto>> Handle(
|
||||
ListInterstitialBlocksQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var collections = await dbContext
|
||||
.Collections.AsNoTracking()
|
||||
.Include(c => c.Items)
|
||||
.OrderBy(c => c.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// «Блок» — не отдельная сущность, а признак состава: коллекция целиком из роликов. Смешанные
|
||||
// коллекции (франшизы) остаются на своём экране и сюда не попадают.
|
||||
var showIds = collections.SelectMany(c => c.Items.Select(i => i.ShowId)).Distinct().ToList();
|
||||
var clips = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => showIds.Contains(s.Id) && s.Kind == ShowKind.Interstitial)
|
||||
.Select(s => new
|
||||
{
|
||||
s.Id,
|
||||
AssetIds = s.Episodes.Select(e => e.MediaAssetId).ToList(),
|
||||
})
|
||||
.ToDictionaryAsync(s => s.Id, s => s.AssetIds, cancellationToken);
|
||||
|
||||
var assetIds = clips.Values.SelectMany(ids => ids).Distinct().ToList();
|
||||
var durations = await dbContext
|
||||
.MediaAssets.AsNoTracking()
|
||||
.Where(a => assetIds.Contains(a.Id) && a.Duration != null)
|
||||
.Select(a => new { a.Id, a.Duration })
|
||||
.ToDictionaryAsync(a => a.Id, a => a.Duration!.Value.TotalSeconds, cancellationToken);
|
||||
|
||||
return collections
|
||||
.Where(c => c.Items.Count > 0 && c.Items.All(i => clips.ContainsKey(i.ShowId)))
|
||||
.Select(c => new InterstitialBlockDto(
|
||||
c.Id,
|
||||
c.Name,
|
||||
c.Items.Count,
|
||||
c.Items.Sum(i =>
|
||||
clips[i.ShowId]
|
||||
.Sum(assetId => durations.TryGetValue(assetId, out var d) ? d : 0)
|
||||
)
|
||||
))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
using LiteCqrs;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials.ListInterstitials;
|
||||
|
||||
public sealed record ListInterstitialsQuery : IQuery<IReadOnlyList<InterstitialDto>>;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Application.Library.Interstitials.ListInterstitials;
|
||||
|
||||
public sealed class ListInterstitialsQueryHandler(IAppDbContext dbContext)
|
||||
: IQueryHandler<ListInterstitialsQuery, IReadOnlyList<InterstitialDto>>
|
||||
{
|
||||
public async Task<IReadOnlyList<InterstitialDto>> Handle(
|
||||
ListInterstitialsQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var shows = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Include(s => s.Episodes)
|
||||
.Where(s => s.Kind == ShowKind.Interstitial)
|
||||
.OrderBy(s => s.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// У ролика ровно одна «серия» — берём её ассет, чтобы показать длительность и статус обработки.
|
||||
var assetIds = shows
|
||||
.Select(s => s.Episodes.OrderBy(e => e.Position).Select(e => e.MediaAssetId).FirstOrDefault())
|
||||
.Where(id => id != Guid.Empty)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var assets = await dbContext
|
||||
.MediaAssets.AsNoTracking()
|
||||
.Where(a => assetIds.Contains(a.Id))
|
||||
.Select(a => new
|
||||
{
|
||||
a.Id,
|
||||
a.Status,
|
||||
a.Duration,
|
||||
})
|
||||
.ToDictionaryAsync(a => a.Id, cancellationToken);
|
||||
|
||||
return shows
|
||||
.Select(s =>
|
||||
{
|
||||
var assetId = s
|
||||
.Episodes.OrderBy(e => e.Position)
|
||||
.Select(e => (Guid?)e.MediaAssetId)
|
||||
.FirstOrDefault();
|
||||
var asset =
|
||||
assetId is { } id && assets.TryGetValue(id, out var a) ? a : null;
|
||||
return new InterstitialDto(
|
||||
s.Id,
|
||||
s.Name,
|
||||
assetId,
|
||||
asset?.Status,
|
||||
asset?.Duration?.TotalSeconds,
|
||||
s.CreatedAt
|
||||
);
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public static class ShowErrors
|
||||
|
||||
public static readonly Error SingleAlreadyHasEpisode = Error.Conflict(
|
||||
"Shows.SingleAlreadyHasEpisode",
|
||||
"Полнометражка/разовый выпуск может содержать только одну серию."
|
||||
"Больше одной серии бывает только у сериала."
|
||||
);
|
||||
|
||||
public static readonly Error AssetNotFound = Error.NotFound(
|
||||
|
||||
Reference in New Issue
Block a user