add multi collections (#315)

* start to add multi-collections

* create multi collection with no items

* edit multi collections

* fix plex credentials threading issue

* add playback order to multi collection items

* group episodes outside of shuffled enumerator

* move playback order onto each schedule item

* fix multi collection grouping

* update changelog
This commit is contained in:
Jason Dove
2021-07-17 21:45:41 -05:00
committed by GitHub
parent 4000c6bc0a
commit 632753ea93
87 changed files with 10828 additions and 197 deletions
@@ -9,5 +9,7 @@ namespace ErsatzTV.Core.Domain
public bool UseCustomPlaybackOrder { get; set; }
public List<MediaItem> MediaItems { get; set; }
public List<CollectionItem> CollectionItems { get; set; }
public List<MultiCollection> MultiCollections { get; set; }
public List<MultiCollectionItem> MultiCollectionItems { get; set; }
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class MultiCollection
{
public int Id { get; set; }
public string Name { get; set; }
public List<Collection> Collections { get; set; }
public List<MultiCollectionItem> MultiCollectionItems { get; set; }
}
}
@@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Domain
{
public class MultiCollectionItem
{
public int MultiCollectionId { get; set; }
public MultiCollection MultiCollection { get; set; }
public int CollectionId { get; set; }
public Collection Collection { get; set; }
public bool ScheduleAsGroup { get; set; }
public PlaybackOrder PlaybackOrder { get; set; }
}
}
+1
View File
@@ -17,6 +17,7 @@
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");
public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size");
public static ConfigElementKey MultiCollectionsPageSize => new("pages.multi_collections.page_size");
public static ConfigElementKey SchedulesPageSize => new("pages.schedules.page_size");
public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size");
public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size");
@@ -10,6 +10,8 @@
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public Collection Collection { get; set; }
public int? MultiCollectionId { get; set; }
public MultiCollection MultiCollection { get; set; }
public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
public CollectionEnumeratorState EnumeratorState { get; set; }
-1
View File
@@ -6,7 +6,6 @@ namespace ErsatzTV.Core.Domain
{
public int Id { get; set; }
public string Name { get; set; }
public PlaybackOrder MediaCollectionPlaybackOrder { get; set; }
public bool KeepMultiPartEpisodesTogether { get; set; }
public bool TreatCollectionsAsShows { get; set; }
public List<ProgramScheduleItem> Items { get; set; }
@@ -16,5 +16,8 @@ namespace ErsatzTV.Core.Domain
public Collection Collection { get; set; }
public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
public int? MultiCollectionId { get; set; }
public MultiCollection MultiCollection { get; set; }
public PlaybackOrder PlaybackOrder { get; set; }
}
}
@@ -5,6 +5,7 @@
Collection = 0,
TelevisionShow = 1,
TelevisionSeason = 2,
Artist = 3
Artist = 3,
MultiCollection = 4
}
}
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Repositories
@@ -9,7 +10,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{
Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id);
Task<List<MediaItem>> GetItems(int id);
Task<List<MediaItem>> GetMultiCollectionItems(int id);
Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id);
Task<List<int>> PlayoutIdsUsingCollection(int collectionId);
Task<List<int>> PlayoutIdsUsingMultiCollection(int multiCollectionId);
Task<bool> IsCustomPlaybackOrder(int collectionId);
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling
{
public record CollectionWithItems(
int CollectionId,
List<MediaItem> MediaItems,
bool ScheduleAsGroup,
PlaybackOrder PlaybackOrder,
bool UseCustomOrder);
}
+33 -2
View File
@@ -1,9 +1,40 @@
using System.Collections.Generic;
using System.Diagnostics;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Scheduling
{
[DebuggerDisplay("{First}")]
public record GroupedMediaItem(MediaItem First, List<MediaItem> Additional);
[DebuggerDisplay("{" + nameof(First) + "}")]
public class GroupedMediaItem
{
public GroupedMediaItem()
{
}
public GroupedMediaItem(MediaItem first, List<MediaItem> additional)
{
First = first;
Additional = additional ?? new List<MediaItem>();
}
public MediaItem First { get; set; }
public List<MediaItem> Additional { get; set; }
public static IList<MediaItem> FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount)
{
var result = new MediaItem[mediaItemCount];
var i = 0;
foreach (GroupedMediaItem group in copy)
{
result[i++] = group.First;
foreach (MediaItem additional in Optional(group.Additional).Flatten())
{
result[i++] = additional;
}
}
return result;
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Scheduling
{
public class MultiCollectionGroup : GroupedMediaItem
{
public MultiCollectionGroup(CollectionWithItems collectionWithItems)
{
if (collectionWithItems.UseCustomOrder)
{
if (collectionWithItems.MediaItems.Count > 0)
{
First = collectionWithItems.MediaItems.Head();
Additional = collectionWithItems.MediaItems.Tail().ToList();
}
else
{
throw new InvalidOperationException("Collection has no items");
}
}
else
{
switch (collectionWithItems.PlaybackOrder)
{
case PlaybackOrder.Chronological:
var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new ChronologicalMediaComparer())
.ToList();
First = sortedItems.Head();
Additional = sortedItems.Tail().ToList();
break;
default:
throw new NotSupportedException(
$"Unsupported MultiCollection PlaybackOrder: {collectionWithItems.PlaybackOrder}");
}
}
}
}
}
@@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Scheduling
{
public class MultiCollectionGrouper
{
public static List<GroupedMediaItem> GroupMediaItems(IList<CollectionWithItems> collections)
{
var result = new List<GroupedMediaItem>();
foreach (CollectionWithItems collection in collections)
{
if (collection.ScheduleAsGroup)
{
result.Add(new MultiCollectionGroup(collection));
}
else
{
result.AddRange(collection.MediaItems.Map(i => new GroupedMediaItem { First = i }));
}
}
return result;
}
}
}
@@ -77,7 +77,7 @@ namespace ErsatzTV.Core.Scheduling
// add to current group
List<MediaItem> additional = group.Additional ?? new List<MediaItem>();
additional.Add(episode);
group = group with { Additional = additional };
group = new GroupedMediaItem(group.First, additional);
}
else
{
@@ -152,22 +152,6 @@ namespace ErsatzTV.Core.Scheduling
return None;
}
public static IList<MediaItem> FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount)
{
var result = new MediaItem[mediaItemCount];
var i = 0;
foreach (GroupedMediaItem group in copy)
{
result[i++] = group.First;
foreach (MediaItem additional in Optional(group.Additional).Flatten())
{
result[i++] = additional;
}
}
return result;
}
private static bool TryParseRoman(string input, out int output)
{
switch (input?.ToLowerInvariant())
+43 -8
View File
@@ -82,6 +82,11 @@ namespace ErsatzTV.Core.Scheduling
List<MusicVideo> artistItems =
await _artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, artistItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.MultiCollection:
List<MediaItem> multiCollectionItems =
await _mediaCollectionRepository.GetMultiCollectionItems(
collectionKey.MultiCollectionId ?? 0);
return Tuple(collectionKey, multiCollectionItems);
default:
return Tuple(collectionKey, new List<MediaItem>());
}
@@ -178,8 +183,11 @@ namespace ErsatzTV.Core.Scheduling
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
{
PlaybackOrder playbackOrder = sortedScheduleItems
.First(item => CollectionKeyForItem(item) == collectionKey)
.PlaybackOrder;
IMediaCollectionEnumerator enumerator =
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems);
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems, playbackOrder);
collectionEnumerators.Add(collectionKey, enumerator);
}
@@ -490,6 +498,7 @@ namespace ErsatzTV.Core.Scheduling
ProgramScheduleId = playout.ProgramScheduleId,
CollectionType = collectionKey.CollectionType,
CollectionId = collectionKey.CollectionId,
MultiCollectionId = collectionKey.MultiCollectionId,
MediaItemId = collectionKey.MediaItemId,
EnumeratorState = maybeEnumeratorState[collectionKey]
});
@@ -503,12 +512,14 @@ namespace ErsatzTV.Core.Scheduling
private async Task<IMediaCollectionEnumerator> GetMediaCollectionEnumerator(
Playout playout,
CollectionKey collectionKey,
List<MediaItem> mediaItems)
List<MediaItem> mediaItems,
PlaybackOrder playbackOrder)
{
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors.FirstOrDefault(
a => a.ProgramScheduleId == playout.ProgramScheduleId
&& a.CollectionType == collectionKey.CollectionType
&& a.CollectionId == collectionKey.CollectionId
&& a.MultiCollectionId == collectionKey.MultiCollectionId
&& a.MediaItemId == collectionKey.MediaItemId);
CollectionEnumeratorState state = maybeAnchor.Match(
@@ -530,8 +541,8 @@ namespace ErsatzTV.Core.Scheduling
state);
}
}
switch (playout.ProgramSchedule.MediaCollectionPlaybackOrder)
switch (playbackOrder)
{
case PlaybackOrder.Chronological:
return new ChronologicalMediaCollectionEnumerator(mediaItems, state);
@@ -539,16 +550,34 @@ namespace ErsatzTV.Core.Scheduling
return new RandomizedMediaCollectionEnumerator(mediaItems, state);
case PlaybackOrder.Shuffle:
return new ShuffledMediaCollectionEnumerator(
mediaItems,
state,
playout.ProgramSchedule.KeepMultiPartEpisodesTogether,
playout.ProgramSchedule.TreatCollectionsAsShows);
await GetGroupedMediaItemsForShuffle(playout, mediaItems, collectionKey),
state);
default:
// TODO: handle this error case differently?
return new RandomizedMediaCollectionEnumerator(mediaItems, state);
}
}
private async Task<List<GroupedMediaItem>> GetGroupedMediaItemsForShuffle(
Playout playout,
List<MediaItem> mediaItems,
CollectionKey collectionKey)
{
if (collectionKey.MultiCollectionId != null)
{
List<CollectionWithItems> collections = await _mediaCollectionRepository
.GetMultiCollectionCollections(collectionKey.MultiCollectionId.Value);
return MultiCollectionGrouper.GroupMediaItems(collections);
}
return playout.ProgramSchedule.KeepMultiPartEpisodesTogether
? MultiPartEpisodeGrouper.GroupMediaItems(
mediaItems,
playout.ProgramSchedule.TreatCollectionsAsShows)
: mediaItems.Map(mi => new GroupedMediaItem(mi, null)).ToList();
}
private static string DisplayTitle(MediaItem mediaItem)
{
switch (mediaItem)
@@ -595,6 +624,11 @@ namespace ErsatzTV.Core.Scheduling
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId
},
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
{
CollectionType = item.CollectionType,
MultiCollectionId = item.MultiCollectionId
},
_ => throw new ArgumentOutOfRangeException(nameof(item))
};
@@ -602,6 +636,7 @@ namespace ErsatzTV.Core.Scheduling
{
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public int? MultiCollectionId { get; set; }
public int? MediaItemId { get; set; }
}
}
@@ -16,16 +16,11 @@ namespace ErsatzTV.Core.Scheduling
private IList<MediaItem> _shuffled;
public ShuffledMediaCollectionEnumerator(
IList<MediaItem> mediaItems,
CollectionEnumeratorState state,
bool keepMultiPartEpisodesTogether,
bool treatCollectionsAsShows)
IList<GroupedMediaItem> mediaItems,
CollectionEnumeratorState state)
{
_mediaItemCount = mediaItems.Count;
_mediaItems = keepMultiPartEpisodesTogether
? MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, treatCollectionsAsShows)
: mediaItems.Map(mi => new GroupedMediaItem(mi, null)).ToList();
_mediaItemCount = mediaItems.Sum(i => 1 + Optional(i.Additional).Flatten().Count());
_mediaItems = mediaItems;
if (state.Index >= _mediaItems.Count)
{
@@ -83,7 +78,7 @@ namespace ErsatzTV.Core.Scheduling
copy[n] = value;
}
return MultiPartEpisodeGrouper.FlattenGroups(copy, _mediaItemCount);
return GroupedMediaItem.FlattenGroups(copy, _mediaItemCount);
}
}
}