scheduling bug fixes (#1238)
This commit is contained in:
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Fixed
|
### Fixed
|
||||||
- Limit `HLS Direct` streams to realtime speed
|
- 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
|
## [0.7.7-beta] - 2023-04-07
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using ErsatzTV.Core.Domain;
|
|||||||
namespace ErsatzTV.Core.Scheduling;
|
namespace ErsatzTV.Core.Scheduling;
|
||||||
|
|
||||||
[DebuggerDisplay("{" + nameof(First) + "}")]
|
[DebuggerDisplay("{" + nameof(First) + "}")]
|
||||||
public class GroupedMediaItem
|
public class GroupedMediaItem : IEquatable<GroupedMediaItem>
|
||||||
{
|
{
|
||||||
public GroupedMediaItem()
|
public GroupedMediaItem()
|
||||||
{
|
{
|
||||||
@@ -16,10 +16,36 @@ public class GroupedMediaItem
|
|||||||
Additional = additional ?? new List<MediaItem>();
|
Additional = additional ?? new List<MediaItem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaItem First { get; set; }
|
public MediaItem First { get; init; }
|
||||||
public List<MediaItem> Additional { get; set; }
|
public List<MediaItem> Additional { get; protected init; }
|
||||||
|
|
||||||
public static IList<MediaItem> 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<MediaItem> FlattenGroups(IEnumerable<GroupedMediaItem> copy, int mediaItemCount)
|
||||||
{
|
{
|
||||||
var result = new MediaItem[mediaItemCount];
|
var result = new MediaItem[mediaItemCount];
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
|||||||
@@ -11,36 +11,34 @@ public static class MediaItemsForCollection
|
|||||||
IArtistRepository artistRepository,
|
IArtistRepository artistRepository,
|
||||||
CollectionKey collectionKey)
|
CollectionKey collectionKey)
|
||||||
{
|
{
|
||||||
|
var result = new List<MediaItem>();
|
||||||
|
|
||||||
switch (collectionKey.CollectionType)
|
switch (collectionKey.CollectionType)
|
||||||
{
|
{
|
||||||
case ProgramScheduleItemCollectionType.Collection:
|
case ProgramScheduleItemCollectionType.Collection:
|
||||||
List<MediaItem> collectionItems =
|
result.AddRange(await mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0));
|
||||||
await mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0);
|
break;
|
||||||
return collectionItems;
|
|
||||||
case ProgramScheduleItemCollectionType.TelevisionShow:
|
case ProgramScheduleItemCollectionType.TelevisionShow:
|
||||||
List<Episode> showItems =
|
result.AddRange(await televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0));
|
||||||
await televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0);
|
break;
|
||||||
return showItems.Cast<MediaItem>().ToList();
|
|
||||||
case ProgramScheduleItemCollectionType.TelevisionSeason:
|
case ProgramScheduleItemCollectionType.TelevisionSeason:
|
||||||
List<Episode> seasonItems =
|
result.AddRange(await televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0));
|
||||||
await televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0);
|
break;
|
||||||
return seasonItems.Cast<MediaItem>().ToList();
|
|
||||||
case ProgramScheduleItemCollectionType.Artist:
|
case ProgramScheduleItemCollectionType.Artist:
|
||||||
List<MusicVideo> artistItems =
|
result.AddRange(await artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0));
|
||||||
await artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0);
|
break;
|
||||||
return artistItems.Cast<MediaItem>().ToList();
|
|
||||||
case ProgramScheduleItemCollectionType.MultiCollection:
|
case ProgramScheduleItemCollectionType.MultiCollection:
|
||||||
List<MediaItem> multiCollectionItems =
|
result.AddRange(
|
||||||
await mediaCollectionRepository.GetMultiCollectionItems(
|
await mediaCollectionRepository.GetMultiCollectionItems(collectionKey.MultiCollectionId ?? 0));
|
||||||
collectionKey.MultiCollectionId ?? 0);
|
break;
|
||||||
return multiCollectionItems;
|
|
||||||
case ProgramScheduleItemCollectionType.SmartCollection:
|
case ProgramScheduleItemCollectionType.SmartCollection:
|
||||||
List<MediaItem> smartCollectionItems =
|
result.AddRange(
|
||||||
await mediaCollectionRepository.GetSmartCollectionItems(
|
await mediaCollectionRepository.GetSmartCollectionItems(collectionKey.SmartCollectionId ?? 0));
|
||||||
collectionKey.SmartCollectionId ?? 0);
|
break;
|
||||||
return smartCollectionItems;
|
|
||||||
default:
|
default:
|
||||||
return new List<MediaItem>();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.DistinctBy(x => x.Id).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace ErsatzTV.Core.Scheduling;
|
namespace ErsatzTV.Core.Scheduling;
|
||||||
|
|
||||||
public class MultiCollectionGrouper
|
public static class MultiCollectionGrouper
|
||||||
{
|
{
|
||||||
public static List<GroupedMediaItem> GroupMediaItems(IList<CollectionWithItems> collections)
|
public static List<GroupedMediaItem> GroupMediaItems(IEnumerable<CollectionWithItems> collections)
|
||||||
{
|
{
|
||||||
var result = new List<GroupedMediaItem>();
|
var result = new List<GroupedMediaItem>();
|
||||||
|
|
||||||
@@ -18,6 +18,6 @@ public class MultiCollectionGrouper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.Distinct().ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
State.Seed = _random.Next();
|
State.Seed = _random.Next();
|
||||||
_random = new Random(State.Seed);
|
_random = new Random(State.Seed);
|
||||||
_shuffled = Shuffle(_collections, _random);
|
_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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
|||||||
State.Seed = _random.Next();
|
State.Seed = _random.Next();
|
||||||
_random = new CloneableRandom(State.Seed);
|
_random = new CloneableRandom(State.Seed);
|
||||||
_shuffled = Shuffle(_mediaItems, _random);
|
_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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public class MediaCollectionRepository : IMediaCollectionRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Distinct().ToList();
|
return result.DistinctBy(x => x.Id).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MediaItem>> GetSmartCollectionItems(int id)
|
public async Task<List<MediaItem>> GetSmartCollectionItems(int id)
|
||||||
@@ -150,7 +150,7 @@ public class MediaCollectionRepository : IMediaCollectionRepository
|
|||||||
result.AddRange(await GetSongItems(dbContext, songIds));
|
result.AddRange(await GetSongItems(dbContext, songIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.DistinctBy(x => x.Id).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id)
|
public async Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id)
|
||||||
@@ -221,14 +221,14 @@ public class MediaCollectionRepository : IMediaCollectionRepository
|
|||||||
|
|
||||||
// remove duplicate items from ungrouped collections
|
// remove duplicate items from ungrouped collections
|
||||||
var toRemoveFrom = result.Filter(c => !c.ScheduleAsGroup).ToList();
|
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))
|
.SelectMany(c => c.MediaItems.Map(i => i.Id))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToHashSet();
|
||||||
|
|
||||||
foreach (CollectionWithItems collection in toRemoveFrom)
|
foreach (CollectionWithItems collection in toRemoveFrom)
|
||||||
{
|
{
|
||||||
collection.MediaItems.RemoveAll(mi => toRemove.Contains(mi.Id));
|
collection.MediaItems.RemoveAll(mi => scheduleAsGroupItemIds.Contains(mi.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
|||||||
State.Seed = _random.Next();
|
State.Seed = _random.Next();
|
||||||
_random = new CloneableRandom(State.Seed);
|
_random = new CloneableRandom(State.Seed);
|
||||||
_shuffled = Shuffle(_random);
|
_shuffled = Shuffle(_random);
|
||||||
} while (_mediaItemCount > 1 && Current == tail);
|
} while (_mediaItemCount > 1 && Current.Map(x => x.Id) == tail.Map(x => x.Id));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject IDialogService _dialog
|
@inject IDialogService _dialog
|
||||||
@inject IMediator _mediator
|
@inject IMediator _mediator
|
||||||
|
@inject ChannelWriter<IBackgroundServiceRequest> WorkerChannel;
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="playouts/add">
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" Link="playouts/add">
|
||||||
@@ -176,7 +177,7 @@
|
|||||||
|
|
||||||
private async Task ResetPlayout(PlayoutNameViewModel playout)
|
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)
|
if (_table != null)
|
||||||
{
|
{
|
||||||
await _table.ReloadServerData();
|
await _table.ReloadServerData();
|
||||||
|
|||||||
Reference in New Issue
Block a user