Backend slice for the ChicoryTV playouts and collections screens.
PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
playout item's stored context by row id via a new
GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)
CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
when the collection's UseCustomPlaybackOrder is set
Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
139 lines
4.5 KiB
C#
139 lines
4.5 KiB
C#
using ErsatzTV.Application.MediaCollections;
|
|
using ErsatzTV.Core;
|
|
using ErsatzTV.Core.Api.LibraryBrowse;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Tests.Support;
|
|
using LanguageExt;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests.Application.MediaCollections;
|
|
|
|
[TestFixture]
|
|
public class GetCollectionItemsHandlerTests
|
|
{
|
|
private InMemoryTvContext _db = null!;
|
|
|
|
[SetUp]
|
|
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
|
|
|
|
[TearDown]
|
|
public async Task TearDown() => await _db.DisposeAsync();
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Return_NotFound_For_Missing_Collection()
|
|
{
|
|
var handler = new GetCollectionItemsHandler(_db.Factory);
|
|
|
|
Either<BaseError, PagedLibraryBrowseItemsResponseModel> result =
|
|
await handler.Handle(new GetCollectionItems(999, 0, 100), CancellationToken.None);
|
|
|
|
result.IsLeft.ShouldBeTrue();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Order_By_Title_When_Custom_Order_Disabled()
|
|
{
|
|
await SeedCollectionGraph(useCustomPlaybackOrder: false);
|
|
var handler = new GetCollectionItemsHandler(_db.Factory);
|
|
|
|
Either<BaseError, PagedLibraryBrowseItemsResponseModel> result =
|
|
await handler.Handle(new GetCollectionItems(10, 0, 100), CancellationToken.None);
|
|
|
|
PagedLibraryBrowseItemsResponseModel page = result.RightToSeq().Single();
|
|
page.TotalCount.ShouldBe(3);
|
|
page.Page.Select(i => i.Title).ShouldBe(["Alpha", "Beta", "Zeta"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Order_By_CustomIndex_With_Nulls_Last_When_Custom_Order_Enabled()
|
|
{
|
|
await SeedCollectionGraph(useCustomPlaybackOrder: true);
|
|
var handler = new GetCollectionItemsHandler(_db.Factory);
|
|
|
|
Either<BaseError, PagedLibraryBrowseItemsResponseModel> result =
|
|
await handler.Handle(new GetCollectionItems(10, 0, 100), CancellationToken.None);
|
|
|
|
PagedLibraryBrowseItemsResponseModel page = result.RightToSeq().Single();
|
|
|
|
// Zeta has CustomIndex 0, Alpha has CustomIndex 1, Beta has no CustomIndex (sorts last).
|
|
page.Page.Select(i => i.Title).ShouldBe(["Zeta", "Alpha", "Beta"]);
|
|
}
|
|
|
|
private async Task SeedCollectionGraph(bool useCustomPlaybackOrder)
|
|
{
|
|
await using TvContext context = _db.CreateContext();
|
|
|
|
var library = new LocalLibrary
|
|
{
|
|
Id = 1,
|
|
Name = "Library",
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
Paths = []
|
|
};
|
|
var path = new LibraryPath
|
|
{
|
|
Id = 1,
|
|
Path = "/media",
|
|
Library = library,
|
|
LibraryFolders = [],
|
|
MediaItems = []
|
|
};
|
|
library.Paths.Add(path);
|
|
|
|
Movie alpha = MakeMovie(101, path, "Alpha");
|
|
Movie beta = MakeMovie(102, path, "Beta");
|
|
Movie zeta = MakeMovie(103, path, "Zeta");
|
|
|
|
var collection = new Collection
|
|
{
|
|
Id = 10,
|
|
Name = "Manual",
|
|
UseCustomPlaybackOrder = useCustomPlaybackOrder,
|
|
MediaItems = [],
|
|
CollectionItems =
|
|
[
|
|
new CollectionItem { MediaItemId = 103, CustomIndex = 0 },
|
|
new CollectionItem { MediaItemId = 101, CustomIndex = 1 },
|
|
new CollectionItem { MediaItemId = 102, CustomIndex = null }
|
|
],
|
|
MultiCollections = [],
|
|
MultiCollectionItems = []
|
|
};
|
|
|
|
context.LocalLibraries.Add(library);
|
|
context.Movies.AddRange(alpha, beta, zeta);
|
|
context.Collections.Add(collection);
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
private static Movie MakeMovie(int id, LibraryPath path, string title) =>
|
|
new()
|
|
{
|
|
Id = id,
|
|
LibraryPath = path,
|
|
Collections = [],
|
|
CollectionItems = [],
|
|
TraktListItems = [],
|
|
MovieMetadata =
|
|
[
|
|
new MovieMetadata
|
|
{
|
|
Title = title,
|
|
SortTitle = title,
|
|
Artwork = [],
|
|
Genres = [],
|
|
Tags = [],
|
|
Studios = [],
|
|
Actors = [],
|
|
Guids = [],
|
|
Subtitles = [],
|
|
Directors = [],
|
|
Writers = []
|
|
}
|
|
],
|
|
MediaVersions = [new MediaVersion { Duration = TimeSpan.FromMinutes(30) }]
|
|
};
|
|
}
|