Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

162 lines
6.3 KiB

using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<ReplaceBlockItems, Either<BaseError, List<BlockItemViewModel>>>
{
public async Task<Either<BaseError, List<BlockItemViewModel>>> Handle(
ReplaceBlockItems request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Block> validation = await Validate(dbContext, request, cancellationToken);
return await validation.Apply(ps => Persist(dbContext, request, ps, cancellationToken));
}
private static async Task<List<BlockItemViewModel>> Persist(
TvContext dbContext,
ReplaceBlockItems request,
Block block,
CancellationToken cancellationToken)
{
block.Name = request.Name;
block.Minutes = request.Minutes;
block.StopScheduling = request.StopScheduling;
block.DateUpdated = DateTime.UtcNow;
dbContext.RemoveRange(block.Items);
block.Items = request.Items.Map(i => BuildItem(block, i.Index, i)).ToList();
await dbContext.SaveChangesAsync(cancellationToken);
// TODO: refresh any playouts that use this schedule
// foreach (Playout playout in programSchedule.Playouts)
// {
// await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh));
// }
return block.Items.Map(Mapper.ProjectToViewModel).ToList();
}
private static BlockItem BuildItem(Block block, int index, ReplaceBlockItem item) =>
new()
{
BlockId = block.Id,
Index = index,
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
SmartCollectionId = item.SmartCollectionId,
MediaItemId = item.MediaItemId,
PlaybackOrder = item.PlaybackOrder,
IncludeInProgramGuide = item.IncludeInProgramGuide,
DisableWatermarks = item.DisableWatermarks
};
private static Task<Validation<BaseError, Block>> Validate(
TvContext dbContext,
ReplaceBlockItems request,
CancellationToken cancellationToken) =>
BlockMustExist(dbContext, request.BlockId, cancellationToken)
.BindT(block => MinutesMustBeValid(request, block))
.BindT(block => BlockNameMustBeValid(dbContext, block, request))
.BindT(block => CollectionTypesMustBeValid(request, block));
private static Task<Validation<BaseError, Block>> BlockMustExist(
TvContext dbContext,
int blockId,
CancellationToken cancellationToken) =>
dbContext.Blocks
.Include(b => b.Items)
.SelectOneAsync(b => b.Id, b => b.Id == blockId, cancellationToken)
.Map(o => o.ToValidation<BaseError>("[BlockId] does not exist."));
private static Validation<BaseError, Block> MinutesMustBeValid(ReplaceBlockItems request, Block block) =>
Optional(block)
.Filter(_ => request.Minutes > 0 && request.Minutes % 5 == 0 && request.Minutes <= 24 * 60)
.ToValidation<BaseError>("Block duration must be between 5 minutes and 24 hours");
private static Validation<BaseError, Block> CollectionTypesMustBeValid(ReplaceBlockItems request, Block block) =>
request.Items.Map(item => CollectionTypeMustBeValid(item, block)).Sequence().Map(_ => block);
private static Validation<BaseError, Block> CollectionTypeMustBeValid(ReplaceBlockItem item, Block block)
{
switch (item.CollectionType)
{
case CollectionType.Collection:
if (item.CollectionId is null)
{
return BaseError.New("[Collection] is required for collection type 'Collection'");
}
break;
case CollectionType.TelevisionShow:
if (item.MediaItemId is null)
{
return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'");
}
break;
case CollectionType.TelevisionSeason:
if (item.MediaItemId is null)
{
return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'");
}
break;
case CollectionType.Artist:
if (item.MediaItemId is null)
{
return BaseError.New("[MediaItem] is required for collection type 'Artist'");
}
break;
case CollectionType.MultiCollection:
if (item.MultiCollectionId is null)
{
return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'");
}
break;
case CollectionType.SmartCollection:
if (item.SmartCollectionId is null)
{
return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'");
}
break;
case CollectionType.FakeCollection:
default:
return BaseError.New("[CollectionType] is invalid");
}
return block;
}
private static async Task<Validation<BaseError, Block>> BlockNameMustBeValid(
TvContext dbContext,
Block block,
ReplaceBlockItems request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Block name \"{request.Name}\" is invalid");
}
Option<Block> maybeExisting = await dbContext.Blocks
.AsNoTracking()
.FirstOrDefaultAsync(d =>
d.Id != request.BlockId && d.BlockGroupId == request.BlockGroupId && d.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A block named \"{request.Name}\" already exists in that block group")
: Success<BaseError, Block>(block);
}
}