diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index 50883bd66..560e76fcb 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -25,6 +25,13 @@ public class BlockPlayoutBuilder( playout.Channel.Number, playout.Channel.Name); + List allowedPlaybackOrders = + [ + PlaybackOrder.Chronological, + PlaybackOrder.SeasonEpisode, + PlaybackOrder.Random + ]; + DateTimeOffset start = DateTimeOffset.Now; int daysToBuild = await configElementRepository.GetValue(ConfigElementKey.PlayoutDaysToBuild) @@ -58,10 +65,10 @@ public class BlockPlayoutBuilder( DateTimeOffset currentTime = effectiveBlock.Start; - foreach (BlockItem blockItem in effectiveBlock.Block.Items) + foreach (BlockItem blockItem in effectiveBlock.Block.Items.OrderBy(i => i.Index)) { // TODO: support other playback orders - if (blockItem.PlaybackOrder is not PlaybackOrder.SeasonEpisode and not PlaybackOrder.Chronological) + if (!allowedPlaybackOrders.Contains(blockItem.PlaybackOrder)) { continue; } @@ -82,61 +89,27 @@ public class BlockPlayoutBuilder( var collectionKey = CollectionKey.ForBlockItem(blockItem); List collectionItems = collectionMediaItems[collectionKey]; + // get enumerator - var enumerator = new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state); + IMediaCollectionEnumerator enumerator = blockItem.PlaybackOrder switch + { + PlaybackOrder.Chronological => new ChronologicalMediaCollectionEnumerator(collectionItems, state), + PlaybackOrder.SeasonEpisode => new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state), + _ => new RandomizedMediaCollectionEnumerator( + collectionItems, + new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 }) + }; // seek to the appropriate place in the collection enumerator foreach (PlayoutHistory history in maybeHistory) { logger.LogDebug("History is applicable: {When}: {History}", history.When, history.Details); - // find next media item - HistoryDetails.Details details = - JsonConvert.DeserializeObject(history.Details); - if (details.SeasonNumber.HasValue && details.EpisodeNumber.HasValue) - { - Option maybeMatchedItem = Optional( - collectionItems.Find( - ci => ci is Episode e && - e.EpisodeMetadata.Any(em => em.EpisodeNumber == details.EpisodeNumber.Value) && - e.Season.SeasonNumber == details.SeasonNumber.Value)); - - var copy = collectionItems.ToList(); - - if (maybeMatchedItem.IsNone) - { - var fakeItem = new Episode - { - Season = new Season { SeasonNumber = details.SeasonNumber.Value }, - EpisodeMetadata = - [ - new EpisodeMetadata - { - EpisodeNumber = details.EpisodeNumber.Value, - ReleaseDate = details.ReleaseDate - } - ] - }; - - copy.Add(fakeItem); - maybeMatchedItem = fakeItem; - } - - foreach (MediaItem matchedItem in maybeMatchedItem) - { - IComparer comparer = blockItem.PlaybackOrder switch - { - PlaybackOrder.Chronological => new ChronologicalMediaComparer(), - _ => new SeasonEpisodeMediaComparer() - }; - - copy.Sort(comparer); - - state.Index = copy.IndexOf(matchedItem); - enumerator.ResetState(state); - enumerator.MoveNext(); - } - } + HistoryDetails.MoveToNextItem( + collectionItems, + history.Details, + enumerator, + blockItem.PlaybackOrder); } foreach (MediaItem mediaItem in enumerator.Current) diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs index 64929365f..3fb763a0b 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/HistoryDetails.cs @@ -1,5 +1,6 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Core.Interfaces.Scheduling; using Newtonsoft.Json; namespace ErsatzTV.Core.Scheduling.BlockScheduling; @@ -32,12 +33,104 @@ internal static class HistoryDetails Details details = mediaItem switch { Episode e => ForEpisode(e), + Movie m => ForMovie(m), _ => new Details(mediaItem.Id, null, null, null) }; return JsonConvert.SerializeObject(details, Formatting.None, JsonSettings); } + public static void MoveToNextItem( + List collectionItems, + string detailsString, + IMediaCollectionEnumerator enumerator, + PlaybackOrder playbackOrder) + { + if (playbackOrder is PlaybackOrder.Random) + { + return; + } + + Option maybeMatchedItem = Option.None; + var copy = collectionItems.ToList(); + + Details details = JsonConvert.DeserializeObject
(detailsString); + if (details.SeasonNumber.HasValue && details.EpisodeNumber.HasValue) + { + int season = details.SeasonNumber.Value; + int episode = details.EpisodeNumber.Value; + + maybeMatchedItem = Optional(collectionItems.Find(ci => MatchSeasonAndEpisode(ci, season, episode))); + + if (maybeMatchedItem.IsNone) + { + var fakeItem = new Episode + { + Season = new Season { SeasonNumber = season }, + EpisodeMetadata = + [ + new EpisodeMetadata + { + EpisodeNumber = episode, + ReleaseDate = details.ReleaseDate + } + ] + }; + + copy.Add(fakeItem); + maybeMatchedItem = fakeItem; + } + } + else if (playbackOrder is PlaybackOrder.Chronological && details.ReleaseDate.HasValue) + { + maybeMatchedItem = Optional(collectionItems.Find(ci => MatchReleaseDate(ci, details.ReleaseDate.Value))); + + if (maybeMatchedItem.IsNone) + { + var fakeItem = new Movie { MovieMetadata = [new MovieMetadata { ReleaseDate = details.ReleaseDate }] }; + copy.Add(fakeItem); + maybeMatchedItem = fakeItem; + } + } + + foreach (MediaItem matchedItem in maybeMatchedItem) + { + IComparer comparer = playbackOrder switch + { + PlaybackOrder.Chronological => new ChronologicalMediaComparer(), + _ => new SeasonEpisodeMediaComparer(), + }; + + copy.Sort(comparer); + + var state = new CollectionEnumeratorState + { + Seed = enumerator.State.Seed, + Index = copy.IndexOf(matchedItem) + }; + enumerator.ResetState(state); + enumerator.MoveNext(); + } + } + + private static bool MatchReleaseDate(MediaItem mediaItem, DateTime releaseDate) => + mediaItem switch + { + Movie m => m.MovieMetadata.Any(mm => mm.ReleaseDate == releaseDate), + Episode e => e.EpisodeMetadata.Any(em => em.ReleaseDate == releaseDate), + //MusicVideo mv => mv.MusicVideoMetadata.Any(mvm => mvm.ReleaseDate == releaseDate), + OtherVideo ov => ov.OtherVideoMetadata.Any(ovm => ovm.ReleaseDate == releaseDate), + _ => false + }; + + private static bool MatchSeasonAndEpisode(MediaItem mediaItem, int seasonNumber, int episodeNumber) => + mediaItem switch + { + Episode e => e.Season.SeasonNumber == seasonNumber && + e.EpisodeMetadata.Any(em => em.EpisodeNumber == episodeNumber), + _ => false + }; + private static Details ForEpisode(Episode e) { int? episodeNumber = null; @@ -51,5 +144,16 @@ internal static class HistoryDetails return new Details(e.Id, releaseDate, e.Season.SeasonNumber, episodeNumber); } + private static Details ForMovie(Movie m) + { + DateTime? releaseDate = null; + foreach (MovieMetadata movieMetadata in m.MovieMetadata.HeadOrNone()) + { + releaseDate = movieMetadata.ReleaseDate; + } + + return new Details(m.Id, releaseDate, null, null); + } + public record Details(int? MediaItemId, DateTime? ReleaseDate, int? SeasonNumber, int? EpisodeNumber); } diff --git a/ErsatzTV/Pages/BlockEditor.razor b/ErsatzTV/Pages/BlockEditor.razor index 346e64ea7..39fa50efe 100644 --- a/ErsatzTV/Pages/BlockEditor.razor +++ b/ErsatzTV/Pages/BlockEditor.razor @@ -89,12 +89,12 @@ - @* Collection *@ + Collection Television Show Television Season @* Artist *@ @* Multi Collection *@ - @* Smart Collection *@ + Smart Collection @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection) { @@ -180,14 +180,14 @@ case ProgramScheduleItemCollectionType.Collection: case ProgramScheduleItemCollectionType.SmartCollection: Chronological - @* Random *@ + Random @* Shuffle *@ @* Shuffle In Order *@ break; case ProgramScheduleItemCollectionType.TelevisionShow: Chronological Season, Episode - @* Random *@ + Random @* Shuffle *@ @* Multi-Episode Shuffle *@ break; @@ -196,7 +196,7 @@ case ProgramScheduleItemCollectionType.FakeCollection: default: Chronological - @* Random *@ + Random @* Shuffle *@ break; }