mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
8.6 KiB
225 lines
8.6 KiB
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Domain.Filler; |
|
using ErsatzTV.Core.Domain.Scheduling; |
|
using ErsatzTV.Core.Extensions; |
|
using ErsatzTV.Core.Interfaces.Repositories; |
|
using ErsatzTV.Core.Interfaces.Scheduling; |
|
using Microsoft.Extensions.Logging; |
|
using Newtonsoft.Json; |
|
|
|
namespace ErsatzTV.Core.Scheduling.BlockScheduling; |
|
|
|
public class BlockPlayoutFillerBuilder( |
|
IMediaCollectionRepository mediaCollectionRepository, |
|
ITelevisionRepository televisionRepository, |
|
IArtistRepository artistRepository, |
|
ILogger<BlockPlayoutFillerBuilder> logger) : IBlockPlayoutFillerBuilder |
|
{ |
|
private static readonly JsonSerializerSettings JsonSettings = new() |
|
{ |
|
NullValueHandling = NullValueHandling.Ignore |
|
}; |
|
|
|
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) |
|
{ |
|
if (mode is PlayoutBuildMode.Reset) |
|
{ |
|
// remove all playout items with type filler |
|
// except block items that are hidden from the guide (guide mode) |
|
var toRemove = playout.Items |
|
.Where(pi => pi.FillerKind is not FillerKind.None and not FillerKind.GuideMode) |
|
.ToList(); |
|
foreach (PlayoutItem playoutItem in toRemove) |
|
{ |
|
BlockPlayoutChangeDetection.RemoveItemAndHistory(playout, playoutItem); |
|
} |
|
} |
|
|
|
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>(); |
|
|
|
// find all unscheduled periods |
|
var queue = new Queue<PlayoutItem>(playout.Items); |
|
while (queue.Count > 1) |
|
{ |
|
PlayoutItem one = queue.Dequeue(); |
|
PlayoutItem two = queue.Peek(); |
|
|
|
DateTimeOffset start = one.FinishOffset; |
|
DateTimeOffset finish = two.StartOffset; |
|
|
|
if (start == finish) |
|
{ |
|
continue; |
|
} |
|
|
|
// find applicable deco |
|
foreach (Deco deco in GetDecoFor(playout, start)) |
|
{ |
|
if (!HasDefaultFiller(deco)) |
|
continue; |
|
|
|
var collectionKey = CollectionKey.ForDecoDefaultFiller(deco); |
|
string historyKey = HistoryDetails.ForDefaultFiller(deco); |
|
|
|
// load collection items from db on demand |
|
if (!collectionEnumerators.TryGetValue(collectionKey, out IMediaCollectionEnumerator enumerator)) |
|
{ |
|
List<MediaItem> collectionItems = await MediaItemsForCollection.Collect( |
|
mediaCollectionRepository, |
|
televisionRepository, |
|
artistRepository, |
|
collectionKey); |
|
|
|
enumerator = BlockPlayoutEnumerator.Shuffle( |
|
collectionItems, |
|
start, |
|
playout, |
|
deco, |
|
historyKey); |
|
|
|
if (enumerator.Count == 0) |
|
{ |
|
logger.LogWarning( |
|
"Block filler contains empty collection {@Key}; no filler will be scheduled", |
|
collectionKey); |
|
} |
|
|
|
collectionEnumerators.Add(collectionKey, enumerator); |
|
} |
|
|
|
// skip this deco if the collection has no items |
|
if (enumerator.Count == 0) |
|
continue; |
|
|
|
DateTimeOffset current = start; |
|
var pastTime = false; |
|
while (current < finish) |
|
{ |
|
foreach (MediaItem mediaItem in enumerator.Current) |
|
{ |
|
TimeSpan itemDuration = DurationForMediaItem(mediaItem); |
|
|
|
// add filler from deco to unscheduled period |
|
var filler = new PlayoutItem |
|
{ |
|
MediaItemId = mediaItem.Id, |
|
Start = current.UtcDateTime, |
|
Finish = current.UtcDateTime + itemDuration, |
|
InPoint = TimeSpan.Zero, |
|
OutPoint = itemDuration, |
|
|
|
// FillerKind.Fallback will loop and avoid hw accel, so don't use that |
|
FillerKind = FillerKind.DecoDefault, |
|
|
|
CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings), |
|
GuideStart = one.GuideStart, |
|
GuideFinish = one.GuideFinish, |
|
GuideGroup = one.GuideGroup |
|
}; |
|
|
|
if (filler.FinishOffset > finish) |
|
{ |
|
if (deco.DefaultFillerTrimToFit) |
|
{ |
|
filler.Finish = finish.UtcDateTime; |
|
filler.OutPoint = filler.Finish - filler.Start; |
|
} |
|
else |
|
{ |
|
pastTime = true; |
|
break; |
|
} |
|
} |
|
|
|
playout.Items.Add(filler); |
|
|
|
// create a playout history record |
|
var nextHistory = new PlayoutHistory |
|
{ |
|
PlayoutId = playout.Id, |
|
PlaybackOrder = PlaybackOrder.Shuffle, |
|
Index = enumerator.State.Index, |
|
When = current.UtcDateTime, |
|
Key = historyKey, |
|
Details = HistoryDetails.ForMediaItem(mediaItem) |
|
}; |
|
|
|
playout.PlayoutHistory.Add(nextHistory); |
|
|
|
current += itemDuration; |
|
enumerator.MoveNext(); |
|
} |
|
|
|
if (pastTime) |
|
{ |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
return playout; |
|
} |
|
|
|
private static Option<Deco> GetDecoFor(Playout playout, DateTimeOffset start) |
|
{ |
|
Option<PlayoutTemplate> maybeTemplate = PlayoutTemplateSelector.GetPlayoutTemplateFor(playout.Templates, start); |
|
foreach (PlayoutTemplate template in maybeTemplate) |
|
{ |
|
if (template.DecoTemplate is not null) |
|
{ |
|
foreach (DecoTemplateItem decoTemplateItem in template.DecoTemplate.Items) |
|
{ |
|
if (decoTemplateItem.StartTime <= start.TimeOfDay && decoTemplateItem.EndTime == TimeSpan.Zero || decoTemplateItem.EndTime > start.TimeOfDay) |
|
{ |
|
switch (decoTemplateItem.Deco.DefaultFillerMode) |
|
{ |
|
case DecoMode.Inherit: |
|
return Optional(playout.Deco); |
|
case DecoMode.Override: |
|
return decoTemplateItem.Deco; |
|
case DecoMode.Disable: |
|
default: |
|
return Option<Deco>.None; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
return Optional(playout.Deco); |
|
} |
|
|
|
private static bool HasDefaultFiller(Deco deco) |
|
{ |
|
switch (deco.DefaultFillerCollectionType) |
|
{ |
|
case ProgramScheduleItemCollectionType.Collection: |
|
return deco.DefaultFillerCollectionId.HasValue; |
|
case ProgramScheduleItemCollectionType.TelevisionShow: |
|
return deco.DefaultFillerMediaItemId.HasValue; |
|
case ProgramScheduleItemCollectionType.TelevisionSeason: |
|
return deco.DefaultFillerMediaItemId.HasValue; |
|
case ProgramScheduleItemCollectionType.Artist: |
|
return deco.DefaultFillerMediaItemId.HasValue; |
|
case ProgramScheduleItemCollectionType.MultiCollection: |
|
return deco.DefaultFillerMultiCollectionId.HasValue; |
|
case ProgramScheduleItemCollectionType.SmartCollection: |
|
return deco.DefaultFillerSmartCollectionId.HasValue; |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
private static TimeSpan DurationForMediaItem(MediaItem mediaItem) |
|
{ |
|
if (mediaItem is Image image) |
|
{ |
|
return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); |
|
} |
|
|
|
MediaVersion version = mediaItem.GetHeadVersion(); |
|
return version.Duration; |
|
} |
|
}
|
|
|