mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* add custom index to collection items * add custom collection order to ui * cleanuppull/65/head v0.0.16-prealpha
31 changed files with 3753 additions and 32 deletions
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaCards |
||||
{ |
||||
public record MediaCardViewModel(string Title, string Subtitle, string SortTitle, string Poster); |
||||
public record MediaCardViewModel(int MediaItemId, string Title, string Subtitle, string SortTitle, string Poster); |
||||
} |
||||
|
@ -1,8 +1,12 @@
@@ -1,8 +1,12 @@
|
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands |
||||
{ |
||||
public record UpdateCollection |
||||
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>> |
||||
{ |
||||
public Option<bool> UseCustomPlaybackOrder { get; set; } = None; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic; |
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands |
||||
{ |
||||
public record UpdateCollectionCustomOrder |
||||
( |
||||
int CollectionId, |
||||
List<MediaItemCustomOrder> MediaItemCustomOrders) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||
|
||||
public record MediaItemCustomOrder(int MediaItemId, int CustomIndex); |
||||
} |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Channels; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Application.Playouts.Commands; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands |
||||
{ |
||||
public class |
||||
UpdateCollectionCustomOrderHandler : MediatR.IRequestHandler<UpdateCollectionCustomOrder, |
||||
Either<BaseError, Unit>> |
||||
{ |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||
|
||||
public UpdateCollectionCustomOrderHandler( |
||||
IMediaCollectionRepository mediaCollectionRepository, |
||||
ChannelWriter<IBackgroundServiceRequest> channel) |
||||
{ |
||||
_mediaCollectionRepository = mediaCollectionRepository; |
||||
_channel = channel; |
||||
} |
||||
|
||||
public Task<Either<BaseError, Unit>> Handle( |
||||
UpdateCollectionCustomOrder request, |
||||
CancellationToken cancellationToken) => |
||||
Validate(request) |
||||
.MapT(c => ApplyUpdateRequest(c, request)) |
||||
.Bind(v => v.ToEitherAsync()); |
||||
|
||||
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollectionCustomOrder request) |
||||
{ |
||||
foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders) |
||||
{ |
||||
Option<CollectionItem> maybeCollectionItem = |
||||
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId); |
||||
|
||||
maybeCollectionItem.IfSome(ci => ci.CustomIndex = updateItem.CustomIndex); |
||||
} |
||||
|
||||
if (await _mediaCollectionRepository.Update(c)) |
||||
{ |
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection( |
||||
request.CollectionId)) |
||||
{ |
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); |
||||
} |
||||
} |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private Task<Validation<BaseError, Collection>> Validate(UpdateCollectionCustomOrder request) => |
||||
CollectionMustExist(request); |
||||
|
||||
private Task<Validation<BaseError, Collection>> CollectionMustExist( |
||||
UpdateCollectionCustomOrder request) => |
||||
_mediaCollectionRepository.Get(request.CollectionId) |
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||
} |
||||
} |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using FluentAssertions; |
||||
using NUnit.Framework; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling |
||||
{ |
||||
public class CustomOrderContentTests |
||||
{ |
||||
[Test] |
||||
public void MediaItems_Should_Sort_By_CustomOrder() |
||||
{ |
||||
Collection collection = CreateCollection(10); |
||||
List<MediaItem> contents = Episodes(10); |
||||
var state = new CollectionEnumeratorState(); |
||||
|
||||
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); |
||||
|
||||
for (var i = 10; i >= 1; i--) |
||||
{ |
||||
customOrderContent.Current.IsSome.Should().BeTrue(); |
||||
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); |
||||
customOrderContent.MoveNext(); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void State_Index_Should_Increment() |
||||
{ |
||||
Collection collection = CreateCollection(10); |
||||
List<MediaItem> contents = Episodes(10); |
||||
var state = new CollectionEnumeratorState(); |
||||
|
||||
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); |
||||
|
||||
for (var i = 0; i < 10; i++) |
||||
{ |
||||
customOrderContent.State.Index.Should().Be(i % 10); |
||||
customOrderContent.MoveNext(); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public void State_Should_Impact_Iterator_Start() |
||||
{ |
||||
Collection collection = CreateCollection(10); |
||||
List<MediaItem> contents = Episodes(10); |
||||
var state = new CollectionEnumeratorState { Index = 5 }; |
||||
|
||||
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); |
||||
|
||||
for (var i = 5; i >= 1; i--) |
||||
{ |
||||
customOrderContent.Current.IsSome.Should().BeTrue(); |
||||
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); |
||||
customOrderContent.State.Index.Should().Be(5 - i + 5); // 5 through 10
|
||||
customOrderContent.MoveNext(); |
||||
} |
||||
} |
||||
|
||||
private static Collection CreateCollection(int episodeCount) |
||||
{ |
||||
var collection = new Collection { CollectionItems = new List<CollectionItem>() }; |
||||
|
||||
for (var i = 1; i <= episodeCount; i++) |
||||
{ |
||||
collection.CollectionItems.Add( |
||||
new CollectionItem |
||||
{ |
||||
MediaItemId = i, |
||||
// reverse order
|
||||
CustomIndex = episodeCount - i |
||||
}); |
||||
} |
||||
|
||||
return collection; |
||||
} |
||||
|
||||
|
||||
private static List<MediaItem> Episodes(int count) => |
||||
Range(1, count).Map( |
||||
i => (MediaItem) new Episode |
||||
{ |
||||
Id = i, |
||||
EpisodeMetadata = new List<EpisodeMetadata> |
||||
{ |
||||
new() |
||||
{ |
||||
ReleaseDate = new DateTime(2020, 1, i) |
||||
} |
||||
} |
||||
}) |
||||
.Reverse() |
||||
.ToList(); |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using LanguageExt; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling |
||||
{ |
||||
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator |
||||
{ |
||||
private readonly IList<MediaItem> _sortedMediaItems; |
||||
|
||||
public CustomOrderCollectionEnumerator( |
||||
Collection collection, |
||||
List<MediaItem> mediaItems, |
||||
CollectionEnumeratorState state) |
||||
{ |
||||
// TODO: this will break if we allow shows and seasons
|
||||
_sortedMediaItems = collection.CollectionItems |
||||
.OrderBy(ci => ci.CustomIndex) |
||||
.Map(ci => mediaItems.First(mi => mi.Id == ci.MediaItemId)) |
||||
.ToList(); |
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed }; |
||||
while (State.Index < state.Index) |
||||
{ |
||||
MoveNext(); |
||||
} |
||||
} |
||||
|
||||
public CollectionEnumeratorState State { get; } |
||||
|
||||
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None; |
||||
|
||||
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; |
||||
} |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations |
||||
{ |
||||
public class CollectionItemConfiguration : IEntityTypeConfiguration<CollectionItem> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<CollectionItem> builder) => builder.ToTable("CollectionItem"); |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Add_CollectionItem_CustomIndex : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) => |
||||
migrationBuilder.AddColumn<int>( |
||||
"CustomIndex", |
||||
"CollectionItem", |
||||
"INTEGER", |
||||
nullable: true); |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) => |
||||
migrationBuilder.DropColumn( |
||||
"CustomIndex", |
||||
"CollectionItem"); |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Add_Collection_UseCustomPlaybackOrder : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) => |
||||
migrationBuilder.AddColumn<bool>( |
||||
"UseCustomPlaybackOrder", |
||||
"Collection", |
||||
"INTEGER", |
||||
nullable: false, |
||||
defaultValue: false); |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) => |
||||
migrationBuilder.DropColumn( |
||||
"UseCustomPlaybackOrder", |
||||
"Collection"); |
||||
} |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Application.MediaCollections.Commands; |
||||
using MediatR; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace ErsatzTV.Controllers |
||||
{ |
||||
[ApiController] |
||||
[ApiExplorerSettings(IgnoreApi = true)] |
||||
public class SortController : ControllerBase |
||||
{ |
||||
private readonly IMediator _mediator; |
||||
|
||||
public SortController(IMediator mediator) => _mediator = mediator; |
||||
|
||||
[HttpPost("media/collections/{collectionId}/items")] |
||||
public Task SortCollectionItems( |
||||
int collectionId, |
||||
[FromForm] |
||||
SortedMediaItemIds sortedMediaItemIds) |
||||
{ |
||||
var ids = sortedMediaItemIds.Item.Map(int.Parse).ToList(); |
||||
|
||||
var request = new UpdateCollectionCustomOrder( |
||||
collectionId, |
||||
ids.Map(i => new MediaItemCustomOrder(i, ids.IndexOf(i))).ToList()); |
||||
|
||||
return _mediator.Send(request); |
||||
} |
||||
} |
||||
|
||||
public record SortedMediaItemIds(List<string> Item); |
||||
} |
Loading…
Reference in new issue