Browse Source

add custom index to collection items

pull/64/head
Jason Dove 5 years ago
parent
commit
533206d2e1
  1. 5
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  2. 100
      ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs
  3. 1
      ErsatzTV.Core/Domain/Collection/CollectionItem.cs
  4. 3
      ErsatzTV.Core/Domain/PlaybackOrder.cs
  5. 1
      ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs
  6. 38
      ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs
  7. 31
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  8. 11
      ErsatzTV.Infrastructure/Data/Configurations/Collection/CollectionItemConfiguration.cs
  9. 8
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  10. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  11. 1626
      ErsatzTV.Infrastructure/Migrations/20210312113202_Add_CollectionItem_CustomIndex.Designer.cs
  12. 19
      ErsatzTV.Infrastructure/Migrations/20210312113202_Add_CollectionItem_CustomIndex.cs
  13. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

5
ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs

@ -17,10 +17,13 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<Collection> Add(Collection collection) => throw new NotSupportedException(); public Task<Collection> Add(Collection collection) => throw new NotSupportedException();
public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException(); public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException();
public Task<bool> AddMediaItems(int collectionId, List<int> mediaItemIds) => throw new NotSupportedException(); public Task<bool> AddMediaItems(int collectionId, List<int> mediaItemIds) => throw new NotSupportedException();
public Task<Option<Collection>> Get(int id) => throw new NotSupportedException(); public Task<Option<Collection>> Get(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException(); public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException(); public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id) =>
throw new NotSupportedException();
public Task<List<Collection>> GetAll() => throw new NotSupportedException(); public Task<List<Collection>> GetAll() => throw new NotSupportedException();
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask(); public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException(); Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();

100
ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs

@ -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();
}
}

1
ErsatzTV.Core/Domain/Collection/CollectionItem.cs

@ -6,5 +6,6 @@
public Collection Collection { get; set; } public Collection Collection { get; set; }
public int MediaItemId { get; set; } public int MediaItemId { get; set; }
public MediaItem MediaItem { get; set; } public MediaItem MediaItem { get; set; }
public int? CustomIndex { get; set; }
} }
} }

3
ErsatzTV.Core/Domain/PlaybackOrder.cs

@ -4,6 +4,7 @@
{ {
Chronological = 1, Chronological = 1,
Random = 2, Random = 2,
Shuffle = 3 Shuffle = 3,
Custom = 4
} }
} }

1
ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs

@ -13,6 +13,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<Collection>> Get(int id); Task<Option<Collection>> Get(int id);
Task<Option<Collection>> GetCollectionWithItems(int id); Task<Option<Collection>> GetCollectionWithItems(int id);
Task<Option<Collection>> GetCollectionWithItemsUntracked(int id); Task<Option<Collection>> GetCollectionWithItemsUntracked(int id);
Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id);
Task<List<Collection>> GetAll(); Task<List<Collection>> GetAll();
Task<Option<List<MediaItem>>> GetItems(int id); Task<Option<List<MediaItem>>> GetItems(int id);
Task<bool> Update(Collection collection); Task<bool> Update(Collection collection);

38
ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs

@ -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;
}
}

31
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -118,8 +118,13 @@ namespace ErsatzTV.Core.Scheduling
} }
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList(); var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
Map<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators = var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
MapExtensions.Map(collectionMediaItems, (c, i) => GetMediaCollectionEnumerator(playout, c, i)); foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
{
IMediaCollectionEnumerator enumerator =
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems);
collectionEnumerators.Add(collectionKey, enumerator);
}
// find start anchor // find start anchor
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems); PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems);
@ -357,7 +362,7 @@ namespace ErsatzTV.Core.Scheduling
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors( private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
Playout playout, Playout playout,
Map<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators) Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
{ {
var result = new List<PlayoutProgramScheduleAnchor>(); var result = new List<PlayoutProgramScheduleAnchor>();
@ -396,7 +401,7 @@ namespace ErsatzTV.Core.Scheduling
return result; return result;
} }
private static IMediaCollectionEnumerator GetMediaCollectionEnumerator( private async Task<IMediaCollectionEnumerator> GetMediaCollectionEnumerator(
Playout playout, Playout playout,
CollectionKey collectionKey, CollectionKey collectionKey,
List<MediaItem> mediaItems) List<MediaItem> mediaItems)
@ -420,6 +425,24 @@ namespace ErsatzTV.Core.Scheduling
return new RandomizedMediaCollectionEnumerator(mediaItems, state); return new RandomizedMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.Shuffle: case PlaybackOrder.Shuffle:
return new ShuffledMediaCollectionEnumerator(mediaItems, state); return new ShuffledMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.Custom:
Option<Collection> collectionWithItems =
await _mediaCollectionRepository.GetCollectionWithCollectionItemsUntracked(
collectionKey.CollectionId ?? 0);
switch (collectionKey.CollectionType)
{
case ProgramScheduleItemCollectionType.Collection when collectionWithItems.IsSome:
return new CustomOrderCollectionEnumerator(
collectionWithItems.ValueUnsafe(),
mediaItems,
state);
default:
_logger.LogError(
"Invalid enumerator state for custom playback order; falling back to randomized content");
return new RandomizedMediaCollectionEnumerator(mediaItems, state);
}
;
default: default:
// TODO: handle this error case differently? // TODO: handle this error case differently?
return new RandomizedMediaCollectionEnumerator(mediaItems, state); return new RandomizedMediaCollectionEnumerator(mediaItems, state);

11
ErsatzTV.Infrastructure/Data/Configurations/Collection/CollectionItemConfiguration.cs

@ -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");
}
}

8
ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs

@ -120,6 +120,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) =>
_dbContext.Collections _dbContext.Collections
.AsNoTracking() .AsNoTracking()
.Include(c => c.CollectionItems)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.ThenInclude(i => i.LibraryPath) .ThenInclude(i => i.LibraryPath)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
@ -145,6 +146,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.SingleOrDefaultAsync(c => c.Id == id) .SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional); .Map(Optional);
public Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id) =>
_dbContext.Collections
.Include(c => c.CollectionItems)
.OrderBy(c => c.Id)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<List<Collection>> GetAll() => public Task<List<Collection>> GetAll() =>
_dbContext.Collections.ToListAsync(); _dbContext.Collections.ToListAsync();

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -34,6 +34,7 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<EpisodeMetadata> EpisodeMetadata { get; set; } public DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
public DbSet<PlexMovie> PlexMovies { get; set; } public DbSet<PlexMovie> PlexMovies { get; set; }
public DbSet<Collection> Collections { get; set; } public DbSet<Collection> Collections { get; set; }
public DbSet<CollectionItem> CollectionItems { get; set; }
public DbSet<ProgramSchedule> ProgramSchedules { get; set; } public DbSet<ProgramSchedule> ProgramSchedules { get; set; }
public DbSet<Playout> Playouts { get; set; } public DbSet<Playout> Playouts { get; set; }
public DbSet<PlayoutItem> PlayoutItems { get; set; } public DbSet<PlayoutItem> PlayoutItems { get; set; }

1626
ErsatzTV.Infrastructure/Migrations/20210312113202_Add_CollectionItem_CustomIndex.Designer.cs generated

File diff suppressed because it is too large Load Diff

19
ErsatzTV.Infrastructure/Migrations/20210312113202_Add_CollectionItem_CustomIndex.cs

@ -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");
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -125,6 +125,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("MediaItemId") b.Property<int>("MediaItemId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int?>("CustomIndex")
.HasColumnType("INTEGER");
b.HasKey("CollectionId", "MediaItemId"); b.HasKey("CollectionId", "MediaItemId");
b.HasIndex("MediaItemId"); b.HasIndex("MediaItemId");

Loading…
Cancel
Save