Browse Source

yaml marathon history (#2094)

* better playlist tests

* fix history for marathon content in yaml playouts
pull/2095/head
Jason Dove 1 year ago committed by GitHub
parent
commit
037cee873f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 18
      ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs
  3. 2
      ErsatzTV.Core/Domain/PlaybackOrder.cs
  4. 6
      ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs
  5. 38
      ErsatzTV.Core/Scheduling/HistoryDetails.cs
  6. 12
      ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs
  7. 19
      ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs
  8. 5
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs
  9. 105
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs
  10. 84
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs
  11. 5
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  12. 21
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs
  13. 3
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs
  14. 3
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs
  15. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs
  16. 6
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentMarathonItem.cs
  17. 9
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlMarathonContentResult.cs
  18. 19
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  19. 10
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutMarathonHelper.cs
  20. 5902
      ErsatzTV.Infrastructure.MySql/Migrations/20250630151214_Add_PlayoutHistoryChildKey.Designer.cs
  21. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250630151214_Add_PlayoutHistoryChildKey.cs
  22. 5905
      ErsatzTV.Infrastructure.MySql/Migrations/20250630185129_Add_PlayoutHistoryIsCurrentChild.Designer.cs
  23. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250630185129_Add_PlayoutHistoryIsCurrentChild.cs
  24. 6
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  25. 5741
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250630151239_Add_PlayoutHistoryChildKey.Designer.cs
  26. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250630151239_Add_PlayoutHistoryChildKey.cs
  27. 5744
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250630185058_Add_PlayoutHistoryIsCurrentChild.Designer.cs
  28. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250630185058_Add_PlayoutHistoryIsCurrentChild.cs
  29. 6
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

2
CHANGELOG.md

@ -65,6 +65,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -65,6 +65,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- All NVIDIA docker users
- Windows NVIDIA users who have set the `ETV_DISABLE_VULKAN` env var
- Fix audio sync issue with QSV acceleration
- YAML playout: fix history for marathon content
- This allows playouts to be extended correctly, instead of always resetting to the earliest item in each group
## [25.2.0] - 2025-06-24
### Added

18
ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs

@ -63,15 +63,18 @@ public class PlaylistEnumeratorTests @@ -63,15 +63,18 @@ public class PlaylistEnumeratorTests
shufflePlaylistItems: false,
CancellationToken.None);
var items = new List<int>();
items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext();
var totalLength = 1;
while (enumerator.State.Index > 0)
{
items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext();
totalLength += 1;
}
totalLength.ShouldBe(8);
items.Count.ShouldBe(8);
items.ShouldBe([10, 20, 21, 30, 10, 20, 21, 31]);
}
[Test]
@ -127,15 +130,18 @@ public class PlaylistEnumeratorTests @@ -127,15 +130,18 @@ public class PlaylistEnumeratorTests
shufflePlaylistItems: false,
CancellationToken.None);
var items = new List<int>();
items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext();
var totalLength = 1;
while (enumerator.State.Index > 0)
{
items.AddRange(enumerator.Current.Map(mi => mi.Id));
enumerator.MoveNext();
totalLength += 1;
}
totalLength.ShouldBe(8);
items.Count.ShouldBe(8);
items.ShouldBe([10, 20, 30, 31, 10, 21, 30, 31]);
}
private static Movie FakeMovie(int id) => new()

2
ErsatzTV.Core/Domain/PlaybackOrder.cs

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
public enum PlaybackOrder
{
None = 0,
Chronological = 1,
Random = 2,
Shuffle = 3,

6
ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs

@ -15,6 +15,12 @@ public class PlayoutHistory @@ -15,6 +15,12 @@ public class PlayoutHistory
// something that uniquely identifies the collection within the block
public string Key { get; set; }
// something that uniquely identifies a child collection within the parent collection
// e.g. for a playlist
public string ChildKey { get; set; }
public bool IsCurrentChild { get; set; }
// last occurence of an item from this collection in the playout
public DateTime When { get; set; }

38
ErsatzTV.Core/Scheduling/HistoryDetails.cs

@ -30,11 +30,36 @@ internal static class HistoryDetails @@ -30,11 +30,36 @@ internal static class HistoryDetails
}
public static string KeyForYamlContent(YamlPlayoutContentItem contentItem)
{
var key = new Dictionary<string, object>
{
{ "Key", contentItem.Key },
{ "Order", contentItem.Order }
};
// we need to ignore history when any of these properties change
if (contentItem is YamlPlayoutContentMarathonItem marathonItem)
{
key["ItemOrder"] = marathonItem.ItemOrder;
//key.ShuffleGroups = marathonItem.ShuffleGroups;
key["GroupBy"] = marathonItem.GroupBy;
//key.PlayAllItems = marathonItem.PlayAllItems;
}
return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
}
public static string KeyForCollectionKey(CollectionKey collectionKey)
{
dynamic key = new
{
contentItem.Key,
contentItem.Order
collectionKey.CollectionType,
collectionKey.CollectionId,
collectionKey.MultiCollectionId,
collectionKey.SmartCollectionId,
collectionKey.MediaItemId,
collectionKey.PlaylistId,
collectionKey.FakeCollectionKey,
};
return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
@ -71,7 +96,8 @@ internal static class HistoryDetails @@ -71,7 +96,8 @@ internal static class HistoryDetails
List<MediaItem> collectionItems,
string detailsString,
IMediaCollectionEnumerator enumerator,
PlaybackOrder playbackOrder)
PlaybackOrder playbackOrder,
bool current = false)
{
if (playbackOrder is PlaybackOrder.Random)
{
@ -143,7 +169,11 @@ internal static class HistoryDetails @@ -143,7 +169,11 @@ internal static class HistoryDetails
Index = copy.IndexOf(matchedItem)
};
enumerator.ResetState(state);
enumerator.MoveNext();
if (!current)
{
enumerator.MoveNext();
}
}
}

12
ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories;
@ -77,6 +78,15 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -77,6 +78,15 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
}
}
public ImmutableDictionary<CollectionKey, IMediaCollectionEnumerator> ChildEnumerators { get; private set; }
public int EnumeratorIndex => _enumeratorIndex;
public void SetEnumeratorIndex(int primaryHistoryIndex)
{
_enumeratorIndex = primaryHistoryIndex;
}
public static async Task<PlaylistEnumerator> Create(
IMediaCollectionRepository mediaCollectionRepository,
Dictionary<PlaylistItem, List<MediaItem>> playlistItemMap,
@ -203,6 +213,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator @@ -203,6 +213,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
}
}
result.ChildEnumerators = enumeratorMap.ToImmutableDictionary();
return result;
}

19
ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs

@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling; @@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepository, ILogger logger)
{
private readonly Dictionary<string, List<MediaItem>> _mediaItems = new();
private readonly Dictionary<PlaylistKey, List<MediaItem>> _playlistMediaItems = new();
private readonly Dictionary<string, IMediaCollectionEnumerator> _enumerators = new();
public System.Collections.Generic.HashSet<string> MissingContentKeys { get; } = [];
@ -17,6 +18,11 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor @@ -17,6 +18,11 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
public List<MediaItem> MediaItemsForContent(string contentKey) =>
_mediaItems.TryGetValue(contentKey, out List<MediaItem> items) ? items : [];
public List<MediaItem> PlaylistMediaItemsForContent(string contentKey, CollectionKey collectionKey) =>
_playlistMediaItems.TryGetValue(new PlaylistKey(contentKey, collectionKey), out List<MediaItem> items)
? items
: [];
public async Task<Option<IMediaCollectionEnumerator>> GetCachedEnumeratorForContent(
YamlPlayoutContext context,
string contentKey,
@ -91,7 +97,16 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor @@ -91,7 +97,16 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
if (content is YamlPlayoutContentMarathonItem marathon)
{
var helper = new YamlPlayoutMarathonHelper(mediaCollectionRepository);
return await helper.GetEnumerator(marathon, state, cancellationToken);
Option<YamlMarathonContentResult> maybeResult = await helper.GetEnumerator(marathon, state, cancellationToken);
foreach (YamlMarathonContentResult result in maybeResult)
{
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in result.Content)
{
_playlistMediaItems.Add(new PlaylistKey(contentKey, collectionKey), mediaItems);
}
return Some(result.PlaylistEnumerator);
}
}
// playlist is a special case that needs to be handled on its own
@ -130,4 +145,6 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor @@ -130,4 +145,6 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
return Option<IMediaCollectionEnumerator>.None;
}
private record PlaylistKey(string ContentKey, CollectionKey CollectionKey);
}

5
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs

@ -61,12 +61,13 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -61,12 +61,13 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
context.AdvanceGuideGroup();
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
instruction.Content,
enumerator,
playoutItem,
mediaItem);
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{

105
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs

@ -38,31 +38,104 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache) @@ -38,31 +38,104 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache)
string historyKey = HistoryDetails.KeyForYamlContent(contentItem);
DateTime historyTime = context.CurrentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = context.Playout.PlayoutHistory
Option<DateTime> maxWhen = await context.Playout.PlayoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
.Map(h => h.When)
.OrderByDescending(h => h)
.HeadOrNone()
.IfNoneAsync(DateTime.MinValue);
var maybeHistory = context.Playout.PlayoutHistory
.Filter(h => h.Key == historyKey)
.Filter(h => h.When == maxWhen)
.ToList();
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
List<MediaItem> collectionItems = enumeratorCache.MediaItemsForContent(contentItem.Key);
// seek to the appropriate place in the collection enumerator
foreach (PlayoutHistory h in maybeHistory)
if (contentItem is YamlPlayoutContentMarathonItem marathonItem && enumerator is PlaylistEnumerator playlistEnumerator)
{
logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
Option<PlayoutHistory> maybePrimaryHistory = maybeHistory
.Filter(h => string.IsNullOrWhiteSpace(h.ChildKey))
.HeadOrNone();
foreach (PlayoutHistory primaryHistory in maybePrimaryHistory)
{
bool hasSetEnumeratorIndex = false;
if (!Enum.TryParse(marathonItem.ItemOrder, true, out PlaybackOrder itemPlaybackOrder))
{
itemPlaybackOrder = PlaybackOrder.None;
}
var childEnumeratorKeys = playlistEnumerator.ChildEnumerators.Keys.ToList();
foreach ((CollectionKey collectionKey, IMediaCollectionEnumerator childEnumerator) in
playlistEnumerator.ChildEnumerators)
{
Option<PlayoutHistory> maybeApplicableHistory = maybeHistory
.Filter(h => h.ChildKey == HistoryDetails.KeyForCollectionKey(collectionKey))
.HeadOrNone();
List<MediaItem> collectionItems =
enumeratorCache.PlaylistMediaItemsForContent(contentItem.Key, collectionKey);
foreach (PlayoutHistory h in maybeApplicableHistory)
{
// logger.LogDebug(
// "History is applicable: {When}: {ChildKey} / {History}",
// h.When,
// h.ChildKey,
// h.Details);
enumerator.ResetState(
new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 });
enumerator.ResetState(
new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 });
if (playbackOrder is PlaybackOrder.Chronological)
if (itemPlaybackOrder is PlaybackOrder.Chronological)
{
HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
childEnumerator,
playbackOrder,
h.IsCurrentChild);
}
if (h.IsCurrentChild)
{
// try to find enumerator based on collection key
playlistEnumerator.SetEnumeratorIndex(childEnumeratorKeys.IndexOf(collectionKey));
hasSetEnumeratorIndex = true;
}
}
}
if (!hasSetEnumeratorIndex)
{
// falling back to enumerator based on index
playlistEnumerator.SetEnumeratorIndex(primaryHistory.Index);
}
}
}
else
{
List<MediaItem> collectionItems = enumeratorCache.MediaItemsForContent(contentItem.Key);
// seek to the appropriate place in the collection enumerator
foreach (PlayoutHistory h in maybeHistory)
{
HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
enumerator,
playbackOrder);
// logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
enumerator.ResetState(
new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 });
if (playbackOrder is PlaybackOrder.Chronological)
{
HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
enumerator,
playbackOrder);
}
}
}
}

84
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
using System.Collections.Immutable;
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
@ -47,40 +49,90 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) @@ -47,40 +49,90 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
return maybeEnumerator;
}
protected static Option<PlayoutHistory> GetHistoryForItem(
protected static List<PlayoutHistory> GetHistoryForItem(
YamlPlayoutContext context,
string contentKey,
IMediaCollectionEnumerator enumerator,
PlayoutItem playoutItem,
MediaItem mediaItem)
MediaItem mediaItem,
ILogger<YamlPlayoutBuilder> logger)
{
int index = context.Definition.Content.FindIndex(c => c.Key == contentKey);
if (index < 0)
{
return Option<PlayoutHistory>.None;
logger.LogDebug("Unable to find history for content matching key {ContentKey}", contentKey);
return [];
}
YamlPlayoutContentItem contentItem = context.Definition.Content[index];
if (!Enum.TryParse(contentItem.Order, true, out PlaybackOrder playbackOrder))
{
return Option<PlayoutHistory>.None;
logger.LogDebug("Unable to find history for content matching playback order {PlaybackOrder}", contentItem.Order);
return [];
}
var result = new List<PlayoutHistory>();
string historyKey = HistoryDetails.KeyForYamlContent(contentItem);
// create a playout history record
var nextHistory = new PlayoutHistory
if (contentItem is YamlPlayoutContentMarathonItem && enumerator is PlaylistEnumerator playlistEnumerator)
{
PlayoutId = context.Playout.Id,
PlaybackOrder = playbackOrder,
Index = enumerator.State.Index,
When = playoutItem.StartOffset.UtcDateTime,
Finish = playoutItem.FinishOffset.UtcDateTime,
Key = historyKey,
Details = HistoryDetails.ForMediaItem(mediaItem)
};
return nextHistory;
// create a playout history record
var nextHistory = new PlayoutHistory
{
PlayoutId = context.Playout.Id,
PlaybackOrder = playbackOrder,
Index = playlistEnumerator.EnumeratorIndex,
When = playoutItem.StartOffset.UtcDateTime,
Finish = playoutItem.FinishOffset.UtcDateTime,
Key = historyKey,
Details = HistoryDetails.ForMediaItem(mediaItem)
};
result.Add(nextHistory);
var childEnumeratorKeys = playlistEnumerator.ChildEnumerators.Keys.ToList();
foreach ((CollectionKey collectionKey, IMediaCollectionEnumerator childEnumerator) in playlistEnumerator.ChildEnumerators)
{
bool isCurrentChild = childEnumeratorKeys.IndexOf(collectionKey) == playlistEnumerator.EnumeratorIndex;
foreach (MediaItem currentMediaItem in childEnumerator.Current)
{
// create a playout history record
var childHistory = new PlayoutHistory
{
PlayoutId = context.Playout.Id,
PlaybackOrder = playbackOrder,
Index = childEnumerator.State.Index,
When = playoutItem.StartOffset.UtcDateTime,
Finish = playoutItem.FinishOffset.UtcDateTime,
Key = historyKey,
ChildKey = HistoryDetails.KeyForCollectionKey(collectionKey),
IsCurrentChild = isCurrentChild,
Details = HistoryDetails.ForMediaItem(currentMediaItem)
};
result.Add(childHistory);
}
}
}
else
{
// create a playout history record
var nextHistory = new PlayoutHistory
{
PlayoutId = context.Playout.Id,
PlaybackOrder = playbackOrder,
Index = enumerator.State.Index,
When = playoutItem.StartOffset.UtcDateTime,
Finish = playoutItem.FinishOffset.UtcDateTime,
Key = historyKey,
Details = HistoryDetails.ForMediaItem(mediaItem)
};
result.Add(nextHistory);
}
return result;
}
protected static TimeSpan DurationForMediaItem(MediaItem mediaItem)

5
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs

@ -61,12 +61,13 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -61,12 +61,13 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
context.AdvanceGuideGroup();
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
instruction.Content,
enumerator,
playoutItem,
mediaItem);
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{

21
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs

@ -62,7 +62,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -62,7 +62,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
GetFillerKind(duration),
duration.CustomTitle,
enumerator,
fallbackEnumerator);
fallbackEnumerator,
logger);
return true;
}
@ -82,7 +83,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -82,7 +83,8 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
FillerKind fillerKind,
string customTitle,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator)
Option<IMediaCollectionEnumerator> fallbackEnumerator,
ILogger<YamlPlayoutBuilder> logger)
{
bool done = false;
TimeSpan remainingToFill = targetTime - context.CurrentTime;
@ -111,12 +113,13 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -111,12 +113,13 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
context.AdvanceGuideGroup();
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
contentKey,
enumerator,
playoutItem,
mediaItem);
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{
@ -144,12 +147,13 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -144,12 +147,13 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
context.AdvanceGuideGroup();
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
contentKey,
enumerator,
playoutItem,
mediaItem);
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{
@ -179,12 +183,13 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -179,12 +183,13 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
context.Playout.Items.Add(playoutItem);
// create history record
Option<PlayoutHistory> maybeHistory = GetHistoryForItem(
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
fallbackContentKey,
fallback,
playoutItem,
mediaItem);
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{

3
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs

@ -64,7 +64,8 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml @@ -64,7 +64,8 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml
GetFillerKind(padToNext),
padToNext.CustomTitle,
enumerator,
fallbackEnumerator);
fallbackEnumerator,
logger);
return true;
}

3
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs

@ -84,7 +84,8 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP @@ -84,7 +84,8 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP
GetFillerKind(padUntil),
padUntil.CustomTitle,
enumerator,
fallbackEnumerator);
fallbackEnumerator,
logger);
return true;
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs

@ -5,7 +5,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; @@ -5,7 +5,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutContentItem
{
public string Key { get; set; }
public string Order { get; set; }
public virtual string Order { get; set; }
[YamlMember(Alias = "multi_part", ApplyNamingConventions = false)]
public bool MultiPart { get; set; }

6
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentMarathonItem.cs

@ -21,4 +21,10 @@ public class YamlPlayoutContentMarathonItem : YamlPlayoutContentItem @@ -21,4 +21,10 @@ public class YamlPlayoutContentMarathonItem : YamlPlayoutContentItem
[YamlMember(Alias = "play_all_items", ApplyNamingConventions = false)]
public bool PlayAllItems { get; set; }
public override string Order
{
get => "none";
set => throw new NotSupportedException();
}
}

9
ErsatzTV.Core/Scheduling/YamlScheduling/YamlMarathonContentResult.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public record YamlMarathonContentResult(
IMediaCollectionEnumerator PlaylistEnumerator,
ImmutableDictionary<CollectionKey, List<MediaItem>> Content);

19
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs

@ -379,16 +379,23 @@ public class YamlPlayoutBuilder( @@ -379,16 +379,23 @@ public class YamlPlayoutBuilder(
foreach ((string _, List<PlayoutHistory> group) in groups)
{
//logger.LogDebug("History key {Key} has {Count} items in group", key, group.Count);
// keep the most recent item from each history group that has already been played completely
IEnumerable<PlayoutHistory> toDelete = group
Option<DateTime> whenToKeep = group
.Filter(h => h.Finish < start.UtcDateTime)
.OrderByDescending(h => h.When)
.Tail();
.Map(h => h.When)
.HeadOrNone();
foreach (PlayoutHistory delete in toDelete)
foreach (DateTime toKeep in whenToKeep)
{
playout.PlayoutHistory.Remove(delete);
// keep the most recent item from each history group that has already been played completely
IEnumerable<PlayoutHistory> toDelete = group
.Filter(h => h.Finish < start.UtcDateTime)
.Filter(h => h.When != toKeep);
foreach (PlayoutHistory delete in toDelete)
{
playout.PlayoutHistory.Remove(delete);
}
}
}
}

10
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutMarathonHelper.cs

@ -1,14 +1,14 @@ @@ -1,14 +1,14 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectionRepository)
{
public async Task<Option<IMediaCollectionEnumerator>> GetEnumerator(
public async Task<Option<YamlMarathonContentResult>> GetEnumerator(
YamlPlayoutContentMarathonItem marathon,
CollectionEnumeratorState state,
CancellationToken cancellationToken)
@ -64,12 +64,16 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio @@ -64,12 +64,16 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
itemMap.Add(playlistItem, group.ToList());
}
return await PlaylistEnumerator.Create(
PlaylistEnumerator enumerator = await PlaylistEnumerator.Create(
mediaCollectionRepository,
itemMap,
state,
marathon.ShuffleGroups,
cancellationToken);
return new YamlMarathonContentResult(
enumerator,
itemMap.ToImmutableDictionary(x => CollectionKey.ForPlaylistItem(x.Key), x => x.Value));
}
private static GroupKey MediaItemKeyByShow(MediaItem mediaItem) =>

5902
ErsatzTV.Infrastructure.MySql/Migrations/20250630151214_Add_PlayoutHistoryChildKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250630151214_Add_PlayoutHistoryChildKey.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutHistoryChildKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ChildKey",
table: "PlayoutHistory",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ChildKey",
table: "PlayoutHistory");
}
}
}

5905
ErsatzTV.Infrastructure.MySql/Migrations/20250630185129_Add_PlayoutHistoryIsCurrentChild.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250630185129_Add_PlayoutHistoryIsCurrentChild.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutHistoryIsCurrentChild : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsCurrentChild",
table: "PlayoutHistory",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsCurrentChild",
table: "PlayoutHistory");
}
}
}

6
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -2476,6 +2476,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2476,6 +2476,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("BlockId")
.HasColumnType("int");
b.Property<string>("ChildKey")
.HasColumnType("longtext");
b.Property<string>("Details")
.HasColumnType("longtext");
@ -2485,6 +2488,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2485,6 +2488,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("Index")
.HasColumnType("int");
b.Property<bool>("IsCurrentChild")
.HasColumnType("tinyint(1)");
b.Property<string>("Key")
.HasColumnType("longtext");

5741
ErsatzTV.Infrastructure.Sqlite/Migrations/20250630151239_Add_PlayoutHistoryChildKey.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20250630151239_Add_PlayoutHistoryChildKey.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutHistoryChildKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ChildKey",
table: "PlayoutHistory",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ChildKey",
table: "PlayoutHistory");
}
}
}

5744
ErsatzTV.Infrastructure.Sqlite/Migrations/20250630185058_Add_PlayoutHistoryIsCurrentChild.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20250630185058_Add_PlayoutHistoryIsCurrentChild.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutHistoryIsCurrentChild : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsCurrentChild",
table: "PlayoutHistory",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsCurrentChild",
table: "PlayoutHistory");
}
}
}

6
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -2351,6 +2351,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2351,6 +2351,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("BlockId")
.HasColumnType("INTEGER");
b.Property<string>("ChildKey")
.HasColumnType("TEXT");
b.Property<string>("Details")
.HasColumnType("TEXT");
@ -2360,6 +2363,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2360,6 +2363,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<bool>("IsCurrentChild")
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");

Loading…
Cancel
Save