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:
@@ -0,0 +1,6 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.AddEpisode;
|
||||
|
||||
public sealed record AddEpisodeCommand(Guid ShowId, Guid MediaAssetId) : ICommand<Result<Guid>>;
|
||||
@@ -0,0 +1,35 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.AddEpisode;
|
||||
|
||||
public sealed class AddEpisodeCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<AddEpisodeCommand, Result<Guid>>
|
||||
{
|
||||
public async Task<Result<Guid>> Handle(
|
||||
AddEpisodeCommand command,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var show = await dbContext.Shows
|
||||
.Include(s => s.Episodes)
|
||||
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
|
||||
if (show is null)
|
||||
return Result.Failure<Guid>(ShowErrors.NotFound);
|
||||
|
||||
if (!show.CanAddEpisode)
|
||||
return Result.Failure<Guid>(ShowErrors.SingleAlreadyHasEpisode);
|
||||
|
||||
var assetExists = await dbContext.MediaAssets.AnyAsync(
|
||||
a => a.Id == command.MediaAssetId,
|
||||
cancellationToken
|
||||
);
|
||||
if (!assetExists)
|
||||
return Result.Failure<Guid>(ShowErrors.AssetNotFound);
|
||||
|
||||
var episode = show.AddEpisode(command.MediaAssetId);
|
||||
return Result.Success(episode.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Application.Library.CreateShow;
|
||||
|
||||
public sealed record CreateShowCommand(string Name, ShowKind Kind, string? Description)
|
||||
: ICommand<Result<Guid>>;
|
||||
@@ -0,0 +1,17 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Application.Library.CreateShow;
|
||||
|
||||
public sealed class CreateShowCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<CreateShowCommand, Result<Guid>>
|
||||
{
|
||||
public Task<Result<Guid>> Handle(CreateShowCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var show = Show.Create(command.Name, command.Kind, command.Description);
|
||||
dbContext.Shows.Add(show);
|
||||
return Task.FromResult(Result.Success(show.Id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace TeleWave.Application.Library.CreateShow;
|
||||
|
||||
public sealed class CreateShowCommandValidator : AbstractValidator<CreateShowCommand>
|
||||
{
|
||||
public CreateShowCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.Description).MaximumLength(2048);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.DeleteShow;
|
||||
|
||||
public sealed record DeleteShowCommand(Guid ShowId) : ICommand<Result>;
|
||||
@@ -0,0 +1,24 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.DeleteShow;
|
||||
|
||||
public sealed class DeleteShowCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<DeleteShowCommand, Result>
|
||||
{
|
||||
public async Task<Result> Handle(DeleteShowCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var show = await dbContext.Shows.FirstOrDefaultAsync(
|
||||
s => s.Id == command.ShowId,
|
||||
cancellationToken
|
||||
);
|
||||
if (show is null)
|
||||
return Result.Failure(ShowErrors.NotFound);
|
||||
|
||||
// TODO(этап 2+): запретить удаление шоу, пока оно привязано к каналу или будущему расписанию.
|
||||
dbContext.Shows.Remove(show);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.GetShow;
|
||||
|
||||
public sealed record GetShowQuery(Guid Id) : IQuery<Result<ShowDto>>;
|
||||
@@ -0,0 +1,51 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.GetShow;
|
||||
|
||||
public sealed class GetShowQueryHandler(IAppDbContext dbContext)
|
||||
: IQueryHandler<GetShowQuery, Result<ShowDto>>
|
||||
{
|
||||
public async Task<Result<ShowDto>> Handle(GetShowQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var show = await dbContext.Shows.AsNoTracking()
|
||||
.Include(s => s.Episodes)
|
||||
.FirstOrDefaultAsync(s => s.Id == query.Id, cancellationToken);
|
||||
if (show is null)
|
||||
return Result.Failure<ShowDto>(ShowErrors.NotFound);
|
||||
|
||||
var episodes = show.Episodes.OrderBy(e => e.Position).ToList();
|
||||
var assetIds = episodes.Select(e => e.MediaAssetId).ToList();
|
||||
var assets = await dbContext.MediaAssets.AsNoTracking()
|
||||
.Where(a => assetIds.Contains(a.Id))
|
||||
.Select(a => new
|
||||
{
|
||||
a.Id,
|
||||
a.OriginalFileName,
|
||||
a.Status,
|
||||
a.Duration,
|
||||
})
|
||||
.ToDictionaryAsync(a => a.Id, cancellationToken);
|
||||
|
||||
var episodeDtos = episodes
|
||||
.Select(e =>
|
||||
{
|
||||
assets.TryGetValue(e.MediaAssetId, out var asset);
|
||||
return new EpisodeDto(
|
||||
e.Id,
|
||||
e.MediaAssetId,
|
||||
e.Position,
|
||||
asset?.OriginalFileName,
|
||||
asset?.Status,
|
||||
asset?.Duration?.TotalSeconds
|
||||
);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Result.Success(
|
||||
new ShowDto(show.Id, show.Name, show.Kind, show.Description, episodeDtos)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using LiteCqrs;
|
||||
|
||||
namespace TeleWave.Application.Library.ListShows;
|
||||
|
||||
public sealed record ListShowsQuery : IQuery<IReadOnlyList<ShowSummaryDto>>;
|
||||
@@ -0,0 +1,20 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Application.Library.ListShows;
|
||||
|
||||
public sealed class ListShowsQueryHandler(IAppDbContext dbContext)
|
||||
: IQueryHandler<ListShowsQuery, IReadOnlyList<ShowSummaryDto>>
|
||||
{
|
||||
public async Task<IReadOnlyList<ShowSummaryDto>> Handle(
|
||||
ListShowsQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
return await dbContext.Shows.AsNoTracking()
|
||||
.OrderBy(s => s.Name)
|
||||
.Select(s => new ShowSummaryDto(s.Id, s.Name, s.Kind, s.Episodes.Count, s.CreatedAt))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.RemoveEpisode;
|
||||
|
||||
public sealed record RemoveEpisodeCommand(Guid ShowId, Guid EpisodeId) : ICommand<Result>;
|
||||
@@ -0,0 +1,26 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library.RemoveEpisode;
|
||||
|
||||
public sealed class RemoveEpisodeCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<RemoveEpisodeCommand, Result>
|
||||
{
|
||||
public async Task<Result> Handle(
|
||||
RemoveEpisodeCommand command,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var show = await dbContext.Shows
|
||||
.Include(s => s.Episodes)
|
||||
.FirstOrDefaultAsync(s => s.Id == command.ShowId, cancellationToken);
|
||||
if (show is null)
|
||||
return Result.Failure(ShowErrors.NotFound);
|
||||
|
||||
return show.RemoveEpisode(command.EpisodeId)
|
||||
? Result.Success()
|
||||
: Result.Failure(ShowErrors.EpisodeNotFound);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
|
||||
namespace TeleWave.Application.Library;
|
||||
|
||||
public sealed record ShowSummaryDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
ShowKind Kind,
|
||||
int EpisodeCount,
|
||||
DateTimeOffset CreatedAt
|
||||
);
|
||||
|
||||
public sealed record EpisodeDto(
|
||||
Guid Id,
|
||||
Guid MediaAssetId,
|
||||
int Position,
|
||||
string? AssetName,
|
||||
MediaAssetStatus? AssetStatus,
|
||||
double? DurationSeconds
|
||||
);
|
||||
|
||||
public sealed record ShowDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
ShowKind Kind,
|
||||
string? Description,
|
||||
IReadOnlyList<EpisodeDto> Episodes
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Library;
|
||||
|
||||
public static class ShowErrors
|
||||
{
|
||||
public static readonly Error NotFound = Error.NotFound("Shows.NotFound", "Шоу не найдено.");
|
||||
|
||||
public static readonly Error EpisodeNotFound = Error.NotFound(
|
||||
"Shows.EpisodeNotFound",
|
||||
"Серия не найдена."
|
||||
);
|
||||
|
||||
public static readonly Error SingleAlreadyHasEpisode = Error.Conflict(
|
||||
"Shows.SingleAlreadyHasEpisode",
|
||||
"Полнометражка/разовый выпуск может содержать только одну серию."
|
||||
);
|
||||
|
||||
public static readonly Error AssetNotFound = Error.NotFound(
|
||||
"Shows.AssetNotFound",
|
||||
"Медиа-ассет для серии не найден."
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user