From a684dcced993ef7a36d6656fcd8337001a13d8f8 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Tue, 11 Apr 2023 09:22:51 -0500 Subject: [PATCH] scheduling bug fixes (#1238) --- CHANGELOG.md | 7 ++++ ErsatzTV.Core/Scheduling/GroupedMediaItem.cs | 34 ++++++++++++++-- .../Scheduling/MediaItemsForCollection.cs | 40 +++++++++---------- .../Scheduling/MultiCollectionGrouper.cs | 6 +-- .../ShuffleInOrderCollectionEnumerator.cs | 2 +- .../ShuffledMediaCollectionEnumerator.cs | 2 +- .../Repositories/MediaCollectionRepository.cs | 10 ++--- ...MultiEpisodeShuffleCollectionEnumerator.cs | 2 +- ErsatzTV/Pages/Playouts.razor | 3 +- 9 files changed, 69 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c250dfd..ad705b015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Fixed - Limit `HLS Direct` streams to realtime speed +- Fix `Reset Playout` button to use worker thread instead of UI thread + - This fixes potential UI hangs and database concurrency bugs + +### Changed +- Remove duplicate items from smart collections before scheduling + - i.e. shows no longer need to be filtered out if search results also include episodes + - Certain multi-collection scenarios may still include duplicates across multiple collections ## [0.7.7-beta] - 2023-04-07 ### Added diff --git a/ErsatzTV.Core/Scheduling/GroupedMediaItem.cs b/ErsatzTV.Core/Scheduling/GroupedMediaItem.cs index 29de3b6f9..606295ab6 100644 --- a/ErsatzTV.Core/Scheduling/GroupedMediaItem.cs +++ b/ErsatzTV.Core/Scheduling/GroupedMediaItem.cs @@ -4,7 +4,7 @@ using ErsatzTV.Core.Domain; namespace ErsatzTV.Core.Scheduling; [DebuggerDisplay("{" + nameof(First) + "}")] -public class GroupedMediaItem +public class GroupedMediaItem : IEquatable { public GroupedMediaItem() { @@ -16,10 +16,36 @@ public class GroupedMediaItem Additional = additional ?? new List(); } - public MediaItem First { get; set; } - public List Additional { get; set; } + public MediaItem First { get; init; } + public List Additional { get; protected init; } - public static IList FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount) + public bool Equals(GroupedMediaItem other) => + Equals(First.Id, other?.First.Id) && Equals(Additional?.Count, other?.Additional?.Count) && + Equals(Additional?.Map(x => x.Id), other?.Additional?.Map(x => x.Id)); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((GroupedMediaItem)obj); + } + + public override int GetHashCode() => HashCode.Combine(First.Id, Additional?.Map(x => x.Id)); + + public static IList FlattenGroups(IEnumerable copy, int mediaItemCount) { var result = new MediaItem[mediaItemCount]; var i = 0; diff --git a/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs b/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs index 15bcece45..58bac1811 100644 --- a/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs +++ b/ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs @@ -11,36 +11,34 @@ public static class MediaItemsForCollection IArtistRepository artistRepository, CollectionKey collectionKey) { + var result = new List(); + switch (collectionKey.CollectionType) { case ProgramScheduleItemCollectionType.Collection: - List collectionItems = - await mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0); - return collectionItems; + result.AddRange(await mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0)); + break; case ProgramScheduleItemCollectionType.TelevisionShow: - List showItems = - await televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0); - return showItems.Cast().ToList(); + result.AddRange(await televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0)); + break; case ProgramScheduleItemCollectionType.TelevisionSeason: - List seasonItems = - await televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0); - return seasonItems.Cast().ToList(); + result.AddRange(await televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0)); + break; case ProgramScheduleItemCollectionType.Artist: - List artistItems = - await artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0); - return artistItems.Cast().ToList(); + result.AddRange(await artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0)); + break; case ProgramScheduleItemCollectionType.MultiCollection: - List multiCollectionItems = - await mediaCollectionRepository.GetMultiCollectionItems( - collectionKey.MultiCollectionId ?? 0); - return multiCollectionItems; + result.AddRange( + await mediaCollectionRepository.GetMultiCollectionItems(collectionKey.MultiCollectionId ?? 0)); + break; case ProgramScheduleItemCollectionType.SmartCollection: - List smartCollectionItems = - await mediaCollectionRepository.GetSmartCollectionItems( - collectionKey.SmartCollectionId ?? 0); - return smartCollectionItems; + result.AddRange( + await mediaCollectionRepository.GetSmartCollectionItems(collectionKey.SmartCollectionId ?? 0)); + break; default: - return new List(); + throw new ArgumentOutOfRangeException(); } + + return result.DistinctBy(x => x.Id).ToList(); } } diff --git a/ErsatzTV.Core/Scheduling/MultiCollectionGrouper.cs b/ErsatzTV.Core/Scheduling/MultiCollectionGrouper.cs index 6f55b779e..f9efabdd5 100644 --- a/ErsatzTV.Core/Scheduling/MultiCollectionGrouper.cs +++ b/ErsatzTV.Core/Scheduling/MultiCollectionGrouper.cs @@ -1,8 +1,8 @@ namespace ErsatzTV.Core.Scheduling; -public class MultiCollectionGrouper +public static class MultiCollectionGrouper { - public static List GroupMediaItems(IList collections) + public static List GroupMediaItems(IEnumerable collections) { var result = new List(); @@ -18,6 +18,6 @@ public class MultiCollectionGrouper } } - return result; + return result.Distinct().ToList(); } } diff --git a/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs index fdfa912ec..f77180f29 100644 --- a/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs @@ -52,7 +52,7 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator State.Seed = _random.Next(); _random = new Random(State.Seed); _shuffled = Shuffle(_collections, _random); - } while (_collections.Count > 1 && Current == tail); + } while (_collections.Count > 1 && Current.Map(x => x.Id) == tail.Map(x => x.Id)); } else { diff --git a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs index e4ac822d2..aa95b5cbb 100644 --- a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs @@ -49,7 +49,7 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator State.Seed = _random.Next(); _random = new CloneableRandom(State.Seed); _shuffled = Shuffle(_mediaItems, _random); - } while (_mediaItems.Count > 1 && Current == tail); + } while (_mediaItems.Count > 1 && Current.Map(x => x.Id) == tail.Map(x => x.Id)); } else { diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 26be4fd40..19ff4c8c9 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -86,7 +86,7 @@ public class MediaCollectionRepository : IMediaCollectionRepository } } - return result.Distinct().ToList(); + return result.DistinctBy(x => x.Id).ToList(); } public async Task> GetSmartCollectionItems(int id) @@ -150,7 +150,7 @@ public class MediaCollectionRepository : IMediaCollectionRepository result.AddRange(await GetSongItems(dbContext, songIds)); } - return result; + return result.DistinctBy(x => x.Id).ToList(); } public async Task> GetMultiCollectionCollections(int id) @@ -221,14 +221,14 @@ public class MediaCollectionRepository : IMediaCollectionRepository // remove duplicate items from ungrouped collections var toRemoveFrom = result.Filter(c => !c.ScheduleAsGroup).ToList(); - var toRemove = result.Filter(c => c.ScheduleAsGroup) + var scheduleAsGroupItemIds = result.Filter(c => c.ScheduleAsGroup) .SelectMany(c => c.MediaItems.Map(i => i.Id)) .Distinct() - .ToList(); + .ToHashSet(); foreach (CollectionWithItems collection in toRemoveFrom) { - collection.MediaItems.RemoveAll(mi => toRemove.Contains(mi.Id)); + collection.MediaItems.RemoveAll(mi => scheduleAsGroupItemIds.Contains(mi.Id)); } return result; diff --git a/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs b/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs index 859a31794..b037f9ee2 100644 --- a/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs +++ b/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs @@ -93,7 +93,7 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato State.Seed = _random.Next(); _random = new CloneableRandom(State.Seed); _shuffled = Shuffle(_random); - } while (_mediaItemCount > 1 && Current == tail); + } while (_mediaItemCount > 1 && Current.Map(x => x.Id) == tail.Map(x => x.Id)); } else { diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 56ceec0b3..f4879fffe 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -5,6 +5,7 @@ @implements IDisposable @inject IDialogService _dialog @inject IMediator _mediator +@inject ChannelWriter WorkerChannel; @@ -176,7 +177,7 @@ private async Task ResetPlayout(PlayoutNameViewModel playout) { - await _mediator.Send(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token); + await WorkerChannel.WriteAsync(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token); if (_table != null) { await _table.ReloadServerData();