add duration discard to fill attempts (#1301)
This commit is contained in:
@@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
### Added
|
||||
- Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted)
|
||||
- Automatically reload playout details table when playout build is complete
|
||||
- Add `Discard To Fill Attempts` setting to duration playout mode
|
||||
- This setting only has an effect when it's configured to be greater than zero
|
||||
- When the current item is longer than the remaining duration, it will be discarded and ETV will try to fit the next item in the collection, up to the configured number of times
|
||||
- When the remaining duration is shorter than all items in the collection, the normal filler logic will be used
|
||||
|
||||
### Fixed
|
||||
- Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item
|
||||
|
||||
@@ -17,6 +17,7 @@ public record AddProgramScheduleItem(
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
TailMode TailMode,
|
||||
int? DiscardToFillAttempts,
|
||||
string CustomTitle,
|
||||
GuideMode GuideMode,
|
||||
int? PreRollFillerId,
|
||||
|
||||
@@ -15,6 +15,7 @@ public interface IProgramScheduleItemRequest
|
||||
int? MultipleCount { get; }
|
||||
TimeSpan? PlayoutDuration { get; }
|
||||
TailMode TailMode { get; }
|
||||
int? DiscardToFillAttempts { get; }
|
||||
string CustomTitle { get; }
|
||||
GuideMode GuideMode { get; }
|
||||
int? PreRollFillerId { get; }
|
||||
|
||||
@@ -83,6 +83,11 @@ public abstract class ProgramScheduleItemCommandBase
|
||||
return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'");
|
||||
}
|
||||
|
||||
if (item.DiscardToFillAttempts is null)
|
||||
{
|
||||
return BaseError.New("[DiscardToFillAttempts] is required for playout mode 'duration'");
|
||||
}
|
||||
|
||||
if (item.TailMode == TailMode.Filler && item.TailFillerId == null)
|
||||
{
|
||||
return BaseError.New("Tail Filler is required with tail mode Filler");
|
||||
@@ -248,6 +253,7 @@ public abstract class ProgramScheduleItemCommandBase
|
||||
PlaybackOrder = item.PlaybackOrder,
|
||||
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
|
||||
TailMode = item.TailMode,
|
||||
DiscardToFillAttempts = item.DiscardToFillAttempts.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle,
|
||||
GuideMode = item.GuideMode,
|
||||
PreRollFillerId = item.PreRollFillerId,
|
||||
|
||||
@@ -17,6 +17,7 @@ public record ReplaceProgramScheduleItem(
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
TailMode TailMode,
|
||||
int? DiscardToFillAttempts,
|
||||
string CustomTitle,
|
||||
GuideMode GuideMode,
|
||||
int? PreRollFillerId,
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
|
||||
return await LanguageExtensions.Apply(validation, ps => PersistItems(dbContext, request, ps));
|
||||
return await validation.Apply(ps => PersistItems(dbContext, request, ps));
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ProgramScheduleItemViewModel>> PersistItems(
|
||||
|
||||
@@ -42,6 +42,7 @@ internal static class Mapper
|
||||
duration.PlaybackOrder,
|
||||
duration.PlayoutDuration,
|
||||
duration.TailMode,
|
||||
duration.DiscardToFillAttempts,
|
||||
duration.CustomTitle,
|
||||
duration.GuideMode,
|
||||
duration.PreRollFiller != null
|
||||
|
||||
@@ -21,6 +21,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
|
||||
PlaybackOrder playbackOrder,
|
||||
TimeSpan playoutDuration,
|
||||
TailMode tailMode,
|
||||
int discardToFillAttempts,
|
||||
string customTitle,
|
||||
GuideMode guideMode,
|
||||
FillerPresetViewModel preRollFiller,
|
||||
@@ -59,8 +60,10 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
|
||||
{
|
||||
PlayoutDuration = playoutDuration;
|
||||
TailMode = tailMode;
|
||||
DiscardToFillAttempts = discardToFillAttempts;
|
||||
}
|
||||
|
||||
public TimeSpan PlayoutDuration { get; }
|
||||
public TailMode TailMode { get; }
|
||||
public int DiscardToFillAttempts { get; }
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ public class ProgramScheduleItemDuration : ProgramScheduleItem
|
||||
{
|
||||
public TimeSpan PlayoutDuration { get; set; }
|
||||
public TailMode TailMode { get; set; }
|
||||
public int DiscardToFillAttempts { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ namespace ErsatzTV.Core.Extensions;
|
||||
|
||||
public static class MediaItemExtensions
|
||||
{
|
||||
public static Option<TimeSpan> GetDuration(this MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.HeadOrNone().Map(v => v.Duration),
|
||||
Episode e => e.MediaVersions.HeadOrNone().Map(v => v.Duration),
|
||||
MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration),
|
||||
OtherVideo ov => ov.MediaVersions.HeadOrNone().Map(v => v.Duration),
|
||||
Song s => s.MediaVersions.HeadOrNone().Map(v => v.Duration),
|
||||
_ => None
|
||||
};
|
||||
|
||||
public static MediaVersion GetHeadVersion(this MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
{
|
||||
|
||||
@@ -8,4 +8,5 @@ public interface IMediaCollectionEnumerator
|
||||
Option<MediaItem> Current { get; }
|
||||
void MoveNext();
|
||||
Option<MediaItem> Peek(int offset);
|
||||
Option<TimeSpan> MinimumDuration { get; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
@@ -6,12 +7,15 @@ namespace ErsatzTV.Core.Scheduling;
|
||||
public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly IList<MediaItem> _sortedMediaItems;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
|
||||
public ChronologicalMediaCollectionEnumerator(
|
||||
IEnumerable<MediaItem> mediaItems,
|
||||
CollectionEnumeratorState state)
|
||||
{
|
||||
_sortedMediaItems = mediaItems.OrderBy(identity, new ChronologicalMediaComparer()).ToList();
|
||||
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(
|
||||
() => _sortedMediaItems.Bind(i => i.GetDuration()).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
|
||||
@@ -35,4 +39,6 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
@@ -6,6 +7,7 @@ namespace ErsatzTV.Core.Scheduling;
|
||||
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly IList<MediaItem> _sortedMediaItems;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
|
||||
public CustomOrderCollectionEnumerator(
|
||||
Collection collection,
|
||||
@@ -17,6 +19,8 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
.OrderBy(ci => ci.CustomIndex)
|
||||
.Map(ci => mediaItems.First(mi => mi.Id == ci.MediaItemId))
|
||||
.ToList();
|
||||
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(
|
||||
() => _sortedMediaItems.Bind(i => i.GetDuration()).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
@@ -33,4 +37,6 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
|
||||
|
||||
var willFinishInTime = true;
|
||||
Option<DateTimeOffset> durationUntil = None;
|
||||
int discardAttempts = 0;
|
||||
|
||||
IMediaCollectionEnumerator contentEnumerator =
|
||||
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
|
||||
@@ -68,77 +69,108 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
|
||||
continue;
|
||||
}
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
TimeSpan remainingDuration = durationUntil.ValueUnsafe() - itemStartTime;
|
||||
if (scheduleItem.DiscardToFillAttempts > 0 &&
|
||||
remainingDuration >= contentEnumerator.MinimumDuration.IfNone(TimeSpan.Zero) &&
|
||||
itemDuration > remainingDuration)
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = nextState.NextGuideGroup,
|
||||
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
|
||||
? FillerKind.GuideMode
|
||||
: FillerKind.None,
|
||||
CustomTitle = scheduleItem.CustomTitle,
|
||||
WatermarkId = scheduleItem.WatermarkId,
|
||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = scheduleItem.SubtitleMode
|
||||
};
|
||||
|
||||
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
||||
|
||||
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
willFinishInTime = itemStartTime > durationFinish ||
|
||||
itemEndTimeWithFiller <= durationFinish;
|
||||
if (willFinishInTime)
|
||||
{
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
playoutItems.AddRange(
|
||||
AddFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
cancellationToken));
|
||||
|
||||
nextState = nextState with
|
||||
discardAttempts++;
|
||||
if (discardAttempts > scheduleItem.DiscardToFillAttempts)
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
nextState = nextState with
|
||||
{
|
||||
DurationFinish = None
|
||||
};
|
||||
|
||||
// only bump guide group if we don't have a custom title
|
||||
NextGuideGroup = string.IsNullOrWhiteSpace(scheduleItem.CustomTitle)
|
||||
? nextState.IncrementGuideGroup
|
||||
: nextState.NextGuideGroup
|
||||
};
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping playout item {Title} with duration {Duration} that is longer than remaining duration {RemainingDuration}",
|
||||
PlayoutBuilder.DisplayTitle(mediaItem),
|
||||
itemDuration,
|
||||
remainingDuration);
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
contentEnumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeSpan durationBlock = itemEndTimeWithFiller - itemStartTime;
|
||||
if (itemEndTimeWithFiller - itemStartTime > scheduleItem.PlayoutDuration)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to schedule duration block of {DurationBlock} which is longer than the configured playout duration {PlayoutDuration}",
|
||||
durationBlock,
|
||||
scheduleItem.PlayoutDuration);
|
||||
}
|
||||
discardAttempts = 0;
|
||||
|
||||
nextState = nextState with
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
DurationFinish = None
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = nextState.NextGuideGroup,
|
||||
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
|
||||
? FillerKind.GuideMode
|
||||
: FillerKind.None,
|
||||
CustomTitle = scheduleItem.CustomTitle,
|
||||
WatermarkId = scheduleItem.WatermarkId,
|
||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = scheduleItem.SubtitleMode
|
||||
};
|
||||
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
||||
|
||||
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
willFinishInTime = itemStartTime > durationFinish ||
|
||||
itemEndTimeWithFiller <= durationFinish;
|
||||
if (willFinishInTime)
|
||||
{
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
playoutItems.AddRange(
|
||||
AddFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItem,
|
||||
itemChapters,
|
||||
cancellationToken));
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
|
||||
// only bump guide group if we don't have a custom title
|
||||
NextGuideGroup = string.IsNullOrWhiteSpace(scheduleItem.CustomTitle)
|
||||
? nextState.IncrementGuideGroup
|
||||
: nextState.NextGuideGroup
|
||||
};
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeSpan durationBlock = itemEndTimeWithFiller - itemStartTime;
|
||||
if (itemEndTimeWithFiller - itemStartTime > scheduleItem.PlayoutDuration)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to schedule duration block of {DurationBlock} which is longer than the configured playout duration {PlayoutDuration}",
|
||||
durationBlock,
|
||||
scheduleItem.PlayoutDuration);
|
||||
}
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
DurationFinish = None
|
||||
};
|
||||
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
@@ -6,12 +7,14 @@ namespace ErsatzTV.Core.Scheduling;
|
||||
public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly IList<MediaItem> _mediaItems;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
private readonly Random _random;
|
||||
private int _index;
|
||||
|
||||
public RandomizedMediaCollectionEnumerator(IList<MediaItem> mediaItems, CollectionEnumeratorState state)
|
||||
{
|
||||
_mediaItems = mediaItems;
|
||||
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(() => _mediaItems.Bind(i => i.GetDuration()).HeadOrNone());
|
||||
_random = new Random(state.Seed);
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
@@ -35,4 +38,6 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
@@ -6,12 +7,15 @@ namespace ErsatzTV.Core.Scheduling;
|
||||
public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly IList<MediaItem> _sortedMediaItems;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
|
||||
public SeasonEpisodeMediaCollectionEnumerator(
|
||||
IEnumerable<MediaItem> mediaItems,
|
||||
CollectionEnumeratorState state)
|
||||
{
|
||||
_sortedMediaItems = mediaItems.OrderBy(identity, new SeasonEpisodeMediaComparer()).ToList();
|
||||
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(
|
||||
() => _sortedMediaItems.Bind(i => i.GetDuration()).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
|
||||
@@ -35,4 +39,6 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
@@ -11,6 +12,7 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
private readonly bool _randomStartPoint;
|
||||
private Random _random;
|
||||
private IList<MediaItem> _shuffled;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
|
||||
public ShuffleInOrderCollectionEnumerator(
|
||||
IList<CollectionWithItems> collections,
|
||||
@@ -31,6 +33,7 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
|
||||
_random = new Random(state.Seed);
|
||||
_shuffled = Shuffle(_collections, _random);
|
||||
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(() => _shuffled.Bind(i => i.GetDuration()).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
@@ -203,4 +206,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
public int Index { get; set; }
|
||||
public IList<Option<MediaItem>> Items { get; set; }
|
||||
}
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
@@ -8,6 +9,7 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private readonly int _mediaItemCount;
|
||||
private readonly IList<GroupedMediaItem> _mediaItems;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
private CloneableRandom _random;
|
||||
private IList<MediaItem> _shuffled;
|
||||
|
||||
@@ -28,6 +30,7 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
_shuffled = Shuffle(_mediaItems, _random);
|
||||
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(() => _shuffled.Bind(i => i.GetDuration()).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
@@ -105,4 +108,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
|
||||
return GroupedMediaItem.FlattenGroups(copy, _mediaItemCount);
|
||||
}
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
}
|
||||
|
||||
+4419
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_ProgramScheduleItemDuration_DiscardToFillAttempts : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DiscardToFillAttempts",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DiscardToFillAttempts",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
|
||||
{
|
||||
@@ -2503,6 +2503,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.Property<int>("DiscardToFillAttempts")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeSpan>("PlayoutDuration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Scripting;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
@@ -15,6 +16,7 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
||||
private readonly List<MediaItem> _ungrouped;
|
||||
private CloneableRandom _random;
|
||||
private IList<MediaItem> _shuffled;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
|
||||
public MultiEpisodeShuffleCollectionEnumerator(
|
||||
IList<MediaItem> mediaItems,
|
||||
@@ -72,6 +74,7 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
||||
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
_shuffled = Shuffle(_random);
|
||||
_lazyMinimumDuration = new Lazy<Option<TimeSpan>>(() => _shuffled.Bind(i => i.GetDuration()).HeadOrNone());
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
@@ -197,4 +200,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule)
|
||||
{
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
|
||||
_navigationManager.NavigateTo($"schedules/{schedule.Id}/items");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@
|
||||
<MudSelectItem Value="@TailMode.Offline">Offline</MudSelectItem>
|
||||
<MudSelectItem Value="@TailMode.Filler">Filler</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudTextField Class="mt-3" Label="Discard To Fill Attempts" @bind-Value="@_selectedItem.DiscardToFillAttempts" For="@(() => _selectedItem.DiscardToFillAttempts)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
||||
<MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/>
|
||||
<MudSelect Class="mt-3" Label="Guide Mode" @bind-Value="@_selectedItem.GuideMode" For="@(() => _selectedItem.GuideMode)">
|
||||
<MudSelectItem Value="@GuideMode.Normal">Normal</MudSelectItem>
|
||||
@@ -489,6 +490,7 @@
|
||||
case ProgramScheduleItemDurationViewModel duration:
|
||||
result.PlayoutDuration = duration.PlayoutDuration;
|
||||
result.TailMode = duration.TailMode;
|
||||
result.DiscardToFillAttempts = duration.DiscardToFillAttempts;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -546,6 +548,7 @@
|
||||
item.MultipleCount,
|
||||
item.PlayoutDuration,
|
||||
item.TailMode,
|
||||
item.DiscardToFillAttempts,
|
||||
item.CustomTitle,
|
||||
item.GuideMode,
|
||||
item.PreRollFiller?.Id,
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule)
|
||||
{
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
|
||||
_navigationManager.NavigateTo($"schedules/{schedule.Id}/items");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule)
|
||||
{
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token);
|
||||
_navigationManager.NavigateTo($"schedules/{schedule.Id}/items");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ public class ProgramScheduleItemEditViewModelValidator : AbstractValidator<Progr
|
||||
() => RuleFor(i => i.MultipleCount).NotNull().GreaterThanOrEqualTo(0));
|
||||
When(
|
||||
i => i.PlayoutMode == PlayoutMode.Duration,
|
||||
() => RuleFor(i => i.PlayoutDuration).NotNull());
|
||||
() =>
|
||||
{
|
||||
RuleFor(i => i.PlayoutDuration).NotNull();
|
||||
RuleFor(i => i.DiscardToFillAttempts).NotNull().GreaterThanOrEqualTo(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private ProgramScheduleItemCollectionType _collectionType;
|
||||
private int? _multipleCount;
|
||||
private int? _discardToFillAttempts;
|
||||
private TimeSpan? _playoutDuration;
|
||||
private TimeSpan? _startTime;
|
||||
|
||||
@@ -96,7 +97,12 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
|
||||
}
|
||||
|
||||
public TailMode TailMode { get; set; }
|
||||
|
||||
|
||||
public int? DiscardToFillAttempts
|
||||
{
|
||||
get => PlayoutMode == PlayoutMode.Duration ? _discardToFillAttempts ?? 0 : null;
|
||||
set => _discardToFillAttempts = value;
|
||||
}
|
||||
public string CustomTitle { get; set; }
|
||||
|
||||
public GuideMode GuideMode { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user