Browse Source

add duration discard to fill attempts (#1301)

pull/1302/head
Jason Dove 2 years ago committed by GitHub
parent
commit
5da2bdbab4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  3. 1
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  4. 6
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  5. 1
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  6. 2
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  7. 1
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  8. 3
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  9. 1
      ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs
  10. 11
      ErsatzTV.Core/Extensions/MediaItemExtensions.cs
  11. 1
      ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs
  12. 6
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  13. 6
      ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs
  14. 152
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  15. 5
      ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs
  16. 6
      ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs
  17. 5
      ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs
  18. 5
      ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs
  19. 4419
      ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.Designer.cs
  20. 29
      ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.cs
  21. 5
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  22. 5
      ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs
  23. 2
      ErsatzTV/Pages/Artist.razor
  24. 3
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  25. 2
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  26. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  27. 6
      ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs
  28. 8
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

4
CHANGELOG.md

@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

1
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

@ -17,6 +17,7 @@ public record AddProgramScheduleItem( @@ -17,6 +17,7 @@ public record AddProgramScheduleItem(
int? MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
int? DiscardToFillAttempts,
string CustomTitle,
GuideMode GuideMode,
int? PreRollFillerId,

1
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

@ -15,6 +15,7 @@ public interface IProgramScheduleItemRequest @@ -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; }

6
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -83,6 +83,11 @@ public abstract class ProgramScheduleItemCommandBase @@ -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 @@ -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,

1
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

@ -17,6 +17,7 @@ public record ReplaceProgramScheduleItem( @@ -17,6 +17,7 @@ public record ReplaceProgramScheduleItem(
int? MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
int? DiscardToFillAttempts,
string CustomTitle,
GuideMode GuideMode,
int? PreRollFillerId,

2
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs

@ -29,7 +29,7 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase @@ -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(

1
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -42,6 +42,7 @@ internal static class Mapper @@ -42,6 +42,7 @@ internal static class Mapper
duration.PlaybackOrder,
duration.PlayoutDuration,
duration.TailMode,
duration.DiscardToFillAttempts,
duration.CustomTitle,
duration.GuideMode,
duration.PreRollFiller != null

3
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -21,6 +21,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode @@ -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 @@ -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; }
}

1
ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs

@ -4,4 +4,5 @@ public class ProgramScheduleItemDuration : ProgramScheduleItem @@ -4,4 +4,5 @@ public class ProgramScheduleItemDuration : ProgramScheduleItem
{
public TimeSpan PlayoutDuration { get; set; }
public TailMode TailMode { get; set; }
public int DiscardToFillAttempts { get; set; }
}

11
ErsatzTV.Core/Extensions/MediaItemExtensions.cs

@ -7,6 +7,17 @@ namespace ErsatzTV.Core.Extensions; @@ -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
{

1
ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs

@ -8,4 +8,5 @@ public interface IMediaCollectionEnumerator @@ -8,4 +8,5 @@ public interface IMediaCollectionEnumerator
Option<MediaItem> Current { get; }
void MoveNext();
Option<MediaItem> Peek(int offset);
Option<TimeSpan> MinimumDuration { get; }
}

6
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -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; @@ -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 @@ -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;
}

6
ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -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; @@ -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 @@ -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 @@ -33,4 +37,6 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
public Option<MediaItem> Peek(int offset) =>
throw new NotSupportedException();
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
}

152
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -26,6 +26,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche @@ -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 @@ -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();
}
}
}

5
ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -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; @@ -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 @@ -35,4 +38,6 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
public Option<MediaItem> Peek(int offset) =>
throw new NotSupportedException();
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
}

6
ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -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; @@ -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 @@ -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;
}

5
ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -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 @@ -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 @@ -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 @@ -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;
}

5
ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -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 @@ -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 @@ -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 @@ -105,4 +108,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
return GroupedMediaItem.FlattenGroups(copy, _mediaItemCount);
}
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
}

4419
ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.cs

@ -0,0 +1,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");
}
}
}

5
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -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 @@ -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");

5
ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -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 @@ -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 @@ -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 @@ -197,4 +200,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
return copy;
}
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
}

2
ErsatzTV/Pages/Artist.razor

@ -258,7 +258,7 @@ @@ -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");
}
}

3
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -228,6 +228,7 @@ @@ -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 @@ @@ -489,6 +490,7 @@
case ProgramScheduleItemDurationViewModel duration:
result.PlayoutDuration = duration.PlayoutDuration;
result.TailMode = duration.TailMode;
result.DiscardToFillAttempts = duration.DiscardToFillAttempts;
break;
}
@ -546,6 +548,7 @@ @@ -546,6 +548,7 @@
item.MultipleCount,
item.PlayoutDuration,
item.TailMode,
item.DiscardToFillAttempts,
item.CustomTitle,
item.GuideMode,
item.PreRollFiller?.Id,

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -241,7 +241,7 @@ @@ -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");
}
}

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -228,7 +228,7 @@ @@ -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");
}
}

6
ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs

@ -16,6 +16,10 @@ public class ProgramScheduleItemEditViewModelValidator : AbstractValidator<Progr @@ -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);
});
}
}

8
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -13,6 +13,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged @@ -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 @@ -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; }

Loading…
Cancel
Save