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:
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user