Browse Source

add filler option to duration playout mode (#428)

* add duration tail options to schedule items editor

* add naive filler scheduling

* fix duration item length in xmltv

* show offline image for unfilled duration tail

* fix tests

* update changelog

* update dependencies
pull/429/head
Jason Dove 4 years ago committed by GitHub
parent
commit
22da19845b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      CHANGELOG.md
  2. 7
      ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsById.cs
  3. 12
      ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs
  4. 7
      ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsById.cs
  5. 7
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  6. 7
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  7. 12
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  8. 7
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  9. 19
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  10. 23
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  11. 4
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs
  12. 10
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  13. 1
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  14. 8
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  15. 1
      ErsatzTV.Core/Domain/PlayoutAnchor.cs
  16. 5
      ErsatzTV.Core/Domain/PlayoutItem.cs
  17. 11
      ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs
  18. 10
      ErsatzTV.Core/Domain/TailMode.cs
  19. 16
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  20. 29
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  21. 26
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  22. 14
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  23. 171
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  24. 22
      ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemDurationConfiguration.cs
  25. 6
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  26. 3343
      ErsatzTV.Infrastructure/Migrations/20211013012823_Add_DurationTail.Designer.cs
  27. 158
      ErsatzTV.Infrastructure/Migrations/20211013012823_Add_DurationTail.cs
  28. 3346
      ErsatzTV.Infrastructure/Migrations/20211013025333_Add_PlayoutAnchor_InDurationFiller.Designer.cs
  29. 23
      ErsatzTV.Infrastructure/Migrations/20211013025333_Add_PlayoutAnchor_InDurationFiller.cs
  30. 3349
      ErsatzTV.Infrastructure/Migrations/20211013230405_Add_PlayoutItem_IsFiller.Designer.cs
  31. 24
      ErsatzTV.Infrastructure/Migrations/20211013230405_Add_PlayoutItem_IsFiller.cs
  32. 3352
      ErsatzTV.Infrastructure/Migrations/20211013231447_Add_PlayoutItem_GuideFinish.Designer.cs
  33. 24
      ErsatzTV.Infrastructure/Migrations/20211013231447_Add_PlayoutItem_GuideFinish.cs
  34. 65
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  35. 4
      ErsatzTV/ErsatzTV.csproj
  36. 2
      ErsatzTV/Pages/Artist.razor
  37. 2
      ErsatzTV/Pages/Playouts.razor
  38. 94
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  39. 2
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  40. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  41. 33
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

10
CHANGELOG.md

@ -4,10 +4,18 @@ All notable changes to this project will be documented in this file. @@ -4,10 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Fix error message/offline continuity with channels that use HLS Segmenter
### Added
- Add filler `Tail Mode` option to `Duration` playout mode (in addition to existing `Offline` option)
- Filler collection will always be randomized (to fill as much time as possible)
- Filler will be hidden from channel guide, but visible in playout details in ErsatzTV
- Unfilled time will show offline image
## [0.1.3-alpha] - 2021-10-12
### Fixed
- Fix startup bug for nvidia docker installations
- Fix startup bug for some docker installations
## [0.1.2-alpha] - 2021-10-12
### Added

7
ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Playouts.Queries
{
public record GetFuturePlayoutItemsById(int PlayoutId, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
}

12
ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsByIdHandler.cs → ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -11,21 +12,23 @@ using static ErsatzTV.Application.Playouts.Mapper; @@ -11,21 +12,23 @@ using static ErsatzTV.Application.Playouts.Mapper;
namespace ErsatzTV.Application.Playouts.Queries
{
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, PagedPlayoutItemsViewModel>
public class GetFuturePlayoutItemsByIdHandler : IRequestHandler<GetFuturePlayoutItemsById, PagedPlayoutItemsViewModel>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
public GetFuturePlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<PagedPlayoutItemsViewModel> Handle(
GetPlayoutItemsById request,
GetFuturePlayoutItemsById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
int totalCount = await dbContext.PlayoutItems
.CountAsync(i => i.PlayoutId == request.PlayoutId, cancellationToken);
DateTime now = DateTimeOffset.Now.UtcDateTime;
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
.Include(i => i.MediaItem)
@ -50,6 +53,7 @@ namespace ErsatzTV.Application.Playouts.Queries @@ -50,6 +53,7 @@ namespace ErsatzTV.Application.Playouts.Queries
.ThenInclude(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Filter(i => i.PlayoutId == request.PlayoutId)
.Filter(i => i.Finish >= now)
.OrderBy(i => i.Start)
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)

7
ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsById.cs

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Playouts.Queries
{
public record GetPlayoutItemsById(int PlayoutId, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
}

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

@ -19,6 +19,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -19,6 +19,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
PlaybackOrder PlaybackOrder,
int? MultipleCount,
TimeSpan? PlayoutDuration,
bool? OfflineTail,
TailMode TailMode,
ProgramScheduleItemCollectionType TailCollectionType,
int? TailCollectionId,
int? TailMultiCollectionId,
int? TailSmartCollectionId,
int? TailMediaItemId,
string CustomTitle) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
}

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

@ -15,7 +15,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -15,7 +15,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
PlaybackOrder PlaybackOrder { get; }
int? MultipleCount { get; }
TimeSpan? PlayoutDuration { get; }
bool? OfflineTail { get; }
TailMode TailMode { get; }
ProgramScheduleItemCollectionType TailCollectionType { get; }
int? TailCollectionId { get; }
int? TailMultiCollectionId { get; }
int? TailSmartCollectionId { get; }
int? TailMediaItemId { get; }
string CustomTitle { get; }
}
}

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

@ -55,11 +55,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -55,11 +55,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'");
}
if (item.OfflineTail is null)
{
return BaseError.New("[OfflineTail] is required for playout mode 'duration'");
}
break;
default:
return BaseError.New("[PlayoutMode] is invalid");
@ -181,7 +176,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -181,7 +176,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
MediaItemId = item.MediaItemId,
PlaybackOrder = item.PlaybackOrder,
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
OfflineTail = item.OfflineTail.GetValueOrDefault(),
TailMode = item.TailMode,
TailCollectionType = item.TailCollectionType,
TailCollectionId = item.TailCollectionId,
TailMultiCollectionId = item.TailMultiCollectionId,
TailSmartCollectionId = item.TailSmartCollectionId,
TailMediaItemId = item.TailMediaItemId,
CustomTitle = item.CustomTitle
},
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")

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

@ -20,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -20,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
PlaybackOrder PlaybackOrder,
int? MultipleCount,
TimeSpan? PlayoutDuration,
bool? OfflineTail,
TailMode TailMode,
ProgramScheduleItemCollectionType TailCollectionType,
int? TailCollectionId,
int? TailMultiCollectionId,
int? TailSmartCollectionId,
int? TailMediaItemId,
string CustomTitle) : IProgramScheduleItemRequest;
public record ReplaceProgramScheduleItems

19
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -40,7 +40,24 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -40,7 +40,24 @@ namespace ErsatzTV.Application.ProgramSchedules
},
duration.PlaybackOrder,
duration.PlayoutDuration,
duration.OfflineTail,
duration.TailMode,
duration.TailCollectionType,
duration.TailCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailCollection)
: null,
duration.TailMultiCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailMultiCollection)
: null,
duration.TailSmartCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailSmartCollection)
: null,
duration.TailMediaItem switch
{
Show show => MediaItems.Mapper.ProjectToViewModel(show),
Season season => MediaItems.Mapper.ProjectToViewModel(season),
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
_ => null
},
duration.CustomTitle),
ProgramScheduleItemFlood flood =>
new ProgramScheduleItemFloodViewModel(

23
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -19,7 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -19,7 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules
NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder,
TimeSpan playoutDuration,
bool offlineTail,
TailMode tailMode,
ProgramScheduleItemCollectionType tailCollectionType,
MediaCollectionViewModel tailCollection,
MultiCollectionViewModel tailMultiCollection,
SmartCollectionViewModel tailSmartCollection,
NamedMediaItemViewModel tailMediaItem,
string customTitle) : base(
id,
index,
@ -35,10 +40,22 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -35,10 +40,22 @@ namespace ErsatzTV.Application.ProgramSchedules
customTitle)
{
PlayoutDuration = playoutDuration;
OfflineTail = offlineTail;
TailMode = tailMode;
TailCollectionType = tailCollectionType;
TailCollection = tailCollection;
TailMultiCollection = tailMultiCollection;
TailSmartCollection = tailSmartCollection;
TailMediaItem = tailMediaItem;
}
public TimeSpan PlayoutDuration { get; }
public bool OfflineTail { get; }
public TailMode TailMode { get; }
public ProgramScheduleItemCollectionType TailCollectionType { get; }
public MediaCollectionViewModel TailCollection { get; }
public MultiCollectionViewModel TailMultiCollection { get; }
public SmartCollectionViewModel TailSmartCollection { get; }
public NamedMediaItemViewModel TailMediaItem { get; }
}
}

4
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs

@ -30,6 +30,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries @@ -30,6 +30,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
.Include(i => i.MultiCollection)
.Include(i => i.SmartCollection)
.Include(i => i.MediaItem)
.Include(i => (i as ProgramScheduleItemDuration).TailCollection)
.Include(i => (i as ProgramScheduleItemDuration).TailMultiCollection)
.Include(i => (i as ProgramScheduleItemDuration).TailSmartCollection)
.Include(i => (i as ProgramScheduleItemDuration).TailMediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)

10
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -111,7 +111,6 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -111,7 +111,6 @@ namespace ErsatzTV.Application.Streaming.Queries
maybeGlobalWatermark,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice,
request.StartAtZero,
request.HlsRealtime);
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);
@ -146,7 +145,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -146,7 +145,8 @@ namespace ErsatzTV.Application.Streaming.Queries
ffmpegPath,
channel,
maybeDuration,
"Channel is Offline");
"Channel is Offline",
request.HlsRealtime);
return new PlayoutItemProcessModel(errorProcess, finish);
@ -165,7 +165,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -165,7 +165,8 @@ namespace ErsatzTV.Application.Streaming.Queries
ffmpegPath,
channel,
maybeDuration,
error.Value);
error.Value,
request.HlsRealtime);
return new PlayoutItemProcessModel(errorProcess, finish);
}
@ -183,7 +184,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -183,7 +184,8 @@ namespace ErsatzTV.Application.Streaming.Queries
ffmpegPath,
channel,
maybeDuration,
"Channel is Offline");
"Channel is Offline",
request.HlsRealtime);
return new PlayoutItemProcessModel(errorProcess, finish);
}

1
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -188,7 +188,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -188,7 +188,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg
None,
VaapiDriver.Default,
"/dev/dri/renderD128",
false,
false);
process.StartInfo.RedirectStandardError = true;

8
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -709,7 +709,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -709,7 +709,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = fixedCollection.Id,
StartTime = TimeSpan.FromHours(2),
PlayoutDuration = TimeSpan.FromHours(2),
OfflineTail = false, // immediately continue
TailMode = TailMode.None, // immediately continue
PlaybackOrder = PlaybackOrder.Chronological
}
};
@ -807,7 +807,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -807,7 +807,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = dynamicCollection.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(2),
OfflineTail = false, // immediately continue
TailMode = TailMode.None, // immediately continue
PlaybackOrder = PlaybackOrder.Chronological
}
};
@ -1089,7 +1089,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1089,7 +1089,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
OfflineTail = false,
TailMode = TailMode.None,
PlaybackOrder = PlaybackOrder.Chronological
},
new ProgramScheduleItemDuration
@ -1100,7 +1100,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1100,7 +1100,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionTwo.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
OfflineTail = false,
TailMode = TailMode.None,
PlaybackOrder = PlaybackOrder.Chronological
}
};

1
ErsatzTV.Core/Domain/PlayoutAnchor.cs

@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Domain @@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Domain
public int? MultipleRemaining { get; set; }
public DateTime? DurationFinish { get; set; }
public bool InFlood { get; set; }
public bool InDurationFiller { get; set; }
public DateTimeOffset NextStartOffset => new DateTimeOffset(NextStart, TimeSpan.Zero).ToLocalTime();

5
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -9,12 +9,17 @@ namespace ErsatzTV.Core.Domain @@ -9,12 +9,17 @@ namespace ErsatzTV.Core.Domain
public MediaItem MediaItem { get; set; }
public DateTime Start { get; set; }
public DateTime Finish { get; set; }
public DateTime? GuideFinish { get; set; }
public string CustomTitle { get; set; }
public bool CustomGroup { get; set; }
public bool IsFiller { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
public DateTimeOffset? GuideFinishOffset => GuideFinish.HasValue
? new DateTimeOffset(GuideFinish.Value, TimeSpan.Zero).ToLocalTime()
: null;
}
}

11
ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs

@ -5,6 +5,15 @@ namespace ErsatzTV.Core.Domain @@ -5,6 +5,15 @@ namespace ErsatzTV.Core.Domain
public class ProgramScheduleItemDuration : ProgramScheduleItem
{
public TimeSpan PlayoutDuration { get; set; }
public bool OfflineTail { get; set; }
public TailMode TailMode { get; set; }
public ProgramScheduleItemCollectionType TailCollectionType { get; set; }
public int? TailCollectionId { get; set; }
public Collection TailCollection { get; set; }
public int? TailMediaItemId { get; set; }
public MediaItem TailMediaItem { get; set; }
public int? TailMultiCollectionId { get; set; }
public MultiCollection TailMultiCollection { get; set; }
public int? TailSmartCollectionId { get; set; }
public SmartCollection TailSmartCollection { get; set; }
}
}

10
ErsatzTV.Core/Domain/TailMode.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Domain
{
public enum TailMode
{
None = 0,
Offline = 1,
Slate = 2,
Filler = 3
}
}

16
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -159,14 +159,24 @@ namespace ErsatzTV.Core.FFmpeg @@ -159,14 +159,24 @@ namespace ErsatzTV.Core.FFmpeg
return result;
}
public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile) =>
new()
public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile)
{
string softwareCodec = ffmpegProfile.VideoCodec switch
{
{ } c when c.Contains("hevc") || c.Contains("265") => "libx265",
{ } c when c.Contains("264") => "libx264",
{ } c when c.Contains("mpeg2") => "mpeg2video",
_ => "libx264"
};
return new FFmpegPlaybackSettings
{
ThreadCount = ffmpegProfile.ThreadCount,
FormatFlags = CommonFormatFlags,
VideoCodec = "libx264",
VideoCodec = softwareCodec,
AudioCodec = ffmpegProfile.AudioCodec,
};
}
private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) =>
ffmpegProfile.Transcode && ffmpegProfile.NormalizeVideo &&

29
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -324,22 +324,29 @@ namespace ErsatzTV.Core.FFmpeg @@ -324,22 +324,29 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithHls(string channelNumber, MediaVersion mediaVersion, bool startAtZero)
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion)
{
const int SEGMENT_SECONDS = 4;
if (!int.TryParse(mediaVersion.RFrameRate, out int frameRate))
var frameRate = 24;
foreach (MediaVersion version in mediaVersion)
{
string[] split = (mediaVersion.RFrameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
if (!int.TryParse(version.RFrameRate, out int fr))
{
frameRate = (int)Math.Round(left / (double)right);
}
else
{
_logger.LogInformation("Unable to detect framerate, using {FrameRate}", 24);
frameRate = 24;
string[] split = (version.RFrameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
_logger.LogInformation("Unable to detect framerate, using {FrameRate}", 24);
fr = 24;
}
}
frameRate = fr;
}
_arguments.AddRange(

26
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -41,7 +41,6 @@ namespace ErsatzTV.Core.FFmpeg @@ -41,7 +41,6 @@ namespace ErsatzTV.Core.FFmpeg
Option<ChannelWatermark> globalWatermark,
VaapiDriver vaapiDriver,
string vaapiDevice,
bool startAtZero,
bool hlsRealtime)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version);
@ -121,7 +120,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -121,7 +120,7 @@ namespace ErsatzTV.Core.FFmpeg
{
// HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(channel.Number, version, startAtZero)
return builder.WithHls(channel.Number, version)
.WithRealtimeOutput(hlsRealtime)
.Build();
default:
@ -131,7 +130,12 @@ namespace ErsatzTV.Core.FFmpeg @@ -131,7 +130,12 @@ namespace ErsatzTV.Core.FFmpeg
}
}
public Process ForError(string ffmpegPath, Channel channel, Option<TimeSpan> duration, string errorMessage)
public Process ForError(
string ffmpegPath,
Channel channel,
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime)
{
FFmpegPlaybackSettings playbackSettings =
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
@ -149,12 +153,22 @@ namespace ErsatzTV.Core.FFmpeg @@ -149,12 +153,22 @@ namespace ErsatzTV.Core.FFmpeg
.WithErrorText(desiredResolution, errorMessage)
.WithPixfmt("yuv420p")
.WithPlaybackArgs(playbackSettings)
.WithMetadata(channel, None)
.WithFormat("mpegts");
.WithMetadata(channel, None);
duration.IfSome(d => builder = builder.WithDuration(d));
return builder.WithPipe().Build();
switch (channel.StreamingMode)
{
// HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(channel.Number, None)
.WithRealtimeOutput(hlsRealtime)
.Build();
default:
return builder.WithFormat("mpegts")
.WithPipe()
.Build();
}
}
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)

14
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -70,13 +70,19 @@ namespace ErsatzTV.Core.Iptv @@ -70,13 +70,19 @@ namespace ErsatzTV.Core.Iptv
foreach ((Channel channel, List<PlayoutItem> sorted) in sortedChannelItems.OrderBy(kvp => kvp.Key.Number))
{
var i = 0;
while (i < sorted.Count && sorted[i].IsFiller)
{
i++;
}
while (i < sorted.Count)
{
PlayoutItem startItem = sorted[i];
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
int finishIndex = i;
while (hasCustomTitle && finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].CustomGroup)
while (finishIndex + 1 < sorted.Count && (hasCustomTitle && sorted[finishIndex + 1].CustomGroup ||
sorted[finishIndex + 1].IsFiller))
{
finishIndex++;
}
@ -97,10 +103,10 @@ namespace ErsatzTV.Core.Iptv @@ -97,10 +103,10 @@ namespace ErsatzTV.Core.Iptv
PlayoutItem finishItem = sorted[finishIndex];
i = finishIndex;
// ReSharper disable once StringLiteralTypo
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
// ReSharper disable once StringLiteralTypo
string stop = finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
string stop = startItem.GuideFinishOffset.HasValue
? startItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty)
: finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
string title = GetTitle(startItem);
string subtitle = GetSubtitle(startItem);

171
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -52,7 +52,7 @@ namespace ErsatzTV.Core.Scheduling @@ -52,7 +52,7 @@ namespace ErsatzTV.Core.Scheduling
bool rebuild = false)
{
var collectionKeys = playout.ProgramSchedule.Items
.Map(CollectionKeyForItem)
.SelectMany(CollectionKeysForItem)
.Distinct()
.ToList();
@ -192,9 +192,11 @@ namespace ErsatzTV.Core.Scheduling @@ -192,9 +192,11 @@ namespace ErsatzTV.Core.Scheduling
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
{
PlaybackOrder playbackOrder = sortedScheduleItems
.First(item => CollectionKeyForItem(item) == collectionKey)
.PlaybackOrder;
// use configured playback order for primary collection, shuffle for filler
Option<ProgramScheduleItem> maybeScheduleItem = sortedScheduleItems
.FirstOrDefault(item => CollectionKeyForItem(item) == collectionKey);
PlaybackOrder playbackOrder = maybeScheduleItem
.Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle);
IMediaCollectionEnumerator enumerator =
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems, playbackOrder);
collectionEnumerators.Add(collectionKey, enumerator);
@ -219,6 +221,7 @@ namespace ErsatzTV.Core.Scheduling @@ -219,6 +221,7 @@ namespace ErsatzTV.Core.Scheduling
Option<int> multipleRemaining = Optional(startAnchor.MultipleRemaining);
Option<DateTimeOffset> durationFinish = startAnchor.DurationFinishOffset;
bool inFlood = startAnchor.InFlood;
bool inDurationFiller = startAnchor.InDurationFiller;
bool customGroup = multipleRemaining.IsSome || durationFinish.IsSome;
@ -234,16 +237,33 @@ namespace ErsatzTV.Core.Scheduling @@ -234,16 +237,33 @@ namespace ErsatzTV.Core.Scheduling
currentTime,
multipleRemaining.IsSome,
durationFinish.IsSome,
inFlood);
inFlood,
inDurationFiller);
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
if (inDurationFiller && scheduleItem is ProgramScheduleItemDuration
{
TailMode: TailMode.Filler
})
{
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
}
IMediaCollectionEnumerator enumerator = collectionEnumerators[CollectionKeyForItem(scheduleItem)];
foreach (CollectionKey tailCollectionKey in maybeTailCollectionKey)
{
enumerator = collectionEnumerators[tailCollectionKey];
}
await enumerator.Current.IfSomeAsync(
mediaItem =>
{
_logger.LogDebug(
"Scheduling media item: {ScheduleItemNumber} / {CollectionType} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
scheduleItem.Index,
scheduleItem.CollectionType,
inDurationFiller
? (scheduleItem as ProgramScheduleItemDuration)?.TailCollectionType
: scheduleItem.CollectionType,
mediaItem.Id,
DisplayTitle(mediaItem),
itemStartTime);
@ -261,7 +281,8 @@ namespace ErsatzTV.Core.Scheduling @@ -261,7 +281,8 @@ namespace ErsatzTV.Core.Scheduling
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + version.Duration,
CustomGroup = customGroup
CustomGroup = customGroup,
IsFiller = inDurationFiller
};
if (!string.IsNullOrWhiteSpace(scheduleItem.CustomTitle))
@ -383,14 +404,34 @@ namespace ErsatzTV.Core.Scheduling @@ -383,14 +404,34 @@ namespace ErsatzTV.Core.Scheduling
"Advancing to next schedule item after playout mode {PlayoutMode}",
"Duration");
index++;
customGroup = false;
if (duration.OfflineTail)
if (duration.TailMode == TailMode.Offline)
{
durationFinish.Do(f => currentTime = f);
}
durationFinish = None;
if (duration.TailMode != TailMode.Filler || inDurationFiller)
{
if (duration.TailMode != TailMode.None)
{
durationFinish.Do(f => currentTime = f);
}
durationFinish = None;
inDurationFiller = false;
customGroup = false;
}
else if (duration.TailMode == TailMode.Filler &&
WillFinishFillerInTime(
scheduleItem,
currentTime,
durationFinish,
collectionEnumerators))
{
inDurationFiller = true;
durationFinish.Do(
f => playoutItem.GuideFinish = f.UtcDateTime);
}
}
}
);
@ -406,7 +447,8 @@ namespace ErsatzTV.Core.Scheduling @@ -406,7 +447,8 @@ namespace ErsatzTV.Core.Scheduling
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
// remove any items outside the desired range
playout.Items.RemoveAll(old => old.FinishOffset < playoutStart || old.StartOffset > playoutFinish);
playout.Items.RemoveAll(
old => old.FinishOffset < playoutStart.AddHours(-4) || old.StartOffset > playoutFinish);
DateTimeOffset minCurrentTime = currentTime;
if (playout.Items.Any())
@ -425,12 +467,61 @@ namespace ErsatzTV.Core.Scheduling @@ -425,12 +467,61 @@ namespace ErsatzTV.Core.Scheduling
NextStart = GetStartTimeAfter(nextScheduleItem, minCurrentTime).UtcDateTime,
MultipleRemaining = multipleRemaining.IsSome ? multipleRemaining.ValueUnsafe() : null,
DurationFinish = durationFinish.IsSome ? durationFinish.ValueUnsafe().UtcDateTime : null,
InFlood = inFlood
InFlood = inFlood,
InDurationFiller = inDurationFiller
};
return playout;
}
private static bool WillFinishFillerInTime(
ProgramScheduleItem scheduleItem,
DateTimeOffset currentTime,
Option<DateTimeOffset> durationFinish,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
{
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
if (scheduleItem is ProgramScheduleItemDuration
{
TailMode: TailMode.Filler
})
{
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
}
foreach (CollectionKey collectionKey in maybeTailCollectionKey)
{
IMediaCollectionEnumerator enumerator = collectionEnumerators[collectionKey];
Option<int> firstId = enumerator.Current.Map(i => i.Id);
while (true)
{
foreach (MediaItem peekMediaItem in enumerator.Current)
{
MediaVersion peekVersion = peekMediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
};
if (currentTime + peekVersion.Duration <= durationFinish.IfNone(SystemTime.MinValueUtc))
{
return true;
}
}
enumerator.MoveNext();
if (enumerator.Current.Map(i => i.Id) == firstId)
{
return false;
}
}
}
return false;
}
private static PlayoutAnchor FindStartAnchor(
Playout playout,
DateTimeOffset start,
@ -465,14 +556,16 @@ namespace ErsatzTV.Core.Scheduling @@ -465,14 +556,16 @@ namespace ErsatzTV.Core.Scheduling
DateTimeOffset start,
bool inMultiple = false,
bool inDuration = false,
bool inFlood = false)
bool inFlood = false,
bool inDurationFiller = false)
{
switch (item.StartType)
{
case StartType.Fixed:
if (item is ProgramScheduleItemMultiple && inMultiple ||
item is ProgramScheduleItemDuration && inDuration ||
item is ProgramScheduleItemFlood && inFlood)
item is ProgramScheduleItemFlood && inFlood ||
item is ProgramScheduleItemDuration && inDurationFiller)
{
return start;
}
@ -646,6 +739,13 @@ namespace ErsatzTV.Core.Scheduling @@ -646,6 +739,13 @@ namespace ErsatzTV.Core.Scheduling
}
}
private static List<CollectionKey> CollectionKeysForItem(ProgramScheduleItem item)
{
var result = new List<CollectionKey> { CollectionKeyForItem(item) };
result.AddRange(TailCollectionKeyForItem(item));
return result;
}
private static CollectionKey CollectionKeyForItem(ProgramScheduleItem item) =>
item.CollectionType switch
{
@ -682,6 +782,49 @@ namespace ErsatzTV.Core.Scheduling @@ -682,6 +782,49 @@ namespace ErsatzTV.Core.Scheduling
_ => throw new ArgumentOutOfRangeException(nameof(item))
};
private static Option<CollectionKey> TailCollectionKeyForItem(ProgramScheduleItem item)
{
if (item is ProgramScheduleItemDuration { TailMode: TailMode.Filler } duration)
{
return duration.TailCollectionType switch
{
ProgramScheduleItemCollectionType.Collection => new CollectionKey
{
CollectionType = duration.TailCollectionType,
CollectionId = duration.TailCollectionId
},
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
{
CollectionType = duration.TailCollectionType,
MediaItemId = duration.TailMediaItemId
},
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
{
CollectionType = duration.TailCollectionType,
MediaItemId = duration.TailMediaItemId
},
ProgramScheduleItemCollectionType.Artist => new CollectionKey
{
CollectionType = duration.TailCollectionType,
MediaItemId = duration.TailMediaItemId
},
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
{
CollectionType = duration.TailCollectionType,
MultiCollectionId = duration.TailMultiCollectionId
},
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
{
CollectionType = duration.TailCollectionType,
SmartCollectionId = duration.TailSmartCollectionId
},
_ => throw new ArgumentOutOfRangeException(nameof(item))
};
}
return None;
}
private class CollectionKey : Record<CollectionKey>
{
public ProgramScheduleItemCollectionType CollectionType { get; set; }

22
ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemDurationConfiguration.cs

@ -6,7 +6,27 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -6,7 +6,27 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class ProgramScheduleItemDurationConfiguration : IEntityTypeConfiguration<ProgramScheduleItemDuration>
{
public void Configure(EntityTypeBuilder<ProgramScheduleItemDuration> builder) =>
public void Configure(EntityTypeBuilder<ProgramScheduleItemDuration> builder)
{
builder.ToTable("ProgramScheduleDurationItem");
builder.HasOne(i => i.TailCollection)
.WithMany()
.HasForeignKey(i => i.TailCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.TailMediaItem)
.WithMany()
.HasForeignKey(i => i.TailMediaItemId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.TailMultiCollection)
.WithMany()
.HasForeignKey(i => i.TailMultiCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
}
}
}

6
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -12,12 +12,12 @@ @@ -12,12 +12,12 @@
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00014" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00014" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00014" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.11" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.63">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

3343
ErsatzTV.Infrastructure/Migrations/20211013012823_Add_DurationTail.Designer.cs generated

File diff suppressed because it is too large Load Diff

158
ErsatzTV.Infrastructure/Migrations/20211013012823_Add_DurationTail.cs

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_DurationTail : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "OfflineTail",
table: "ProgramScheduleDurationItem",
newName: "TailMode");
migrationBuilder.AddColumn<int>(
name: "TailCollectionId",
table: "ProgramScheduleDurationItem",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "TailCollectionType",
table: "ProgramScheduleDurationItem",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "TailMediaItemId",
table: "ProgramScheduleDurationItem",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "TailMultiCollectionId",
table: "ProgramScheduleDurationItem",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "TailSmartCollectionId",
table: "ProgramScheduleDurationItem",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleDurationItem_TailCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailCollectionId");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleDurationItem_TailMediaItemId",
table: "ProgramScheduleDurationItem",
column: "TailMediaItemId");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleDurationItem_TailMultiCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailMultiCollectionId");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleDurationItem_TailSmartCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailSmartCollectionId");
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleDurationItem_Collection_TailCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailCollectionId",
principalTable: "Collection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleDurationItem_MediaItem_TailMediaItemId",
table: "ProgramScheduleDurationItem",
column: "TailMediaItemId",
principalTable: "MediaItem",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleDurationItem_MultiCollection_TailMultiCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailMultiCollectionId",
principalTable: "MultiCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailSmartCollectionId",
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleDurationItem_Collection_TailCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleDurationItem_MediaItem_TailMediaItemId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleDurationItem_MultiCollection_TailMultiCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleDurationItem_TailCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleDurationItem_TailMediaItemId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleDurationItem_TailMultiCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleDurationItem_TailSmartCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropColumn(
name: "TailCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropColumn(
name: "TailCollectionType",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropColumn(
name: "TailMediaItemId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropColumn(
name: "TailMultiCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropColumn(
name: "TailSmartCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.RenameColumn(
name: "TailMode",
table: "ProgramScheduleDurationItem",
newName: "OfflineTail");
}
}
}

3346
ErsatzTV.Infrastructure/Migrations/20211013025333_Add_PlayoutAnchor_InDurationFiller.Designer.cs generated

File diff suppressed because it is too large Load Diff

23
ErsatzTV.Infrastructure/Migrations/20211013025333_Add_PlayoutAnchor_InDurationFiller.cs

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_PlayoutAnchor_InDurationFiller : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Anchor_InDurationFiller",
table: "Playout",
type: "INTEGER",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Anchor_InDurationFiller",
table: "Playout");
}
}
}

3349
ErsatzTV.Infrastructure/Migrations/20211013230405_Add_PlayoutItem_IsFiller.Designer.cs generated

File diff suppressed because it is too large Load Diff

24
ErsatzTV.Infrastructure/Migrations/20211013230405_Add_PlayoutItem_IsFiller.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_PlayoutItem_IsFiller : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsFiller",
table: "PlayoutItem",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsFiller",
table: "PlayoutItem");
}
}
}

3352
ErsatzTV.Infrastructure/Migrations/20211013231447_Add_PlayoutItem_GuideFinish.Designer.cs generated

File diff suppressed because it is too large Load Diff

24
ErsatzTV.Infrastructure/Migrations/20211013231447_Add_PlayoutItem_GuideFinish.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_PlayoutItem_GuideFinish : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "GuideFinish",
table: "PlayoutItem",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GuideFinish",
table: "PlayoutItem");
}
}
}

65
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -1097,6 +1097,12 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1097,6 +1097,12 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<DateTime>("Finish")
.HasColumnType("TEXT");
b.Property<DateTime?>("GuideFinish")
.HasColumnType("TEXT");
b.Property<bool>("IsFiller")
.HasColumnType("INTEGER");
b.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
@ -1808,12 +1814,35 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1808,12 +1814,35 @@ namespace ErsatzTV.Infrastructure.Migrations
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<bool>("OfflineTail")
.HasColumnType("INTEGER");
b.Property<TimeSpan>("PlayoutDuration")
.HasColumnType("TEXT");
b.Property<int?>("TailCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("TailCollectionType")
.HasColumnType("INTEGER");
b.Property<int?>("TailMediaItemId")
.HasColumnType("INTEGER");
b.Property<int>("TailMode")
.HasColumnType("INTEGER");
b.Property<int?>("TailMultiCollectionId")
.HasColumnType("INTEGER");
b.Property<int?>("TailSmartCollectionId")
.HasColumnType("INTEGER");
b.HasIndex("TailCollectionId");
b.HasIndex("TailMediaItemId");
b.HasIndex("TailMultiCollectionId");
b.HasIndex("TailSmartCollectionId");
b.ToTable("ProgramScheduleDurationItem");
});
@ -2422,6 +2451,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2422,6 +2451,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b1.Property<DateTime?>("DurationFinish")
.HasColumnType("TEXT");
b1.Property<bool>("InDurationFiller")
.HasColumnType("INTEGER");
b1.Property<bool>("InFlood")
.HasColumnType("INTEGER");
@ -2903,6 +2935,33 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2903,6 +2935,33 @@ namespace ErsatzTV.Infrastructure.Migrations
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Collection", "TailCollection")
.WithMany()
.HasForeignKey("TailCollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "TailMediaItem")
.WithMany()
.HasForeignKey("TailMediaItemId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "TailMultiCollection")
.WithMany()
.HasForeignKey("TailMultiCollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "TailSmartCollection")
.WithMany()
.HasForeignKey("TailSmartCollectionId");
b.Navigation("TailCollection");
b.Navigation("TailMediaItem");
b.Navigation("TailMultiCollection");
b.Navigation("TailSmartCollection");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>

4
ErsatzTV/ErsatzTV.csproj

@ -21,8 +21,8 @@ @@ -21,8 +21,8 @@
<PackageReference Include="Markdig" Version="0.26.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="3.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
ErsatzTV/Pages/Artist.razor

@ -213,7 +213,7 @@ @@ -213,7 +213,7 @@
DialogResult result = await dialog.Result;
if (!result.Cancelled && 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, null, null));
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null));
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
}
}

2
ErsatzTV/Pages/Playouts.razor

@ -182,7 +182,7 @@ @@ -182,7 +182,7 @@
if (_selectedPlayoutId.HasValue)
{
PagedPlayoutItemsViewModel data =
await _mediator.Send(new GetPlayoutItemsById(_selectedPlayoutId.Value, state.Page, state.PageSize));
await _mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, state.Page, state.PageSize));
return new TableData<PlayoutItemViewModel>
{
TotalItems = data.TotalCount,

94
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -94,7 +94,7 @@ @@ -94,7 +94,7 @@
}
</MudSelect>
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
<MudSelect Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
@ -154,7 +154,7 @@ @@ -154,7 +154,7 @@
SearchFunc="@SearchArtists"
ToStringFunc="@(s => s?.Name)"/>
}
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
@switch (_selectedItem.CollectionType)
{
case ProgramScheduleItemCollectionType.MultiCollection:
@ -183,9 +183,77 @@ @@ -183,9 +183,77 @@
</MudSelect>
<MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/>
<MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
<MudElement HtmlTag="div" Class="mt-3">
<MudSwitch Label="Offline Tail" @bind-Checked="@_selectedItem.OfflineTail" For="@(() => _selectedItem.OfflineTail)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
</MudElement>
<MudSelect Class="mt-3" Label="Tail Mode" @bind-Value="@_selectedItem.TailMode" For="@(() => _selectedItem.TailMode)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)">
<MudSelectItem Value="@TailMode.None">(none)</MudSelectItem>
<MudSelectItem Value="@TailMode.Offline">Offline</MudSelectItem>
<MudSelectItem Value="@TailMode.Filler">Filler</MudSelectItem>
</MudSelect>
<MudSelect Class="mt-3" Label="Tail Collection Type" @bind-Value="_selectedItem.TailCollectionType" For="@(() => _selectedItem.TailCollectionType)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
}
</MudSelect>
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudAutocomplete Class="mt-3"
T="MediaCollectionViewModel"
Label="Collection"
@bind-value="_selectedItem.TailCollection"
SearchFunc="@SearchMediaCollections"
ToStringFunc="@(c => c?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
{
<MudAutocomplete Class="mt-3"
T="MultiCollectionViewModel"
Label="Multi Collection"
@bind-value="_selectedItem.TailMultiCollection"
SearchFunc="@SearchMultiCollections"
ToStringFunc="@(c => c?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
{
<MudAutocomplete Class="mt-3"
T="SmartCollectionViewModel"
Label="Smart Collection"
@bind-value="_selectedItem.TailSmartCollection"
SearchFunc="@SearchSmartCollections"
ToStringFunc="@(c => c?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Show"
@bind-value="_selectedItem.TailMediaItem"
SearchFunc="@SearchTelevisionShows"
ToStringFunc="@(s => s?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Television Season"
@bind-value="_selectedItem.TailMediaItem"
SearchFunc="@SearchTelevisionSeasons"
ToStringFunc="@(s => s?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.Artist)
{
<MudAutocomplete Class="mt-3"
T="NamedMediaItemViewModel"
Label="Artist"
@bind-value="_selectedItem.TailMediaItem"
SearchFunc="@SearchArtists"
ToStringFunc="@(s => s?.Name)"
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
}
<MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/>
</MudCardContent>
</MudCard>
@ -274,7 +342,7 @@ @@ -274,7 +342,7 @@
SmartCollection = item.SmartCollection,
MediaItem = item.MediaItem,
PlaybackOrder = item.PlaybackOrder,
CustomTitle = item.CustomTitle
CustomTitle = item.CustomTitle,
};
switch (item)
@ -284,7 +352,12 @@ @@ -284,7 +352,12 @@
break;
case ProgramScheduleItemDurationViewModel duration:
result.PlayoutDuration = duration.PlayoutDuration;
result.OfflineTail = duration.OfflineTail;
result.TailMode = duration.TailMode;
result.TailCollectionType = duration.TailCollectionType;
result.TailCollection = duration.TailCollection;
result.TailMultiCollection = duration.TailMultiCollection;
result.TailSmartCollection = duration.TailSmartCollection;
result.TailMediaItem = duration.TailMediaItem;
break;
}
@ -359,7 +432,12 @@ @@ -359,7 +432,12 @@
item.PlaybackOrder,
item.MultipleCount,
item.PlayoutDuration,
item.PlayoutMode == PlayoutMode.Duration ? item.OfflineTail.IfNone(false) : null,
item.TailMode,
item.TailCollectionType,
item.TailCollection?.Id,
item.TailMultiCollection?.Id,
item.TailSmartCollection?.Id,
item.TailMediaItem?.MediaItemId,
item.CustomTitle)).ToList();
Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items)).Map(e => e.LeftToSeq());

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -197,7 +197,7 @@ @@ -197,7 +197,7 @@
DialogResult result = await dialog.Result;
if (!result.Cancelled && 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, null, null));
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null));
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
}
}

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -225,7 +225,7 @@ @@ -225,7 +225,7 @@
DialogResult result = await dialog.Result;
if (!result.Cancelled && 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, null, null));
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null));
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
}
}

33
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -12,9 +12,9 @@ namespace ErsatzTV.ViewModels @@ -12,9 +12,9 @@ namespace ErsatzTV.ViewModels
{
private ProgramScheduleItemCollectionType _collectionType;
private int? _multipleCount;
private bool? _offlineTail;
private TimeSpan? _playoutDuration;
private TimeSpan? _startTime;
private ProgramScheduleItemCollectionType _tailCollectionType;
public int Id { get; set; }
public int Index { get; set; }
@ -40,10 +40,12 @@ namespace ErsatzTV.ViewModels @@ -40,10 +40,12 @@ namespace ErsatzTV.ViewModels
Collection = null;
MultiCollection = null;
MediaItem = null;
SmartCollection = null;
OnPropertyChanged(nameof(Collection));
OnPropertyChanged(nameof(MultiCollection));
OnPropertyChanged(nameof(MediaItem));
OnPropertyChanged(nameof(SmartCollection));
}
if (_collectionType == ProgramScheduleItemCollectionType.MultiCollection)
@ -83,11 +85,34 @@ namespace ErsatzTV.ViewModels @@ -83,11 +85,34 @@ namespace ErsatzTV.ViewModels
set => _playoutDuration = value;
}
public bool? OfflineTail
public TailMode TailMode { get; set; }
public ProgramScheduleItemCollectionType TailCollectionType
{
get => PlayoutMode == PlayoutMode.Duration ? _offlineTail : null;
set => _offlineTail = value;
get => _tailCollectionType;
set
{
if (_tailCollectionType != value)
{
_tailCollectionType = value;
TailCollection = null;
TailMultiCollection = null;
TailMediaItem = null;
TailSmartCollection = null;
OnPropertyChanged(nameof(TailCollection));
OnPropertyChanged(nameof(TailMultiCollection));
OnPropertyChanged(nameof(TailMediaItem));
OnPropertyChanged(nameof(TailSmartCollection));
}
}
}
public MediaCollectionViewModel TailCollection { get; set; }
public MultiCollectionViewModel TailMultiCollection { get; set; }
public SmartCollectionViewModel TailSmartCollection { get; set; }
public NamedMediaItemViewModel TailMediaItem { get; set; }
public string CustomTitle { get; set; }

Loading…
Cancel
Save