Files
ersatztv/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs
T
ef9bba8a52 feat(70): accept per-source weight on the multi-collection API
Without this the weight is only reachable by editing the database, so the
enumerator has nothing to distribute by.

Weight is threaded through create and update (all four handler branches: add and
update, plain and smart) and defaults to 1, so it is optional on the wire and
/api/v1 stays additive under the freeze.

It is returned on the read path too, which is load-bearing rather than symmetry:
the update replaces the item list, so a client that GETs, edits a name, and PUTs
back would silently reset every weight to the default if the GET didn't carry it.

Weight edits ride the existing MultiCollection Version token, so If-Match/412
concurrency needs no new design.

Regenerated v1.json + v1.d.ts + endpoint-index via update-openapi.sh and
generate:api (never hand-edited). The spec picks up weight on both request and
response models and WeightedShuffle in the PlaybackOrder enum; weight is emitted
optional.

Refs #70

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 17:00:46 +00:00

110 lines
4.2 KiB
C#

using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections;
public class CreateMultiCollectionHandler :
IRequestHandler<CreateMultiCollection, Either<BaseError, MultiCollectionViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ISearchTargets _searchTargets;
public CreateMultiCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
{
_dbContextFactory = dbContextFactory;
_searchTargets = searchTargets;
}
public async Task<Either<BaseError, MultiCollectionViewModel>> Handle(
CreateMultiCollection request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistCollection(dbContext, c));
}
private async Task<MultiCollectionViewModel> PersistCollection(
TvContext dbContext,
MultiCollection multiCollection)
{
await dbContext.MultiCollections.AddAsync(multiCollection);
await dbContext.SaveChangesAsync();
_searchTargets.SearchTargetsChanged();
await dbContext.Entry(multiCollection)
.Collection(c => c.MultiCollectionItems)
.Query()
.Include(i => i.Collection)
.LoadAsync();
await dbContext.Entry(multiCollection)
.Collection(c => c.MultiCollectionSmartItems)
.Query()
.Include(i => i.SmartCollection)
.LoadAsync();
return ProjectToViewModel(multiCollection);
}
private static Task<Validation<BaseError, MultiCollection>> Validate(
TvContext dbContext,
CreateMultiCollection request) =>
ValidateName(dbContext, request).MapT(name => new MultiCollection
{
Name = name,
MultiCollectionItems = request.Items.Bind(i =>
{
if (i.CollectionId.HasValue)
{
return Some(
new MultiCollectionItem
{
CollectionId = i.CollectionId.Value,
ScheduleAsGroup = i.ScheduleAsGroup,
PlaybackOrder = i.PlaybackOrder,
Weight = i.Weight
});
}
return Option<MultiCollectionItem>.None;
})
.ToList(),
MultiCollectionSmartItems = request.Items.Bind(i =>
{
if (i.SmartCollectionId.HasValue)
{
return Some(
new MultiCollectionSmartItem
{
SmartCollectionId = i.SmartCollectionId.Value,
ScheduleAsGroup = i.ScheduleAsGroup,
PlaybackOrder = i.PlaybackOrder,
Weight = i.Weight
});
}
return Option<MultiCollectionSmartItem>.None;
})
.ToList()
});
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateMultiCollection createMultiCollection)
{
Validation<BaseError, string> result1 = createMultiCollection.NotEmpty(c => c.Name)
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name));
bool duplicateName = await dbContext.MultiCollections
.AnyAsync(c => c.Name == createMultiCollection.Name);
Validation<BaseError, Unit> result2 = duplicateName
? Fail<BaseError, Unit>("MultiCollection name must be unique")
: Success<BaseError, Unit>(Unit.Default);
return (result1, result2).Apply((_, _) => createMultiCollection.Name);
}
}