Browse Source

filler rework (#449)

* add chapter statistics and new filler options

* refactor playout builder

* more refactor prep for filler

* rewrite schedulers

* refactor collectionkey

* add tail filler kind

* migrate tail filler to filler preset

* optionally show filler

* fix playout detail row count

* remove duration tail filler options

* implement tail and fallback in flood scheduler

* implement tail and fallback in one scheduler

* implement tail and fallback in multiple scheduler

* implement looping fallback filler

* more duration tests

* start to add post-roll filler to flood

* rework playoutitem filler tagging

* rework scheduler logging

* calculate whether configured filler will fit

* implement pre-roll and post-roll duration and count filler

* improve duration filler calculation

* add minutes to search index

* update channel guide to work with new filler

* add mid-roll filler

* don't clone enumerators for filler calculations

* support pre-roll and post-roll pad filler

* implement mid-roll pad filler

* allow clearing filler selections in schedule editor

* fix tests

* filler config validation

* use consistent time zone for tests
pull/450/head
Jason Dove 4 years ago committed by GitHub
parent
commit
6d147de2f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      CHANGELOG.md
  2. 24
      ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs
  3. 55
      ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs
  4. 8
      ErsatzTV.Application/Filler/Commands/DeleteFillerPreset.cs
  5. 43
      ErsatzTV.Application/Filler/Commands/DeleteFillerPresetHandler.cs
  6. 25
      ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs
  7. 60
      ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs
  8. 20
      ErsatzTV.Application/Filler/FillerPresetViewModel.cs
  9. 22
      ErsatzTV.Application/Filler/Mapper.cs
  10. 6
      ErsatzTV.Application/Filler/PagedFillerPresetsViewModel.cs
  11. 7
      ErsatzTV.Application/Filler/Queries/GetAllFillerPresets.cs
  12. 29
      ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsHandler.cs
  13. 7
      ErsatzTV.Application/Filler/Queries/GetFillerPresetById.cs
  14. 29
      ErsatzTV.Application/Filler/Queries/GetFillerPresetByIdHandler.cs
  15. 6
      ErsatzTV.Application/Filler/Queries/GetPagedFillerPresets.cs
  16. 46
      ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs
  17. 15
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  18. 37
      ErsatzTV.Application/Playouts/Mapper.cs
  19. 5
      ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsById.cs
  20. 8
      ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs
  21. 12
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  22. 10
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  23. 74
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  24. 12
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  25. 23
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  26. 81
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  27. 32
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  28. 15
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  29. 15
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  30. 15
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  31. 8
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  32. 9
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs
  33. 2
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  34. 6
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  35. 162
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
  36. 7
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  37. 3
      ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs
  38. 47
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  39. 205
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  40. 552
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs
  41. 715
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs
  42. 630
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  43. 460
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs
  44. 78
      ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs
  45. 2
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  46. 12
      ErsatzTV.Core/Domain/Filler/FillerKind.cs
  47. 10
      ErsatzTV.Core/Domain/Filler/FillerMode.cs
  48. 24
      ErsatzTV.Core/Domain/Filler/FillerPreset.cs
  49. 15
      ErsatzTV.Core/Domain/MediaItem/MediaChapter.cs
  50. 1
      ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs
  51. 1
      ErsatzTV.Core/Domain/PlayoutAnchor.cs
  52. 26
      ErsatzTV.Core/Domain/PlayoutItem.cs
  53. 11
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  54. 9
      ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs
  55. 10
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  56. 10
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  57. 14
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  58. 1
      ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs
  59. 17
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutModeScheduler.cs
  60. 34
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  61. 76
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  62. 2
      ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs
  63. 6
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  64. 88
      ErsatzTV.Core/Scheduling/CollectionKey.cs
  65. 11
      ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs
  66. 677
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  67. 18
      ErsatzTV.Core/Scheduling/PlayoutBuilderState.cs
  68. 6
      ErsatzTV.Core/Scheduling/PlayoutModeBlock.cs
  69. 700
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  70. 182
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  71. 147
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  72. 131
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  73. 108
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  74. 3
      ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs
  75. 5
      ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs
  76. 30
      ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs
  77. 1
      ErsatzTV.Core/SystemTime.cs
  78. 38
      ErsatzTV.Infrastructure/Data/Configurations/Filler/FillerPresetConfiguration.cs
  79. 11
      ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaChapterConfiguration.cs
  80. 5
      ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaVersionConfiguration.cs
  81. 36
      ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs
  82. 22
      ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemDurationConfiguration.cs
  83. 1
      ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs
  84. 7
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  85. 26
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  86. 4
      ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs
  87. 2
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  88. 2
      ErsatzTV.Infrastructure/Data/TvContext.cs
  89. 3551
      ErsatzTV.Infrastructure/Migrations/20211016173400_Add_MediaChapter.Designer.cs
  90. 58
      ErsatzTV.Infrastructure/Migrations/20211016173400_Add_MediaChapter.cs
  91. 3684
      ErsatzTV.Infrastructure/Migrations/20211017110923_Add_FillerPreset.Designer.cs
  92. 256
      ErsatzTV.Infrastructure/Migrations/20211017110923_Add_FillerPreset.cs
  93. 3696
      ErsatzTV.Infrastructure/Migrations/20211019001254_Add_ProgramScheduleItem_TailFiller.Designer.cs
  94. 140
      ErsatzTV.Infrastructure/Migrations/20211019001254_Add_ProgramScheduleItem_TailFiller.cs
  95. 3696
      ErsatzTV.Infrastructure/Migrations/20211019173552_Migrate_TailFiller_FillerPreset.Designer.cs
  96. 35
      ErsatzTV.Infrastructure/Migrations/20211019173552_Migrate_TailFiller_FillerPreset.cs
  97. 3645
      ErsatzTV.Infrastructure/Migrations/20211019231930_Remove_DurationTailFiller.Designer.cs
  98. 148
      ErsatzTV.Infrastructure/Migrations/20211019231930_Remove_DurationTailFiller.cs
  99. 3648
      ErsatzTV.Infrastructure/Migrations/20211020014526_Add_PlayoutItemIsFallback.Designer.cs
  100. 24
      ErsatzTV.Infrastructure/Migrations/20211020014526_Add_PlayoutItemIsFallback.cs
  101. Some files were not shown because too many files have changed in this diff Show More

18
CHANGELOG.md

@ -6,10 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,10 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Fix generated streams with mpeg2video
- Fix incorrect row count in playout detail table
- Fix deleting movies that have been removed from Jellyfin and Emby
### Added
- Add new filler system
- `Pre-Roll Filler` plays before each media item
- `Mid-Roll Filler` plays between media item chapters
- `Post-Roll Filler` plays after each media item
- `Tail Filler` plays after all media items, until the next media item
- `Fallback Filler` loops instead of default offline image to fill any remaining gaps
- Store chapter details with media statistics; this is needed to support mid-roll filler
- This requires re-ingesting statistics for all media items the first time this version is launched
- Add switch to show/hide filler in playout detail table
- Add `minutes` field to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
### Changed
- Change some debug log messages to info so they show by default again
- Remove tail collection options from `Duration` playout mode
- Show localized start time in schedule items tables
## [0.1.5-alpha] - 2021-10-18
### Fixed
@ -18,13 +34,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -18,13 +34,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix updating Jellyfin and Emby artwork
- Fix Plex, Jellyfin, Emby worker crash attempting to sync library that no longer exists
- Fix bug with `Duration` mode scheduling when media items are too long to fit in the requested duration
- Fix bug with `Duration` mode scheduling with `Filler` tail mode where other duration items in the schedule would be skipped
### Added
- Include music video thumbnails in channel guide (xmltv)
### Changed
- Automatically find working Plex address on startup
- Automatically select schedule item in schedules that contain only one item
- Change default log level from `Debug` to `Information`
- The `Debug` log level can be enabled in the `appsettings.json` file for non-docker installs
- The `Debug` log level can be enabled by setting the environment variable `Serilog:MinimumLevel=Debug` for docker installs

24
ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using System;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public record CreateFillerPreset(
string Name,
FillerKind FillerKind,
FillerMode FillerMode,
TimeSpan? Duration,
int? Count,
int? PadToNearestMinute,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
) : IRequest<Either<BaseError, Unit>>;
}

55
ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, Unit>> Handle(CreateFillerPreset request, CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
var fillerPreset = new FillerPreset
{
Name = request.Name,
FillerKind = request.FillerKind,
FillerMode = request.FillerMode,
Duration = request.Duration,
Count = request.Count,
PadToNearestMinute = request.PadToNearestMinute,
CollectionType = request.CollectionType,
CollectionId = request.CollectionId,
MediaItemId = request.MediaItemId,
MultiCollectionId = request.MultiCollectionId,
SmartCollectionId = request.SmartCollectionId
};
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return Unit.Default;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}

8
ErsatzTV.Application/Filler/Commands/DeleteFillerPreset.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Filler.Commands
{
public record DeleteFillerPreset(int FillerPresetId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
}

43
ErsatzTV.Application/Filler/Commands/DeleteFillerPresetHandler.cs

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Unit>> Handle(
DeleteFillerPreset request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
return await validation.Apply(ps => DoDeletion(dbContext, ps));
}
private static Task<Unit> DoDeletion(TvContext dbContext, FillerPreset fillerPreset)
{
dbContext.FillerPresets.Remove(fillerPreset);
return dbContext.SaveChangesAsync().ToUnit();
}
private Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
TvContext dbContext,
DeleteFillerPreset request) =>
dbContext.FillerPresets
.SelectOneAsync(fp => fp.Id, ps => ps.Id == request.FillerPresetId)
.Map(o => o.ToValidation<BaseError>($"FillerPreset {request.FillerPresetId} does not exist."));
}
}

25
ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using System;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public record UpdateFillerPreset(
int Id,
string Name,
FillerKind FillerKind,
FillerMode FillerMode,
TimeSpan? Duration,
int? Count,
int? PadToNearestMinute,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
) : IRequest<Either<BaseError, Unit>>;
}

60
ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, Unit>> Handle(UpdateFillerPreset request, CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
}
private async Task<Unit> ApplyUpdateRequest(
TvContext dbContext,
FillerPreset existing,
UpdateFillerPreset request)
{
existing.Name = request.Name;
existing.FillerKind = request.FillerKind;
existing.FillerMode = request.FillerMode;
existing.Duration = request.Duration;
existing.Count = request.Count;
existing.PadToNearestMinute = request.PadToNearestMinute;
existing.CollectionType = request.CollectionType;
existing.CollectionId = request.CollectionId;
existing.MediaItemId = request.MediaItemId;
existing.MultiCollectionId = request.MultiCollectionId;
existing.SmartCollectionId = request.SmartCollectionId;
await dbContext.SaveChangesAsync();
return Unit.Default;
}
private static Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
TvContext dbContext,
UpdateFillerPreset request) =>
dbContext.FillerPresets
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id)
.Map(o => o.ToValidation<BaseError>("FillerPreset does not exist"));
}
}

20
ErsatzTV.Application/Filler/FillerPresetViewModel.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using System;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Application.Filler
{
public record FillerPresetViewModel(
int Id,
string Name,
FillerKind FillerKind,
FillerMode FillerMode,
TimeSpan? Duration,
int? Count,
int? PadToNearestMinute,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId);
}

22
ErsatzTV.Application/Filler/Mapper.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Application.Filler
{
internal static class Mapper
{
internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) =>
new(
fillerPreset.Id,
fillerPreset.Name,
fillerPreset.FillerKind,
fillerPreset.FillerMode,
fillerPreset.Duration,
fillerPreset.Count,
fillerPreset.PadToNearestMinute,
fillerPreset.CollectionType,
fillerPreset.CollectionId,
fillerPreset.MediaItemId,
fillerPreset.MultiCollectionId,
fillerPreset.SmartCollectionId);
}
}

6
ErsatzTV.Application/Filler/PagedFillerPresetsViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.Filler
{
public record PagedFillerPresetsViewModel(int TotalCount, List<FillerPresetViewModel> Page);
}

7
ErsatzTV.Application/Filler/Queries/GetAllFillerPresets.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Filler.Queries
{
public record GetAllFillerPresets : IRequest<List<FillerPresetViewModel>>;
}

29
ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsHandler.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Infrastructure.Data;
using MediatR;
using Microsoft.EntityFrameworkCore;
using LanguageExt;
using static ErsatzTV.Application.Filler.Mapper;
namespace ErsatzTV.Application.Filler.Queries
{
public class GetAllFillerPresetsHandler : IRequestHandler<GetAllFillerPresets, List<FillerPresetViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllFillerPresetsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<FillerPresetViewModel>> Handle(
GetAllFillerPresets request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FillerPresets.ToListAsync(cancellationToken)
.Map(presets => presets.Map(ProjectToViewModel).ToList());
}
}
}

7
ErsatzTV.Application/Filler/Queries/GetFillerPresetById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Filler.Queries
{
public record GetFillerPresetById(int Id) : IRequest<Option<FillerPresetViewModel>>;
}

29
ErsatzTV.Application/Filler/Queries/GetFillerPresetByIdHandler.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Filler.Mapper;
namespace ErsatzTV.Application.Filler.Queries
{
public class GetFillerPresetByIdHandler : IRequestHandler<GetFillerPresetById, Option<FillerPresetViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFillerPresetByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<FillerPresetViewModel>> Handle(
GetFillerPresetById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FillerPresets
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
.MapT(ProjectToViewModel);
}
}
}

6
ErsatzTV.Application/Filler/Queries/GetPagedFillerPresets.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.Filler.Queries
{
public record GetPagedFillerPresets(int PageNum, int PageSize) : IRequest<PagedFillerPresetsViewModel>;
}

46
ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using ErsatzTV.Infrastructure.Data;
using MediatR;
using Microsoft.EntityFrameworkCore;
using LanguageExt;
using static ErsatzTV.Application.Filler.Mapper;
namespace ErsatzTV.Application.Filler.Queries
{
public class GetPagedFillerPresetsHandler : IRequestHandler<GetPagedFillerPresets, PagedFillerPresetsViewModel>
{
private readonly IDbConnection _dbConnection;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPagedFillerPresetsHandler(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
{
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public async Task<PagedFillerPresetsViewModel> Handle(
GetPagedFillerPresets request,
CancellationToken cancellationToken)
{
int count = await _dbConnection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM FillerPreset");
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
List<FillerPresetViewModel> page = await dbContext.FillerPresets.FromSqlRaw(
@"SELECT * FROM FillerPreset
ORDER BY Name
COLLATE NOCASE
LIMIT {0} OFFSET {1}",
request.PageSize,
request.PageNum * request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new PagedFillerPresetsViewModel(count, page);
}
}
}

15
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -52,6 +52,21 @@ namespace ErsatzTV.Application.Playouts.Commands @@ -52,6 +52,21 @@ namespace ErsatzTV.Application.Playouts.Commands
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
}

37
ErsatzTV.Application/Playouts/Mapper.cs

@ -8,13 +8,13 @@ namespace ErsatzTV.Application.Playouts @@ -8,13 +8,13 @@ namespace ErsatzTV.Application.Playouts
{
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
new(
GetDisplayTitle(playoutItem.MediaItem),
GetDisplayTitle(playoutItem),
playoutItem.StartOffset,
GetDisplayDuration(playoutItem.MediaItem));
GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset));
private static string GetDisplayTitle(MediaItem mediaItem)
private static string GetDisplayTitle(PlayoutItem playoutItem)
{
switch (mediaItem)
switch (playoutItem.MediaItem)
{
case Episode e:
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
@ -28,6 +28,11 @@ namespace ErsatzTV.Application.Playouts @@ -28,6 +28,11 @@ namespace ErsatzTV.Application.Playouts
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
var titlesString = $"{string.Join('/', episodeTitles)}";
if (!string.IsNullOrWhiteSpace(playoutItem.ChapterTitle))
{
titlesString += $" ({playoutItem.ChapterTitle})";
}
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
case Movie m:
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
@ -36,29 +41,21 @@ namespace ErsatzTV.Application.Playouts @@ -36,29 +41,21 @@ namespace ErsatzTV.Application.Playouts
.Map(am => $"{am.Title} - ").IfNone(string.Empty);
return mv.MusicVideoMetadata.HeadOrNone()
.Map(mvm => $"{artistName}{mvm.Title}")
.Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown music video]");
case OtherVideo ov:
return ov.OtherVideoMetadata.HeadOrNone().Map(ovm => ovm.Title ?? string.Empty)
return ov.OtherVideoMetadata.HeadOrNone()
.Map(ovm => ovm.Title ?? string.Empty)
.Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown video]");
default:
return string.Empty;
}
}
private static string GetDisplayDuration(MediaItem mediaItem)
{
MediaVersion version = mediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
OtherVideo ov => ov.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
return string.Format(
version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
version.Duration);
}
private static string GetDisplayDuration(TimeSpan duration) =>
string.Format(
duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
duration);
}
}

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

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

8
ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs

@ -4,6 +4,7 @@ using System.Linq; @@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
@ -25,10 +26,10 @@ namespace ErsatzTV.Application.Playouts.Queries @@ -25,10 +26,10 @@ namespace ErsatzTV.Application.Playouts.Queries
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
int totalCount = await dbContext.PlayoutItems
.CountAsync(i => i.PlayoutId == request.PlayoutId, cancellationToken);
DateTime now = DateTimeOffset.Now.UtcDateTime;
int totalCount = await dbContext.PlayoutItems
.CountAsync(i => i.Finish >= now && i.PlayoutId == request.PlayoutId && (request.ShowFiller || i.FillerKind == FillerKind.None), cancellationToken);
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
.Include(i => i.MediaItem)
@ -58,6 +59,7 @@ namespace ErsatzTV.Application.Playouts.Queries @@ -58,6 +59,7 @@ namespace ErsatzTV.Application.Playouts.Queries
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Filter(i => i.PlayoutId == request.PlayoutId)
.Filter(i => i.Finish >= now)
.Filter(i => request.ShowFiller || i.FillerKind == FillerKind.None)
.OrderBy(i => i.Start)
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)

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

@ -20,11 +20,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -20,11 +20,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
int? MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
ProgramScheduleItemCollectionType TailCollectionType,
int? TailCollectionId,
int? TailMultiCollectionId,
int? TailSmartCollectionId,
int? TailMediaItemId,
string CustomTitle,
GuideMode GuideMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
GuideMode GuideMode,
int? PreRollFillerId,
int? MidRollFillerId,
int? PostRollFillerId,
int? TailFillerId,
int? FallbackFillerId) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
}

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

@ -16,12 +16,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -16,12 +16,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
int? MultipleCount { get; }
TimeSpan? PlayoutDuration { get; }
TailMode TailMode { get; }
ProgramScheduleItemCollectionType TailCollectionType { get; }
int? TailCollectionId { get; }
int? TailMultiCollectionId { get; }
int? TailSmartCollectionId { get; }
int? TailMediaItemId { get; }
string CustomTitle { get; }
GuideMode GuideMode { get; }
int? PreRollFillerId { get; }
int? MidRollFillerId { get; }
int? PostRollFillerId { get; }
int? TailFillerId { get; }
int? FallbackFillerId { get; }
}
}

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

@ -1,11 +1,15 @@ @@ -1,11 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.ProgramSchedules.Commands
{
@ -20,6 +24,33 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -20,6 +24,33 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
.SelectOneAsync(ps => ps.Id, ps => ps.Id == programScheduleId)
.Map(o => o.ToValidation<BaseError>("[ProgramScheduleId] does not exist."));
protected static async Task<Either<BaseError, ProgramSchedule>> FillerConfigurationMustBeValid(
TvContext dbContext,
IProgramScheduleItemRequest item,
ProgramSchedule programSchedule)
{
var allFillerIds = Optional(item.PreRollFillerId)
.Append(Optional(item.MidRollFillerId))
.Append(Optional(item.PostRollFillerId))
.ToList();
List<FillerPreset> allFiller = await dbContext.FillerPresets
.Filter(fp => allFillerIds.Contains(fp.Id))
.ToListAsync();
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
{
return BaseError.New("Schedule may only contain one filler preset that is configured to pad");
}
if (allFiller.Any(fp => fp.PadToNearestMinute.HasValue) && !item.FallbackFillerId.HasValue)
{
return BaseError.New("Fallback filler is required when padding");
}
return programSchedule;
}
protected static Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
IProgramScheduleItemRequest item,
ProgramSchedule programSchedule)
@ -55,6 +86,16 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -55,6 +86,16 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
return BaseError.New("[PlayoutDuration] 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");
}
if (item.TailFillerId != null && item.TailMode != TailMode.Filler)
{
return BaseError.New("Tail Filler will not be used unless tail mode is set to Filler");
}
break;
default:
return BaseError.New("[PlayoutMode] is invalid");
@ -136,7 +177,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -136,7 +177,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
MediaItemId = item.MediaItemId,
PlaybackOrder = item.PlaybackOrder,
CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode
GuideMode = item.GuideMode,
PreRollFillerId = item.PreRollFillerId,
MidRollFillerId = item.MidRollFillerId,
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId
},
PlayoutMode.One => new ProgramScheduleItemOne
{
@ -150,7 +196,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -150,7 +196,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
MediaItemId = item.MediaItemId,
PlaybackOrder = item.PlaybackOrder,
CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode
GuideMode = item.GuideMode,
PreRollFillerId = item.PreRollFillerId,
MidRollFillerId = item.MidRollFillerId,
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId
},
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
{
@ -165,7 +216,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -165,7 +216,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
PlaybackOrder = item.PlaybackOrder,
Count = item.MultipleCount.GetValueOrDefault(),
CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode
GuideMode = item.GuideMode,
PreRollFillerId = item.PreRollFillerId,
MidRollFillerId = item.MidRollFillerId,
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId
},
PlayoutMode.Duration => new ProgramScheduleItemDuration
{
@ -180,13 +236,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -180,13 +236,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
PlaybackOrder = item.PlaybackOrder,
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
TailMode = item.TailMode,
TailCollectionType = item.TailCollectionType,
TailCollectionId = item.TailCollectionId,
TailMultiCollectionId = item.TailMultiCollectionId,
TailSmartCollectionId = item.TailSmartCollectionId,
TailMediaItemId = item.TailMediaItemId,
CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode
GuideMode = item.GuideMode,
PreRollFillerId = item.PreRollFillerId,
MidRollFillerId = item.MidRollFillerId,
PostRollFillerId = item.PostRollFillerId,
TailFillerId = item.TailFillerId,
FallbackFillerId = item.FallbackFillerId
},
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
};

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

@ -21,13 +21,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -21,13 +21,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
int? MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
ProgramScheduleItemCollectionType TailCollectionType,
int? TailCollectionId,
int? TailMultiCollectionId,
int? TailSmartCollectionId,
int? TailMediaItemId,
string CustomTitle,
GuideMode GuideMode) : IProgramScheduleItemRequest;
GuideMode GuideMode,
int? PreRollFillerId,
int? MidRollFillerId,
int? PostRollFillerId,
int? TailFillerId,
int? FallbackFillerId) : IProgramScheduleItemRequest;
public record ReplaceProgramScheduleItems
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<

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

@ -63,7 +63,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -63,7 +63,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleMustExist(dbContext, request.ProgramScheduleId)
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule))
.BindT(programSchedule => PlaybackOrdersMustBeValid(request, programSchedule));
.BindT(programSchedule => PlaybackOrdersMustBeValid(request, programSchedule))
.BindT(programSchedule => FillerConfigurationsMustBeValid(dbContext, request, programSchedule));
private static Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
ReplaceProgramScheduleItems request,
@ -77,6 +78,26 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -77,6 +78,26 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence()
.Map(_ => programSchedule);
private static async Task<Validation<BaseError, ProgramSchedule>> FillerConfigurationsMustBeValid(
TvContext dbContext,
ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule)
{
foreach (ReplaceProgramScheduleItem item in request.Items)
{
Either<BaseError, ProgramSchedule> result = await FillerConfigurationMustBeValid(
dbContext,
item,
programSchedule);
if (result.IsLeft)
{
return result.ToValidation();
}
}
return programSchedule;
}
private static Validation<BaseError, ProgramSchedule> PlaybackOrdersMustBeValid(
ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule)

81
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -41,25 +41,23 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -41,25 +41,23 @@ namespace ErsatzTV.Application.ProgramSchedules
duration.PlaybackOrder,
duration.PlayoutDuration,
duration.TailMode,
duration.TailCollectionType,
duration.TailCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailCollection)
duration.CustomTitle,
duration.GuideMode,
duration.PreRollFiller != null
? Filler.Mapper.ProjectToViewModel(duration.PreRollFiller)
: null,
duration.TailMultiCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailMultiCollection)
duration.MidRollFiller != null
? Filler.Mapper.ProjectToViewModel(duration.MidRollFiller)
: null,
duration.TailSmartCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailSmartCollection)
duration.PostRollFiller != null
? Filler.Mapper.ProjectToViewModel(duration.PostRollFiller)
: 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,
duration.GuideMode),
duration.TailFiller != null
? Filler.Mapper.ProjectToViewModel(duration.TailFiller)
: null,
duration.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller)
: null),
ProgramScheduleItemFlood flood =>
new ProgramScheduleItemFloodViewModel(
flood.Id,
@ -85,7 +83,22 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -85,7 +83,22 @@ namespace ErsatzTV.Application.ProgramSchedules
},
flood.PlaybackOrder,
flood.CustomTitle,
flood.GuideMode),
flood.GuideMode,
flood.PreRollFiller != null
? Filler.Mapper.ProjectToViewModel(flood.PreRollFiller)
: null,
flood.MidRollFiller != null
? Filler.Mapper.ProjectToViewModel(flood.MidRollFiller)
: null,
flood.PostRollFiller != null
? Filler.Mapper.ProjectToViewModel(flood.PostRollFiller)
: null,
flood.TailFiller != null
? Filler.Mapper.ProjectToViewModel(flood.TailFiller)
: null,
flood.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller)
: null),
ProgramScheduleItemMultiple multiple =>
new ProgramScheduleItemMultipleViewModel(
multiple.Id,
@ -112,7 +125,22 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -112,7 +125,22 @@ namespace ErsatzTV.Application.ProgramSchedules
multiple.PlaybackOrder,
multiple.Count,
multiple.CustomTitle,
multiple.GuideMode),
multiple.GuideMode,
multiple.PreRollFiller != null
? Filler.Mapper.ProjectToViewModel(multiple.PreRollFiller)
: null,
multiple.MidRollFiller != null
? Filler.Mapper.ProjectToViewModel(multiple.MidRollFiller)
: null,
multiple.PostRollFiller != null
? Filler.Mapper.ProjectToViewModel(multiple.PostRollFiller)
: null,
multiple.TailFiller != null
? Filler.Mapper.ProjectToViewModel(multiple.TailFiller)
: null,
multiple.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller)
: null),
ProgramScheduleItemOne one =>
new ProgramScheduleItemOneViewModel(
one.Id,
@ -138,7 +166,22 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -138,7 +166,22 @@ namespace ErsatzTV.Application.ProgramSchedules
},
one.PlaybackOrder,
one.CustomTitle,
one.GuideMode),
one.GuideMode,
one.PreRollFiller != null
? Filler.Mapper.ProjectToViewModel(one.PreRollFiller)
: null,
one.MidRollFiller != null
? Filler.Mapper.ProjectToViewModel(one.MidRollFiller)
: null,
one.PostRollFiller != null
? Filler.Mapper.ProjectToViewModel(one.PostRollFiller)
: null,
one.TailFiller != null
? Filler.Mapper.ProjectToViewModel(one.TailFiller)
: null,
one.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(one.FallbackFiller)
: null),
_ => throw new NotSupportedException(
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
};

32
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using ErsatzTV.Application.Filler;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
@ -20,13 +21,13 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -20,13 +21,13 @@ namespace ErsatzTV.Application.ProgramSchedules
PlaybackOrder playbackOrder,
TimeSpan playoutDuration,
TailMode tailMode,
ProgramScheduleItemCollectionType tailCollectionType,
MediaCollectionViewModel tailCollection,
MultiCollectionViewModel tailMultiCollection,
SmartCollectionViewModel tailSmartCollection,
NamedMediaItemViewModel tailMediaItem,
string customTitle,
GuideMode guideMode) : base(
GuideMode guideMode,
FillerPresetViewModel preRollFiller,
FillerPresetViewModel midRollFiller,
FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller) : base(
id,
index,
startType,
@ -39,25 +40,18 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -39,25 +40,18 @@ namespace ErsatzTV.Application.ProgramSchedules
mediaItem,
playbackOrder,
customTitle,
guideMode)
guideMode,
preRollFiller,
midRollFiller,
postRollFiller,
tailFiller,
fallbackFiller)
{
PlayoutDuration = playoutDuration;
TailMode = tailMode;
TailCollectionType = tailCollectionType;
TailCollection = tailCollection;
TailMultiCollection = tailMultiCollection;
TailSmartCollection = tailSmartCollection;
TailMediaItem = tailMediaItem;
}
public TimeSpan PlayoutDuration { 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; }
}
}

15
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using ErsatzTV.Application.Filler;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
@ -19,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -19,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules
NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder,
string customTitle,
GuideMode guideMode) : base(
GuideMode guideMode,
FillerPresetViewModel preRollFiller,
FillerPresetViewModel midRollFiller,
FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller) : base(
id,
index,
startType,
@ -32,7 +38,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -32,7 +38,12 @@ namespace ErsatzTV.Application.ProgramSchedules
mediaItem,
playbackOrder,
customTitle,
guideMode)
guideMode,
preRollFiller,
midRollFiller,
postRollFiller,
tailFiller,
fallbackFiller)
{
}
}

15
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using ErsatzTV.Application.Filler;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
@ -20,7 +21,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -20,7 +21,12 @@ namespace ErsatzTV.Application.ProgramSchedules
PlaybackOrder playbackOrder,
int count,
string customTitle,
GuideMode guideMode) : base(
GuideMode guideMode,
FillerPresetViewModel preRollFiller,
FillerPresetViewModel midRollFiller,
FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller) : base(
id,
index,
startType,
@ -33,7 +39,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -33,7 +39,12 @@ namespace ErsatzTV.Application.ProgramSchedules
mediaItem,
playbackOrder,
customTitle,
guideMode) =>
guideMode,
preRollFiller,
midRollFiller,
postRollFiller,
tailFiller,
fallbackFiller) =>
Count = count;
public int Count { get; }

15
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using ErsatzTV.Application.Filler;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
@ -19,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -19,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules
NamedMediaItemViewModel mediaItem,
PlaybackOrder playbackOrder,
string customTitle,
GuideMode guideMode) : base(
GuideMode guideMode,
FillerPresetViewModel preRollFiller,
FillerPresetViewModel midRollFiller,
FillerPresetViewModel postRollFiller,
FillerPresetViewModel tailFiller,
FillerPresetViewModel fallbackFiller) : base(
id,
index,
startType,
@ -32,7 +38,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -32,7 +38,12 @@ namespace ErsatzTV.Application.ProgramSchedules
mediaItem,
playbackOrder,
customTitle,
guideMode)
guideMode,
preRollFiller,
midRollFiller,
postRollFiller,
tailFiller,
fallbackFiller)
{
}
}

8
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using ErsatzTV.Application.Filler;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
@ -18,7 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -18,7 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules
NamedMediaItemViewModel MediaItem,
PlaybackOrder PlaybackOrder,
string CustomTitle,
GuideMode GuideMode)
GuideMode GuideMode,
FillerPresetViewModel PreRollFiller,
FillerPresetViewModel MidRollFiller,
FillerPresetViewModel PostRollFiller,
FillerPresetViewModel TailFiller,
FillerPresetViewModel FallbackFiller)
{
public string Name => CollectionType switch
{

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

@ -30,10 +30,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries @@ -30,10 +30,6 @@ 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)
@ -49,6 +45,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries @@ -49,6 +45,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Artist).ArtistMetadata)
.ThenInclude(am => am.Artwork)
.Include(i => i.PreRollFiller)
.Include(i => i.MidRollFiller)
.Include(i => i.PostRollFiller)
.Include(i => i.TailFiller)
.Include(i => i.FallbackFiller)
.ToListAsync(cancellationToken)
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel).ToList());
}

2
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -161,7 +161,7 @@ namespace ErsatzTV.Application.Streaming @@ -161,7 +161,7 @@ namespace ErsatzTV.Application.Streaming
Process process = processModel.Process;
_logger.LogDebug(
_logger.LogInformation(
"ffmpeg hls arguments {FFmpegArguments}",
string.Join(" ", process.StartInfo.ArgumentList));

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

@ -114,11 +114,15 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -114,11 +114,15 @@ namespace ErsatzTV.Application.Streaming.Queries
version,
playoutItemWithPath.Path,
playoutItemWithPath.PlayoutItem.StartOffset,
playoutItemWithPath.PlayoutItem.FinishOffset,
request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now,
maybeGlobalWatermark,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice,
request.HlsRealtime);
request.HlsRealtime,
playoutItemWithPath.PlayoutItem.FillerKind,
playoutItemWithPath.PlayoutItem.InPoint,
playoutItemWithPath.PlayoutItem.OutPoint);
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);

162
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs

@ -29,7 +29,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -29,7 +29,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ThreadCount.Should().Be(7);
}
@ -46,7 +48,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -46,7 +48,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ThreadCount.Should().Be(7);
}
@ -63,7 +67,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -63,7 +67,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length);
@ -82,7 +88,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -82,7 +88,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length);
@ -101,7 +109,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -101,7 +109,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.RealtimeOutput.Should().BeTrue();
}
@ -118,7 +128,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -118,7 +128,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.RealtimeOutput.Should().BeTrue();
}
@ -137,7 +149,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -137,7 +149,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
now,
now.AddMinutes(5));
now.AddMinutes(5),
TimeSpan.Zero,
TimeSpan.Zero);
actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@ -157,7 +171,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -157,7 +171,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
now,
now.AddMinutes(5));
now.AddMinutes(5),
TimeSpan.Zero,
TimeSpan.Zero);
actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@ -175,7 +191,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -175,7 +191,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
}
@ -199,7 +217,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -199,7 +217,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
}
@ -223,7 +243,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -223,7 +243,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
}
@ -247,7 +269,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -247,7 +269,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -273,7 +297,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -273,7 +297,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -298,7 +324,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -298,7 +324,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
scaledSize.Width.Should().Be(1280);
@ -326,7 +354,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -326,7 +354,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -352,7 +382,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -352,7 +382,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -379,7 +411,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -379,7 +411,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -409,7 +443,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -409,7 +443,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream { Codec = "mpeg2video" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -439,7 +475,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -439,7 +475,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream { Codec = "mpeg2video" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -468,7 +506,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -468,7 +506,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream { Codec = "libx264" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -498,7 +538,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -498,7 +538,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream { Codec = "mpeg2video" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -530,7 +572,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -530,7 +572,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream { Codec = "mpeg2video" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -560,7 +604,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -560,7 +604,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -589,7 +635,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -589,7 +635,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream { Codec = "mpeg2video" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -617,7 +665,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -617,7 +665,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue();
@ -647,7 +697,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -647,7 +697,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream { Codec = "mpeg2video" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
@ -673,7 +725,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -673,7 +725,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "aac" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioCodec.Should().Be("aac");
}
@ -696,7 +750,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -696,7 +750,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioCodec.Should().Be("copy");
}
@ -720,7 +776,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -720,7 +776,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioCodec.Should().Be("aac");
}
@ -744,7 +802,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -744,7 +802,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioCodec.Should().Be("copy");
}
@ -769,7 +829,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -769,7 +829,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioBitrate.IfNone(0).Should().Be(2424);
}
@ -794,7 +856,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -794,7 +856,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioBufferSize.IfNone(0).Should().Be(2424);
}
@ -819,7 +883,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -819,7 +883,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioChannels.IfNone(0).Should().Be(6);
}
@ -844,7 +910,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -844,7 +910,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioSampleRate.IfNone(0).Should().Be(48);
}
@ -868,7 +936,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -868,7 +936,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioChannels.IfNone(0).Should().Be(6);
}
@ -892,7 +962,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -892,7 +962,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.AudioSampleRate.IfNone(0).Should().Be(48);
}
@ -908,7 +980,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -908,7 +980,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioCodec = "ac3"
};
var version = new MediaVersion { Duration = TimeSpan.FromMinutes(2) };
var version = new MediaVersion { Duration = TimeSpan.FromMinutes(5) }; // not pulled from here
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@ -917,7 +989,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -917,7 +989,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.FromMinutes(2));
actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2));
}
@ -941,7 +1015,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -941,7 +1015,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.NormalizeLoudness.Should().BeTrue();
}
@ -965,7 +1041,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -965,7 +1041,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream { Codec = "ac3" },
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.NormalizeLoudness.Should().BeFalse();
}
@ -991,7 +1069,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -991,7 +1069,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv);
}

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

@ -5,6 +5,7 @@ using System.IO; @@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
@ -184,11 +185,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -184,11 +185,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
v,
file,
now,
now + TimeSpan.FromSeconds(5),
now,
None,
VaapiDriver.Default,
"/dev/dri/renderD128",
false);
false,
FillerKind.None,
TimeSpan.Zero,
TimeSpan.FromSeconds(5));
process.StartInfo.RedirectStandardError = true;

3
ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs

@ -26,7 +26,8 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -26,7 +26,8 @@ namespace ErsatzTV.Core.Tests.Metadata
var input = new LocalStatisticsProvider.FFprobe(
new LocalStatisticsProvider.FFprobeFormat("123.45"),
new List<LocalStatisticsProvider.FFprobeStream>());
new List<LocalStatisticsProvider.FFprobeStream>(),
new List<LocalStatisticsProvider.FFprobeChapter>());
MediaVersion result = provider.ProjectToMediaVersion("test", input);

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

@ -3,6 +3,7 @@ using System.Collections.Generic; @@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Core.Tests.Fakes;
@ -1164,7 +1165,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1164,7 +1165,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
}
};
var collectionTwo = new Collection
{
Id = 2,
@ -1174,7 +1175,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1174,7 +1175,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
}
};
var collectionThree = new Collection
{
Id = 3,
@ -1184,13 +1185,13 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1184,13 +1185,13 @@ namespace ErsatzTV.Core.Tests.Scheduling
TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1))
}
};
var fakeRepository = new FakeMediaCollectionRepository(
Map(
(collectionOne.Id, collectionOne.MediaItems.ToList()),
(collectionTwo.Id, collectionTwo.MediaItems.ToList()),
(collectionThree.Id, collectionThree.MediaItems.ToList())));
var items = new List<ProgramScheduleItem>
{
new ProgramScheduleItemDuration
@ -1203,9 +1204,12 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1203,9 +1204,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
PlayoutDuration = TimeSpan.FromHours(3),
PlaybackOrder = PlaybackOrder.Chronological,
TailMode = TailMode.Filler,
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
TailCollection = collectionThree,
TailCollectionId = collectionThree.Id
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
},
new ProgramScheduleItemDuration
{
@ -1217,12 +1221,15 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1217,12 +1221,15 @@ namespace ErsatzTV.Core.Tests.Scheduling
PlayoutDuration = TimeSpan.FromHours(3),
PlaybackOrder = PlaybackOrder.Chronological,
TailMode = TailMode.Filler,
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
TailCollection = collectionThree,
TailCollectionId = collectionThree.Id
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
}
};
var playout = new Playout
{
ProgramSchedule = new ProgramSchedule
@ -1231,7 +1238,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1231,7 +1238,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
@ -1241,42 +1248,42 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1241,42 +1248,42 @@ namespace ErsatzTV.Core.Tests.Scheduling
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
result.Items.Count.Should().Be(12);
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0));
result.Items[0].MediaItemId.Should().Be(1);
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(55));
result.Items[1].MediaItemId.Should().Be(1);
result.Items[2].StartOffset.TimeOfDay.Should().Be(new TimeSpan(1, 50, 0));
result.Items[2].MediaItemId.Should().Be(1);
result.Items[3].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 45, 0));
result.Items[3].MediaItemId.Should().Be(3);
result.Items[4].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 50, 0));
result.Items[4].MediaItemId.Should().Be(3);
result.Items[5].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 55, 0));
result.Items[5].MediaItemId.Should().Be(3);
result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
result.Items[6].MediaItemId.Should().Be(2);
result.Items[7].StartOffset.TimeOfDay.Should().Be(new TimeSpan(3, 55, 0));
result.Items[7].MediaItemId.Should().Be(2);
result.Items[8].StartOffset.TimeOfDay.Should().Be(new TimeSpan(4, 50, 0));
result.Items[8].MediaItemId.Should().Be(2);
result.Items[9].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 45, 0));
result.Items[9].MediaItemId.Should().Be(3);
result.Items[10].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 50, 0));
result.Items[10].MediaItemId.Should().Be(3);
result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0));
result.Items[11].MediaItemId.Should().Be(3);
result.Anchor.NextScheduleItem.Should().Be(items[0]);
result.Anchor.DurationFinish.Should().BeNull();
}

205
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs

@ -0,0 +1,205 @@ @@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
using FluentAssertions;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class PlayoutModeSchedulerBaseTests
{
[Test]
public void CalculateEndTimeWithFiller_Should_Not_Touch_Enumerator()
{
var collection = new Collection
{
Id = 1,
Name = "Filler Items",
MediaItems = new List<MediaItem>()
};
for (var i = 0; i < 5; i++)
{
collection.MediaItems.Add(TestMovie(i + 1, TimeSpan.FromHours(i + 1), new DateTime(2020, 2, i + 1)));
}
var fillerPreset = new FillerPreset
{
FillerKind = FillerKind.PreRoll,
FillerMode = FillerMode.Count,
Count = 3,
Collection = collection,
CollectionId = collection.Id
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collection.MediaItems,
new CollectionEnumeratorState { Index = 0, Seed = 1 });
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>
{
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator }
},
new ProgramScheduleItemOne
{
PreRollFiller = fillerPreset
},
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 18, 12, 30, TimeSpan.FromHours(-5)));
enumerator.State.Index.Should().Be(0);
enumerator.State.Seed.Should().Be(1);
}
[Test]
public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_15()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_30()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 16, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_45()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_00()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 46, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_30()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30
}
},
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_00()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30
}
},
new DateTimeOffset(2020, 2, 1, 12, 20, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5)));
}
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) =>
new()
{
Id = id,
MovieMetadata = new List<MovieMetadata> { new() { ReleaseDate = aired } },
MediaVersions = new List<MediaVersion>
{
new() { Duration = duration }
}
};
}
}

552
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs

@ -0,0 +1,552 @@ @@ -0,0 +1,552 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Scheduling;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
{
[Test]
public void Should_Fill_Exact_Duration()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
var scheduleItem = new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
TailMode = TailMode.None,
PlaybackOrder = PlaybackOrder.Chronological
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Not_Have_Gap_Duration_Tail_Mode_None()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
var scheduleItem = new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
TailMode = TailMode.None,
PlaybackOrder = PlaybackOrder.Chronological
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Have_Gap_Duration_Tail_Mode_Offline_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
var scheduleItem = new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
TailMode = TailMode.Offline,
PlaybackOrder = PlaybackOrder.Chronological
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
// duration block should end after exact duration, with gap
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Not_Have_Gap_Duration_Tail_Mode_Offline_With_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
TailMode = TailMode.Offline,
PlaybackOrder = PlaybackOrder.Chronological,
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_Exact_Duration()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
TailMode = TailMode.Filler,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(1);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Have_Gap_Duration_Tail_Mode_Filler_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
var scheduleItem = new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
TailMode = TailMode.Filler,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(1);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_With_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
TailMode = TailMode.Filler,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(
scheduleItem,
enumerator1,
scheduleItem.TailFiller,
enumerator2,
scheduleItem.FallbackFiller,
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
enumerator3.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(7);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(1);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[6].MediaItemId.Should().Be(5);
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[6].GuideGroup.Should().Be(1);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
}
}
}

715
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs

@ -0,0 +1,715 @@ @@ -0,0 +1,715 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Scheduling;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
{
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item_With_Post_Roll_Multiple_One()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
PostRollFiller = new FillerPreset
{
FillerKind = FillerKind.PostRoll,
FillerMode = FillerMode.Count,
Count = 1,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
TailFiller = null,
FallbackFiller = null
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[2].MediaItemId.Should().Be(2);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems[2].GuideGroup.Should().Be(2);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(4);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(2);
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[4].MediaItemId.Should().Be(1);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.None);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.PostRoll);
}
[Test]
public void Should_Have_Gap_With_No_Tail_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Not_Have_Gap_With_Exact_Tail()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = null
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Not_Have_Gap_With_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Have_Gap_With_Tail_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = null
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Not_Have_Gap_With_Tail_And_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(
scheduleItem,
enumerator1,
scheduleItem.TailFiller,
enumerator2,
scheduleItem.FallbackFiller,
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
enumerator3.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(7);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[6].MediaItemId.Should().Be(5);
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[6].GuideGroup.Should().Be(3);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var sortedScheduleItems = new List<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(
scheduleItem,
enumerator1,
scheduleItem.TailFiller,
enumerator2,
scheduleItem.FallbackFiller,
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(0);
enumerator3.State.Index.Should().Be(0);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{
StartTime = TimeSpan.FromHours(3)
};
}
}

630
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs

@ -0,0 +1,630 @@ @@ -0,0 +1,630 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Scheduling;
using FluentAssertions;
using LanguageExt;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
{
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 3
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
}.ToMap();
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Have_Gap_With_No_Tail_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null,
Count = 3
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
}.ToMap();
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Not_Have_Gap_With_Exact_Tail()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = null,
Count = 3
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
}.ToMap();
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(1);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Not_Have_Gap_With_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
Count = 3
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionTwo.MediaItems }
}.ToMap();
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Have_Gap_With_Tail_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = null,
Count = 3
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
}.ToMap();
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(1);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Not_Have_Gap_With_Tail_And_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
},
Count = 3
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
}.ToMap();
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(
scheduleItem,
enumerator1,
scheduleItem.TailFiller,
enumerator2,
scheduleItem.FallbackFiller,
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
enumerator3.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(7);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(1);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[6].MediaItemId.Should().Be(5);
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[6].GuideGroup.Should().Be(1);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
},
Count = 3
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
}.ToMap();
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(
scheduleItem,
enumerator1,
scheduleItem.TailFiller,
enumerator2,
scheduleItem.FallbackFiller,
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(0);
enumerator3.State.Index.Should().Be(0);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
}
protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{
StartTime = TimeSpan.FromHours(3)
};
}
}

460
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs

@ -0,0 +1,460 @@ @@ -0,0 +1,460 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Scheduling;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Scheduling
{
[TestFixture]
public class PlayoutModeSchedulerOneTests : SchedulerTestBase
{
[Test]
public void Should_Have_Gap_With_No_Tail_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = null
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(1);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Not_Have_Gap_With_Exact_Tail()
{
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = null
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Not_Have_Gap_With_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = null,
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(2);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Have_Gap_With_Tail_No_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = null
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
}
[Test]
public void Should_Not_Have_Gap_With_Tail_And_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(
scheduleItem,
enumerator1,
scheduleItem.TailFiller,
enumerator2,
scheduleItem.FallbackFiller,
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1);
enumerator3.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(5);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(5);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(3));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
Collection = collectionTwo,
CollectionId = collectionTwo.Id
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
Collection = collectionThree,
CollectionId = collectionThree.Id
}
};
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState,
CollectionEnumerators(
scheduleItem,
enumerator1,
scheduleItem.TailFiller,
enumerator2,
scheduleItem.FallbackFiller,
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop);
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(0);
enumerator3.State.Index.Should().Be(0);
playoutItems.Count.Should().Be(1);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
}
protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{
StartTime = TimeSpan.FromHours(3)
};
}
}

78
ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
using LanguageExt;
namespace ErsatzTV.Core.Tests.Scheduling
{
public abstract class SchedulerTestBase
{
protected static PlayoutBuilderState StartState => new(
0,
Prelude.None,
Prelude.None,
false,
false,
1,
new DateTimeOffset(new DateTime(2020, 10, 18, 0, 0, 0, DateTimeKind.Local)));
protected virtual ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{
StartTime = null
};
protected static DateTimeOffset HardStop => StartState.CurrentTime.AddHours(6);
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator) =>
new()
{
{ CollectionKey.ForScheduleItem(scheduleItem), enumerator }
};
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator1,
FillerPreset fillerPreset, IMediaCollectionEnumerator enumerator2,
FillerPreset fillerPreset2, IMediaCollectionEnumerator enumerator3) =>
new()
{
{ CollectionKey.ForScheduleItem(scheduleItem), enumerator1 },
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator2 },
{ CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 }
};
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) =>
new()
{
Id = id,
MovieMetadata = new List<MovieMetadata> { new() { ReleaseDate = aired } },
MediaVersions = new List<MediaVersion>
{
new() { Duration = duration }
}
};
protected static Collection TwoItemCollection(int id1, int id2, TimeSpan duration) => new()
{
Id = id1,
Name = $"Collection of Items {id1}",
MediaItems = new List<MediaItem>
{
TestMovie(id1, duration, new DateTime(2020, 1, 1)),
TestMovie(id2, duration, new DateTime(2020, 1, 2))
}
};
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator1,
FillerPreset fillerPreset, IMediaCollectionEnumerator enumerator2) =>
new()
{
{ CollectionKey.ForScheduleItem(scheduleItem), enumerator1 },
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator2 }
};
}
}

2
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -25,8 +25,10 @@ @@ -25,8 +25,10 @@
public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size");
public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size");
public static ConfigElementKey PlayoutsDetailPageSize => new("pages.playouts.detail_page_size");
public static ConfigElementKey PlayoutsDetailShowFiller => new("pages.playouts.detail_show_filler");
public static ConfigElementKey LogsPageSize => new("pages.logs.page_size");
public static ConfigElementKey TraktListsPageSize => new("pages.trakt.lists_page_size");
public static ConfigElementKey FillerPresetsPageSize => new("pages.filler_presets.page_size");
public static ConfigElementKey LibraryRefreshInterval => new("scanner.library_refresh_interval");
public static ConfigElementKey PlayoutDaysToBuild => new("playout.days_to_build");
}

12
ErsatzTV.Core/Domain/Filler/FillerKind.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Domain.Filler
{
public enum FillerKind
{
None = 0,
PreRoll = 1,
MidRoll = 2,
PostRoll = 3,
Tail = 4,
Fallback = 5
}
}

10
ErsatzTV.Core/Domain/Filler/FillerMode.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Domain.Filler
{
public enum FillerMode
{
None = 0,
Duration = 1,
Count = 2,
Pad = 3
}
}

24
ErsatzTV.Core/Domain/Filler/FillerPreset.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using System;
namespace ErsatzTV.Core.Domain.Filler
{
public class FillerPreset
{
public int Id { get; set; }
public string Name { get; set; }
public FillerKind FillerKind { get; set; }
public FillerMode FillerMode { get; set; }
public TimeSpan? Duration { get; set; }
public int? Count { get; set; }
public int? PadToNearestMinute { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public Collection Collection { get; set; }
public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
public int? MultiCollectionId { get; set; }
public MultiCollection MultiCollection { get; set; }
public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; }
}
}

15
ErsatzTV.Core/Domain/MediaItem/MediaChapter.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using System;
namespace ErsatzTV.Core.Domain
{
public class MediaChapter
{
public int Id { get; set; }
public int MediaVersionId { get; set; }
public MediaVersion MediaVersion { get; set; }
public long ChapterId { get; set; }
public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
public string Title { get; set; }
}
}

1
ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs

@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Domain @@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Domain
public string Name { get; set; }
public List<MediaFile> MediaFiles { get; set; }
public List<MediaStream> Streams { get; set; }
public List<MediaChapter> Chapters { get; set; }
public TimeSpan Duration { get; set; }
public string SampleAspectRatio { get; set; }
public string DisplayAspectRatio { get; set; }

1
ErsatzTV.Core/Domain/PlayoutAnchor.cs

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

26
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Core.Domain
{
@ -13,15 +14,36 @@ namespace ErsatzTV.Core.Domain @@ -13,15 +14,36 @@ namespace ErsatzTV.Core.Domain
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 GuideGroup { get; set; }
public FillerKind FillerKind { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public TimeSpan InPoint { get; set; }
public TimeSpan OutPoint { get; set; }
public string ChapterTitle { 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;
public PlayoutItem ForChapter(MediaChapter chapter) =>
new()
{
MediaItemId = MediaItemId,
MediaItem = MediaItem,
Start = Start,
Finish = Start + chapter.EndTime - chapter.StartTime,
GuideFinish = GuideFinish,
CustomTitle = CustomTitle,
GuideGroup = GuideGroup,
FillerKind = FillerKind,
PlayoutId = PlayoutId,
Playout = Playout,
InPoint = chapter.StartTime,
OutPoint = chapter.EndTime,
ChapterTitle = chapter.Title
};
}
}

11
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Core.Domain
{
@ -22,5 +23,15 @@ namespace ErsatzTV.Core.Domain @@ -22,5 +23,15 @@ namespace ErsatzTV.Core.Domain
public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; }
public PlaybackOrder PlaybackOrder { get; set; }
public int? PreRollFillerId { get; set; }
public FillerPreset PreRollFiller { get; set; }
public int? MidRollFillerId { get; set; }
public FillerPreset MidRollFiller { get; set; }
public int? PostRollFillerId { get; set; }
public FillerPreset PostRollFiller { get; set; }
public int? TailFillerId { get; set; }
public FillerPreset TailFiller { get; set; }
public int? FallbackFillerId { get; set; }
public FillerPreset FallbackFiller { get; set; }
}
}

9
ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs

@ -6,14 +6,5 @@ namespace ErsatzTV.Core.Domain @@ -6,14 +6,5 @@ namespace ErsatzTV.Core.Domain
{
public TimeSpan PlayoutDuration { 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/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -49,7 +49,9 @@ namespace ErsatzTV.Core.FFmpeg @@ -49,7 +49,9 @@ namespace ErsatzTV.Core.FFmpeg
MediaStream videoStream,
Option<MediaStream> audioStream,
DateTimeOffset start,
DateTimeOffset now)
DateTimeOffset now,
TimeSpan inPoint,
TimeSpan outPoint)
{
var result = new FFmpegPlaybackSettings
{
@ -57,9 +59,9 @@ namespace ErsatzTV.Core.FFmpeg @@ -57,9 +59,9 @@ namespace ErsatzTV.Core.FFmpeg
FormatFlags = CommonFormatFlags
};
if (now != start)
if (now != start || inPoint != TimeSpan.Zero)
{
result.StreamSeek = now - start;
result.StreamSeek = now - start + inPoint;
}
switch (streamingMode)
@ -140,7 +142,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -140,7 +142,7 @@ namespace ErsatzTV.Core.FFmpeg
});
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
result.AudioDuration = version.Duration;
result.AudioDuration = outPoint - inPoint;
result.NormalizeLoudness = ffmpegProfile.NormalizeLoudness;
}
else

10
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -141,10 +141,14 @@ namespace ErsatzTV.Core.FFmpeg @@ -141,10 +141,14 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithInfiniteLoop()
public FFmpegProcessBuilder WithInfiniteLoop(bool loop = true)
{
_arguments.Add("-stream_loop");
_arguments.Add("-1");
if (loop)
{
_arguments.Add("-stream_loop");
_arguments.Add("-1");
}
return this;
}

14
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -3,6 +3,7 @@ using System.Diagnostics; @@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
@ -37,11 +38,15 @@ namespace ErsatzTV.Core.FFmpeg @@ -37,11 +38,15 @@ namespace ErsatzTV.Core.FFmpeg
MediaVersion version,
string path,
DateTimeOffset start,
DateTimeOffset finish,
DateTimeOffset now,
Option<ChannelWatermark> globalWatermark,
VaapiDriver vaapiDriver,
string vaapiDevice,
bool hlsRealtime)
bool hlsRealtime,
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, version);
@ -53,7 +58,9 @@ namespace ErsatzTV.Core.FFmpeg @@ -53,7 +58,9 @@ namespace ErsatzTV.Core.FFmpeg
videoStream,
maybeAudioStream,
start,
now);
now,
inPoint,
outPoint);
(Option<ChannelWatermark> maybeWatermark, Option<string> maybeWatermarkPath) =
GetWatermarkOptions(channel, globalWatermark);
@ -70,6 +77,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -70,6 +77,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithSeek(playbackSettings.StreamSeek)
.WithInfiniteLoop(fillerKind == FillerKind.Fallback)
.WithInputCodec(path, playbackSettings.VideoDecoder, videoStream.Codec, videoStream.PixelFormat)
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
@ -114,7 +122,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -114,7 +122,7 @@ namespace ErsatzTV.Core.FFmpeg
builder = builder.WithPlaybackArgs(playbackSettings)
.WithMetadata(channel, maybeAudioStream)
.WithDuration(start + version.Duration - now);
.WithDuration(finish - now);
switch (channel.StreamingMode)
{

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

@ -8,5 +8,6 @@ namespace ErsatzTV.Core.Interfaces.Scheduling @@ -8,5 +8,6 @@ namespace ErsatzTV.Core.Interfaces.Scheduling
CollectionEnumeratorState State { get; }
Option<MediaItem> Current { get; }
void MoveNext();
Option<MediaItem> Peek(int offset);
}
}

17
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutModeScheduler.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Interfaces.Scheduling
{
public interface IPlayoutModeScheduler<in T> where T : ProgramScheduleItem
{
Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
T scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop);
}
}

34
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -5,6 +5,7 @@ using System.Linq; @@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Xml;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin;
using LanguageExt;
@ -70,7 +71,7 @@ namespace ErsatzTV.Core.Iptv @@ -70,7 +71,7 @@ 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)
while (i < sorted.Count && sorted[i].FillerKind != FillerKind.None && sorted[i].FillerKind != FillerKind.PreRoll)
{
i++;
}
@ -78,23 +79,28 @@ namespace ErsatzTV.Core.Iptv @@ -78,23 +79,28 @@ namespace ErsatzTV.Core.Iptv
while (i < sorted.Count)
{
PlayoutItem startItem = sorted[i];
int j = i;
while (j + 1 < sorted.Count && sorted[j].FillerKind != FillerKind.None)
{
j++;
}
PlayoutItem displayItem = sorted[j];
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
int finishIndex = i;
while (finishIndex + 1 < sorted.Count && (hasCustomTitle && sorted[finishIndex + 1].CustomGroup ||
sorted[finishIndex + 1].IsFiller))
while (finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup)
{
finishIndex++;
}
int customShowId = -1;
if (sorted[i].MediaItem is Episode ep)
if (displayItem.MediaItem is Episode ep)
{
customShowId = ep.Season.ShowId;
}
bool isSameCustomShow = hasCustomTitle;
for (int x = i; x <= finishIndex; x++)
for (int x = j; x <= finishIndex; x++)
{
isSameCustomShow = isSameCustomShow && sorted[x].MediaItem is Episode e &&
customShowId == e.Season.ShowId;
@ -104,14 +110,14 @@ namespace ErsatzTV.Core.Iptv @@ -104,14 +110,14 @@ namespace ErsatzTV.Core.Iptv
i = finishIndex;
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
string stop = startItem.GuideFinishOffset.HasValue
? startItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty)
string stop = displayItem.GuideFinishOffset.HasValue
? displayItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty)
: finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
string title = GetTitle(startItem);
string subtitle = GetSubtitle(startItem);
string description = GetDescription(startItem);
Option<ContentRating> contentRating = GetContentRating(startItem);
string title = GetTitle(displayItem);
string subtitle = GetSubtitle(displayItem);
string description = GetDescription(displayItem);
Option<ContentRating> contentRating = GetContentRating(displayItem);
xml.WriteStartElement("programme");
xml.WriteAttributeString("start", start);
@ -142,7 +148,7 @@ namespace ErsatzTV.Core.Iptv @@ -142,7 +148,7 @@ namespace ErsatzTV.Core.Iptv
}
}
if (!hasCustomTitle && startItem.MediaItem is Movie movie)
if (!hasCustomTitle && displayItem.MediaItem is Movie movie)
{
foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone())
{
@ -175,7 +181,7 @@ namespace ErsatzTV.Core.Iptv @@ -175,7 +181,7 @@ namespace ErsatzTV.Core.Iptv
}
}
if (!hasCustomTitle && startItem.MediaItem is MusicVideo musicVideo)
if (!hasCustomTitle && displayItem.MediaItem is MusicVideo musicVideo)
{
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
{
@ -208,7 +214,7 @@ namespace ErsatzTV.Core.Iptv @@ -208,7 +214,7 @@ namespace ErsatzTV.Core.Iptv
}
}
if (startItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow))
if (displayItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow))
{
Option<ShowMetadata> maybeMetadata =
Optional(episode.Season?.Show?.ShowMetadata.HeadOrNone()).Flatten();

76
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -110,6 +110,7 @@ namespace ErsatzTV.Core.Metadata @@ -110,6 +110,7 @@ namespace ErsatzTV.Core.Metadata
startInfo.ArgumentList.Add("json");
startInfo.ArgumentList.Add("-show_format");
startInfo.ArgumentList.Add("-show_streams");
startInfo.ArgumentList.Add("-show_chapters");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(filePath);
@ -138,9 +139,18 @@ namespace ErsatzTV.Core.Metadata @@ -138,9 +139,18 @@ namespace ErsatzTV.Core.Metadata
json =>
{
var version = new MediaVersion
{ Name = "Main", DateAdded = DateTime.UtcNow, Streams = new List<MediaStream>() };
if (double.TryParse(json.format.duration, NumberStyles.Number, CultureInfo.InvariantCulture, out double duration))
{
Name = "Main",
DateAdded = DateTime.UtcNow,
Streams = new List<MediaStream>(),
Chapters = new List<MediaChapter>()
};
if (double.TryParse(
json.format.duration,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double duration))
{
var seconds = TimeSpan.FromSeconds(duration);
version.Duration = seconds;
@ -240,10 +250,56 @@ namespace ErsatzTV.Core.Metadata @@ -240,10 +250,56 @@ namespace ErsatzTV.Core.Metadata
version.Streams.Add(stream);
}
foreach (FFprobeChapter probedChapter in json.chapters)
{
if (double.TryParse(
probedChapter.start_time,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double startTime)
&& double.TryParse(
probedChapter.end_time,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double endTime))
{
var chapter = new MediaChapter
{
MediaVersionId = version.Id,
ChapterId = probedChapter.id,
StartTime = TimeSpan.FromSeconds(startTime),
EndTime = TimeSpan.FromSeconds(endTime),
Title = probedChapter?.tags?.title
};
version.Chapters.Add(chapter);
}
else
{
_logger.LogWarning(
"Media item at {Path} has a missing or invalid chapter start/end time",
path);
}
}
if (version.Chapters.Any())
{
MediaChapter last = version.Chapters.Last();
if (last.EndTime != version.Duration)
{
last.EndTime = version.Duration;
}
}
return version;
},
_ => new MediaVersion
{ Name = "Main", DateAdded = DateTime.UtcNow, Streams = new List<MediaStream>() });
{
Name = "Main",
DateAdded = DateTime.UtcNow,
Streams = new List<MediaStream>(),
Chapters = new List<MediaChapter>()
});
private VideoScanKind ScanKindFromFieldOrder(string fieldOrder) =>
fieldOrder?.ToLowerInvariant() switch
@ -254,13 +310,13 @@ namespace ErsatzTV.Core.Metadata @@ -254,13 +310,13 @@ namespace ErsatzTV.Core.Metadata
};
// ReSharper disable InconsistentNaming
public record FFprobe(FFprobeFormat format, List<FFprobeStream> streams);
public record FFprobe(FFprobeFormat format, List<FFprobeStream> streams, List<FFprobeChapter> chapters);
public record FFprobeFormat(string duration);
public record FFprobeDisposition(int @default, int forced);
public record FFProbeTags(string language, string title);
public record FFprobeTags(string language, string title);
public record FFprobeStream(
int index,
@ -277,7 +333,13 @@ namespace ErsatzTV.Core.Metadata @@ -277,7 +333,13 @@ namespace ErsatzTV.Core.Metadata
string r_frame_rate,
string bits_per_raw_sample,
FFprobeDisposition disposition,
FFProbeTags tags);
FFprobeTags tags);
public record FFprobeChapter(
long id,
string start_time,
string end_time,
FFprobeTags tags);
// ReSharper restore InconsistentNaming
}
}

2
ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs

@ -119,8 +119,6 @@ namespace ErsatzTV.Core.Metadata @@ -119,8 +119,6 @@ namespace ErsatzTV.Core.Metadata
foreach (string file in allFiles.OrderBy(identity))
{
_logger.LogDebug("Other video found at {File}", file);
Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository
.GetOrAdd(libraryPath, file)
.BindT(video => UpdateStatistics(video, ffprobePath))

6
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
@ -36,5 +37,8 @@ namespace ErsatzTV.Core.Scheduling @@ -36,5 +37,8 @@ namespace ErsatzTV.Core.Scheduling
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<MediaItem> Peek(int offset) =>
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
}
}

88
ErsatzTV.Core/Scheduling/CollectionKey.cs

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
using System;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using LanguageExt;
namespace ErsatzTV.Core.Scheduling
{
public class CollectionKey : Record<CollectionKey>
{
public static CollectionKey ForScheduleItem(ProgramScheduleItem item) =>
item.CollectionType switch
{
ProgramScheduleItemCollectionType.Collection => new CollectionKey
{
CollectionType = item.CollectionType,
CollectionId = item.CollectionId
},
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId
},
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId
},
ProgramScheduleItemCollectionType.Artist => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId
},
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
{
CollectionType = item.CollectionType,
MultiCollectionId = item.MultiCollectionId
},
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
{
CollectionType = item.CollectionType,
SmartCollectionId = item.SmartCollectionId
},
_ => throw new ArgumentOutOfRangeException(nameof(item))
};
public static CollectionKey ForFillerPreset(FillerPreset filler) =>
filler.CollectionType switch
{
ProgramScheduleItemCollectionType.Collection => new CollectionKey
{
CollectionType = filler.CollectionType,
CollectionId = filler.CollectionId
},
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
{
CollectionType = filler.CollectionType,
MediaItemId = filler.MediaItemId
},
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
{
CollectionType = filler.CollectionType,
MediaItemId = filler.MediaItemId
},
ProgramScheduleItemCollectionType.Artist => new CollectionKey
{
CollectionType = filler.CollectionType,
MediaItemId = filler.MediaItemId
},
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
{
CollectionType = filler.CollectionType,
MultiCollectionId = filler.MultiCollectionId
},
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
{
CollectionType = filler.CollectionType,
SmartCollectionId = filler.SmartCollectionId
},
_ => throw new ArgumentOutOfRangeException(nameof(filler))
};
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public int? MultiCollectionId { get; set; }
public int? SmartCollectionId { get; set; }
public int? MediaItemId { get; set; }
}
}

11
ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
@ -9,13 +10,16 @@ namespace ErsatzTV.Core.Scheduling @@ -9,13 +10,16 @@ namespace ErsatzTV.Core.Scheduling
{
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
{
private readonly Collection _collection;
private readonly IList<MediaItem> _sortedMediaItems;
public CustomOrderCollectionEnumerator(
Collection collection,
List<MediaItem> mediaItems,
IList<MediaItem> mediaItems,
CollectionEnumeratorState state)
{
_collection = collection;
// TODO: this will break if we allow shows and seasons
_sortedMediaItems = collection.CollectionItems
.OrderBy(ci => ci.CustomIndex)
@ -34,5 +38,8 @@ namespace ErsatzTV.Core.Scheduling @@ -34,5 +38,8 @@ namespace ErsatzTV.Core.Scheduling
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<MediaItem> Peek(int offset) =>
throw new NotSupportedException();
}
}

677
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -51,58 +51,17 @@ namespace ErsatzTV.Core.Scheduling @@ -51,58 +51,17 @@ namespace ErsatzTV.Core.Scheduling
DateTimeOffset playoutFinish,
bool rebuild = false)
{
var collectionKeys = playout.ProgramSchedule.Items
.SelectMany(CollectionKeysForItem)
.Distinct()
.ToList();
if (!collectionKeys.Any())
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(playout);
if (!collectionMediaItems.Any())
{
_logger.LogWarning(
"Playout {Playout} schedule {Schedule} has no items",
playout.Channel.Name,
playout.ProgramSchedule.Name);
return playout;
}
IEnumerable<Tuple<CollectionKey, List<MediaItem>>> tuples = await collectionKeys.Map(
async collectionKey =>
{
switch (collectionKey.CollectionType)
{
case ProgramScheduleItemCollectionType.Collection:
List<MediaItem> collectionItems =
await _mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0);
return Tuple(collectionKey, collectionItems);
case ProgramScheduleItemCollectionType.TelevisionShow:
List<Episode> showItems =
await _televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, showItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.TelevisionSeason:
List<Episode> seasonItems =
await _televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, seasonItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.Artist:
List<MusicVideo> artistItems =
await _artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, artistItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.MultiCollection:
List<MediaItem> multiCollectionItems =
await _mediaCollectionRepository.GetMultiCollectionItems(
collectionKey.MultiCollectionId ?? 0);
return Tuple(collectionKey, multiCollectionItems);
case ProgramScheduleItemCollectionType.SmartCollection:
List<MediaItem> smartCollectionItems =
await _mediaCollectionRepository.GetSmartCollectionItems(
collectionKey.SmartCollectionId ?? 0);
return Tuple(collectionKey, smartCollectionItems);
default:
return Tuple(collectionKey, new List<MediaItem>());
}
}).Sequence();
var collectionMediaItems = Map.createRange(tuples);
_logger.LogDebug(
"{Action} playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
rebuild ? "Rebuilding" : "Building",
@ -110,71 +69,12 @@ namespace ErsatzTV.Core.Scheduling @@ -110,71 +69,12 @@ namespace ErsatzTV.Core.Scheduling
playout.Channel.Number,
playout.Channel.Name);
foreach ((CollectionKey _, List<MediaItem> items) in collectionMediaItems)
{
var zeroItems = new List<MediaItem>();
foreach (MediaItem item in items)
{
bool isZero = item switch
{
Movie m => await m.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
Episode e => await e.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
MusicVideo mv => await mv.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
OtherVideo ov => await ov.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
_ => true
};
if (isZero)
{
_logger.LogWarning(
"Skipping media item with zero duration {MediaItem} - {MediaItemTitle}",
item.Id,
DisplayTitle(item));
zeroItems.Add(item);
}
}
items.RemoveAll(i => zeroItems.Contains(i));
}
// this guard needs to be below the place where we modify the collections (by removing zero-duration items)
Option<CollectionKey> emptyCollection =
collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
if (emptyCollection.IsSome)
Option<CollectionKey> maybeEmptyCollection = await CheckForEmptyCollections(collectionMediaItems);
foreach (CollectionKey emptyCollection in maybeEmptyCollection)
{
_logger.LogError(
"Unable to rebuild playout; collection {@CollectionKey} has no valid items!",
emptyCollection.ValueUnsafe());
return playout;
}
// leaving this guard in for a while to ensure the zero item removal is working properly
Option<CollectionKey> zeroDurationCollection = collectionMediaItems.Find(
c => c.Value.Any(
mi => mi switch
{
Movie m => m.MediaVersions.HeadOrNone().Map(mv => mv.Duration)
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
Episode e => e.MediaVersions.HeadOrNone().Map(mv => mv.Duration)
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration)
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
OtherVideo ov => ov.MediaVersions.HeadOrNone().Map(v => v.Duration)
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
_ => true
})).Map(c => c.Key);
if (zeroDurationCollection.IsSome)
{
_logger.LogError(
"BUG: Unable to rebuild playout; collection {@CollectionKey} contains items with zero duration!",
zeroDurationCollection.ValueUnsafe());
emptyCollection);
return playout;
}
@ -189,14 +89,13 @@ namespace ErsatzTV.Core.Scheduling @@ -189,14 +89,13 @@ namespace ErsatzTV.Core.Scheduling
playout.ProgramScheduleAnchors.Clear();
}
var sortedScheduleItems =
playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
{
// use configured playback order for primary collection, shuffle for filler
Option<ProgramScheduleItem> maybeScheduleItem = sortedScheduleItems
.FirstOrDefault(item => CollectionKeyForItem(item) == collectionKey);
.FirstOrDefault(item => CollectionKey.ForScheduleItem(item) == collectionKey);
PlaybackOrder playbackOrder = maybeScheduleItem
.Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle);
IMediaCollectionEnumerator enumerator =
@ -226,251 +125,73 @@ namespace ErsatzTV.Core.Scheduling @@ -226,251 +125,73 @@ namespace ErsatzTV.Core.Scheduling
}
// start with the previously-decided schedule item
int index = sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem);
// start with the previous multiple/duration states
Option<int> multipleRemaining = Optional(startAnchor.MultipleRemaining);
Option<DateTimeOffset> durationFinish = startAnchor.DurationFinishOffset;
bool inFlood = startAnchor.InFlood;
bool inDurationFiller = startAnchor.InDurationFiller;
var playoutBuilderState = new PlayoutBuilderState(
sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem),
Optional(startAnchor.MultipleRemaining),
startAnchor.DurationFinishOffset,
startAnchor.InFlood,
startAnchor.InDurationFiller,
startAnchor.NextGuideGroup,
currentTime);
bool customGroup = multipleRemaining.IsSome || durationFinish.IsSome;
var schedulerOne = new PlayoutModeSchedulerOne(_logger);
var schedulerMultiple = new PlayoutModeSchedulerMultiple(collectionMediaItems, _logger);
var schedulerDuration = new PlayoutModeSchedulerDuration(_logger);
var schedulerFlood = new PlayoutModeSchedulerFlood(sortedScheduleItems, _logger);
// loop until we're done filling the desired amount of time
while (currentTime < playoutFinish)
while (playoutBuilderState.CurrentTime < playoutFinish)
{
// get the schedule item out of the sorted list
ProgramScheduleItem scheduleItem = sortedScheduleItems[index % sortedScheduleItems.Count];
// find when we should start this item, based on the current time
DateTimeOffset itemStartTime = GetStartTimeAfter(
scheduleItem,
currentTime,
multipleRemaining.IsSome,
durationFinish.IsSome,
inFlood,
inDurationFiller);
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
if (inDurationFiller && scheduleItem is ProgramScheduleItemDuration
{
TailMode: TailMode.Filler
})
ProgramScheduleItem scheduleItem =
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
ProgramScheduleItem nextScheduleItem =
sortedScheduleItems[(playoutBuilderState.ScheduleItemIndex + 1) % sortedScheduleItems.Count];
Tuple<PlayoutBuilderState, List<PlayoutItem>> result = scheduleItem switch
{
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
}
ProgramScheduleItemMultiple multiple => schedulerMultiple.Schedule(
playoutBuilderState,
collectionEnumerators,
multiple,
nextScheduleItem,
playoutFinish),
ProgramScheduleItemDuration duration => schedulerDuration.Schedule(
playoutBuilderState,
collectionEnumerators,
duration,
nextScheduleItem,
playoutFinish),
ProgramScheduleItemFlood flood => schedulerFlood.Schedule(
playoutBuilderState,
collectionEnumerators,
flood,
nextScheduleItem,
playoutFinish),
ProgramScheduleItemOne one => schedulerOne.Schedule(
playoutBuilderState,
collectionEnumerators,
one,
nextScheduleItem,
playoutFinish),
_ => throw new ArgumentOutOfRangeException(nameof(scheduleItem))
};
IMediaCollectionEnumerator enumerator = collectionEnumerators[CollectionKeyForItem(scheduleItem)];
foreach (CollectionKey tailCollectionKey in maybeTailCollectionKey)
(PlayoutBuilderState nextState, List<PlayoutItem> playoutItems) = result;
foreach (PlayoutItem playoutItem in playoutItems)
{
enumerator = collectionEnumerators[tailCollectionKey];
playout.Items.Add(playoutItem);
}
await enumerator.Current.IfSomeAsync(
mediaItem =>
{
_logger.LogDebug(
"Scheduling media item: {ScheduleItemNumber} / {CollectionType} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
scheduleItem.Index,
inDurationFiller
? (scheduleItem as ProgramScheduleItemDuration)?.TailCollectionType
: scheduleItem.CollectionType,
mediaItem.Id,
DisplayTitle(mediaItem),
itemStartTime);
MediaVersion version = mediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
OtherVideo mv => mv.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + version.Duration,
CustomGroup = customGroup,
IsFiller = inDurationFiller || scheduleItem.GuideMode == GuideMode.Filler
};
if (!string.IsNullOrWhiteSpace(scheduleItem.CustomTitle))
{
playoutItem.CustomTitle = scheduleItem.CustomTitle;
}
enumerator.MoveNext();
if (scheduleItem is ProgramScheduleItemDuration d &&
version.Duration > d.PlayoutDuration)
{
_logger.LogWarning(
"Skipping playout item {Title} with duration {Duration} that is longer than schedule item duration {PlayoutDuration}",
DisplayTitle(mediaItem),
version.Duration,
d.PlayoutDuration);
return;
}
currentTime = itemStartTime + version.Duration;
playout.Items.Add(playoutItem);
switch (scheduleItem)
{
case ProgramScheduleItemOne:
// only play one item from collection, so always advance to the next item
_logger.LogDebug(
"Advancing to next schedule item after playout mode {PlayoutMode}",
"One");
index++;
customGroup = false;
break;
case ProgramScheduleItemMultiple multiple:
if (multipleRemaining.IsNone)
{
if (multiple.Count == 0)
{
multipleRemaining = collectionMediaItems[CollectionKeyForItem(scheduleItem)]
.Count;
}
else
{
multipleRemaining = multiple.Count;
}
customGroup = true;
}
multipleRemaining = multipleRemaining.Map(i => i - 1);
if (multipleRemaining.IfNone(-1) == 0)
{
_logger.LogDebug(
"Advancing to next schedule item after playout mode {PlayoutMode}",
"Multiple");
index++;
multipleRemaining = None;
customGroup = false;
}
break;
case ProgramScheduleItemFlood:
enumerator.Current.Do(
peekMediaItem =>
{
customGroup = true;
MediaVersion peekVersion = peekMediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
OtherVideo ov => ov.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
};
ProgramScheduleItem peekScheduleItem =
sortedScheduleItems[(index + 1) % sortedScheduleItems.Count];
DateTimeOffset peekScheduleItemStart =
peekScheduleItem.StartType == StartType.Fixed
? GetStartTimeAfter(peekScheduleItem, currentTime)
: DateTimeOffset.MaxValue;
// if the current time is before the next schedule item, but the current finish
// is after, we need to move on to the next schedule item
// eventually, spots probably have to fit in this gap
bool willNotFinishInTime = currentTime <= peekScheduleItemStart &&
currentTime + peekVersion.Duration >
peekScheduleItemStart;
if (willNotFinishInTime)
{
_logger.LogDebug(
"Advancing to next schedule item after playout mode {PlayoutMode}",
"Flood");
index++;
customGroup = false;
inFlood = false;
}
else
{
inFlood = true;
}
});
break;
case ProgramScheduleItemDuration duration:
enumerator.Current.Do(
peekMediaItem =>
{
MediaVersion peekVersion = peekMediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
OtherVideo ov => ov.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
};
// remember when we need to finish this duration item
if (durationFinish.IsNone)
{
durationFinish = itemStartTime + duration.PlayoutDuration;
customGroup = true;
}
bool willNotFinishInTime =
currentTime <= durationFinish.IfNone(SystemTime.MinValueUtc) &&
currentTime + peekVersion.Duration >
durationFinish.IfNone(SystemTime.MinValueUtc);
if (willNotFinishInTime)
{
_logger.LogDebug(
"Advancing to next schedule item after playout mode {PlayoutMode}",
"Duration");
index++;
if (duration.TailMode == TailMode.Offline)
{
durationFinish.Do(f => currentTime = f);
}
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))
{
// if we're starting filler, we don't actually need to move
// to the next schedule item yet
index--;
inDurationFiller = true;
durationFinish.Do(
f => playoutItem.GuideFinish = f.UtcDateTime);
}
}
}
);
break;
}
});
playoutBuilderState = nextState;
}
// once more to get playout anchor
ProgramScheduleItem nextScheduleItem = sortedScheduleItems[index % sortedScheduleItems.Count];
ProgramScheduleItem anchorScheduleItem =
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
// build program schedule anchors
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
@ -479,79 +200,120 @@ namespace ErsatzTV.Core.Scheduling @@ -479,79 +200,120 @@ namespace ErsatzTV.Core.Scheduling
playout.Items.RemoveAll(
old => old.FinishOffset < playoutStart.AddHours(-4) || old.StartOffset > playoutFinish);
DateTimeOffset minCurrentTime = currentTime;
if (playout.Items.Any())
{
DateTimeOffset maxStartTime = playout.Items.Max(i => i.FinishOffset);
if (maxStartTime < currentTime)
if (maxStartTime < playoutBuilderState.CurrentTime)
{
minCurrentTime = maxStartTime;
playoutBuilderState = playoutBuilderState with { CurrentTime = maxStartTime };
}
}
playout.Anchor = new PlayoutAnchor
{
NextScheduleItem = nextScheduleItem,
NextScheduleItemId = nextScheduleItem.Id,
NextStart = GetStartTimeAfter(nextScheduleItem, minCurrentTime).UtcDateTime,
MultipleRemaining = multipleRemaining.IsSome ? multipleRemaining.ValueUnsafe() : null,
DurationFinish = durationFinish.IsSome ? durationFinish.ValueUnsafe().UtcDateTime : null,
InFlood = inFlood,
InDurationFiller = inDurationFiller
NextScheduleItem = anchorScheduleItem,
NextScheduleItemId = anchorScheduleItem.Id,
NextStart = PlayoutModeSchedulerBase<ProgramScheduleItem>.GetStartTimeAfter(playoutBuilderState, anchorScheduleItem)
.UtcDateTime,
MultipleRemaining = playoutBuilderState.MultipleRemaining.IsSome
? playoutBuilderState.MultipleRemaining.ValueUnsafe()
: null,
DurationFinish = playoutBuilderState.DurationFinish.IsSome
? playoutBuilderState.DurationFinish.ValueUnsafe().UtcDateTime
: null,
InFlood = playoutBuilderState.InFlood,
InDurationFiller = playoutBuilderState.InDurationFiller,
NextGuideGroup = playoutBuilderState.NextGuideGroup
};
return playout;
}
private static bool WillFinishFillerInTime(
ProgramScheduleItem scheduleItem,
DateTimeOffset currentTime,
Option<DateTimeOffset> durationFinish,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(Playout playout)
{
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
if (scheduleItem is ProgramScheduleItemDuration
{
TailMode: TailMode.Filler
})
{
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
}
var collectionKeys = playout.ProgramSchedule.Items
.SelectMany(CollectionKeysForItem)
.Distinct()
.ToList();
foreach (CollectionKey collectionKey in maybeTailCollectionKey)
{
IMediaCollectionEnumerator enumerator = collectionEnumerators[collectionKey];
Option<int> firstId = enumerator.Current.Map(i => i.Id);
while (true)
IEnumerable<Tuple<CollectionKey, List<MediaItem>>> tuples = await collectionKeys.Map(
async collectionKey =>
{
foreach (MediaItem peekMediaItem in enumerator.Current)
switch (collectionKey.CollectionType)
{
MediaVersion peekVersion = peekMediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
OtherVideo ov => ov.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
};
if (currentTime + peekVersion.Duration <= durationFinish.IfNone(SystemTime.MinValueUtc))
{
return true;
}
case ProgramScheduleItemCollectionType.Collection:
List<MediaItem> collectionItems =
await _mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0);
return Tuple(collectionKey, collectionItems);
case ProgramScheduleItemCollectionType.TelevisionShow:
List<Episode> showItems =
await _televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, showItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.TelevisionSeason:
List<Episode> seasonItems =
await _televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, seasonItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.Artist:
List<MusicVideo> artistItems =
await _artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0);
return Tuple(collectionKey, artistItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.MultiCollection:
List<MediaItem> multiCollectionItems =
await _mediaCollectionRepository.GetMultiCollectionItems(
collectionKey.MultiCollectionId ?? 0);
return Tuple(collectionKey, multiCollectionItems);
case ProgramScheduleItemCollectionType.SmartCollection:
List<MediaItem> smartCollectionItems =
await _mediaCollectionRepository.GetSmartCollectionItems(
collectionKey.SmartCollectionId ?? 0);
return Tuple(collectionKey, smartCollectionItems);
default:
return Tuple(collectionKey, new List<MediaItem>());
}
}).Sequence();
return Map.createRange(tuples);
}
private async Task<Option<CollectionKey>> CheckForEmptyCollections(
Map<CollectionKey, List<MediaItem>> collectionMediaItems)
{
foreach ((CollectionKey _, List<MediaItem> items) in collectionMediaItems)
{
var zeroItems = new List<MediaItem>();
enumerator.MoveNext();
if (enumerator.Current.Map(i => i.Id) == firstId)
foreach (MediaItem item in items)
{
bool isZero = item switch
{
return false;
Movie m => await m.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
Episode e => await e.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
MusicVideo mv => await mv.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
OtherVideo ov => await ov.MediaVersions.Map(v => v.Duration).HeadOrNone()
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
_ => true
};
if (isZero)
{
_logger.LogWarning(
"Skipping media item with zero duration {MediaItem} - {MediaItemTitle}",
item.Id,
DisplayTitle(item));
zeroItems.Add(item);
}
}
items.RemoveAll(i => zeroItems.Contains(i));
}
return false;
return collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
}
private static PlayoutAnchor FindStartAnchor(
Playout playout,
DateTimeOffset start,
@ -581,35 +343,6 @@ namespace ErsatzTV.Core.Scheduling @@ -581,35 +343,6 @@ namespace ErsatzTV.Core.Scheduling
}
});
private static DateTimeOffset GetStartTimeAfter(
ProgramScheduleItem item,
DateTimeOffset start,
bool inMultiple = false,
bool inDuration = 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 ProgramScheduleItemDuration && inDurationFiller)
{
return start;
}
TimeSpan startTime = item.StartTime.GetValueOrDefault();
DateTimeOffset result = start.Date + startTime;
// need to wrap to the next day if appropriate
return start.TimeOfDay > startTime ? result.AddDays(1) : result;
case StartType.Dynamic:
default:
return start;
}
}
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
Playout playout,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
@ -746,7 +479,7 @@ namespace ErsatzTV.Core.Scheduling @@ -746,7 +479,7 @@ namespace ErsatzTV.Core.Scheduling
return result;
}
private static string DisplayTitle(MediaItem mediaItem)
internal static string DisplayTitle(MediaItem mediaItem)
{
switch (mediaItem)
{
@ -775,97 +508,37 @@ namespace ErsatzTV.Core.Scheduling @@ -775,97 +508,37 @@ 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
var result = new List<CollectionKey>
{
ProgramScheduleItemCollectionType.Collection => new CollectionKey
{
CollectionType = item.CollectionType,
CollectionId = item.CollectionId
},
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId
},
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId
},
ProgramScheduleItemCollectionType.Artist => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId
},
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
{
CollectionType = item.CollectionType,
MultiCollectionId = item.MultiCollectionId
},
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
{
CollectionType = item.CollectionType,
SmartCollectionId = item.SmartCollectionId
},
_ => throw new ArgumentOutOfRangeException(nameof(item))
CollectionKey.ForScheduleItem(item)
};
private static Option<CollectionKey> TailCollectionKeyForItem(ProgramScheduleItem item)
{
if (item is ProgramScheduleItemDuration { TailMode: TailMode.Filler } duration)
if (item.PreRollFiller != null)
{
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))
};
result.Add(CollectionKey.ForFillerPreset(item.PreRollFiller));
}
return None;
}
if (item.MidRollFiller != null)
{
result.Add(CollectionKey.ForFillerPreset(item.MidRollFiller));
}
private class CollectionKey : Record<CollectionKey>
{
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public int? MultiCollectionId { get; set; }
public int? SmartCollectionId { get; set; }
public int? MediaItemId { get; set; }
if (item.PostRollFiller != null)
{
result.Add(CollectionKey.ForFillerPreset(item.PostRollFiller));
}
if (item.TailFiller != null)
{
result.Add(CollectionKey.ForFillerPreset(item.TailFiller));
}
if (item.FallbackFiller != null)
{
result.Add(CollectionKey.ForFillerPreset(item.FallbackFiller));
}
return result;
}
}
}

18
ErsatzTV.Core/Scheduling/PlayoutBuilderState.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using System;
using LanguageExt;
namespace ErsatzTV.Core.Scheduling
{
public record PlayoutBuilderState(
int ScheduleItemIndex,
Option<int> MultipleRemaining,
Option<DateTimeOffset> DurationFinish,
bool InFlood,
bool InDurationFiller,
int NextGuideGroup,
DateTimeOffset CurrentTime)
{
public int IncrementGuideGroup => (NextGuideGroup + 1) % 10000;
public int DecrementGuideGroup => (NextGuideGroup - 1) % 10000;
}
}

6
ErsatzTV.Core/Scheduling/PlayoutModeBlock.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System;
namespace ErsatzTV.Core.Scheduling
{
public record PlayoutModeBlock(DateTimeOffset StartTime, DateTimeOffset FinishTime);
}

700
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -0,0 +1,700 @@ @@ -0,0 +1,700 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt.UnsafeValueAccess;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using LanguageExt;
namespace ErsatzTV.Core.Scheduling
{
public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> where T : ProgramScheduleItem
{
protected readonly ILogger _logger;
protected PlayoutModeSchedulerBase(ILogger logger)
{
_logger = logger;
}
public static DateTimeOffset GetStartTimeAfter(
PlayoutBuilderState state,
ProgramScheduleItem scheduleItem)
{
DateTimeOffset startTime = state.CurrentTime;
bool isIncomplete = scheduleItem is ProgramScheduleItemMultiple && state.MultipleRemaining.IsSome ||
scheduleItem is ProgramScheduleItemDuration && state.DurationFinish.IsSome ||
scheduleItem is ProgramScheduleItemFlood && state.InFlood ||
scheduleItem is ProgramScheduleItemDuration && state.InDurationFiller;
if (scheduleItem.StartType == StartType.Fixed && !isIncomplete)
{
TimeSpan itemStartTime = scheduleItem.StartTime.GetValueOrDefault();
DateTime date = startTime.Date;
DateTimeOffset result = new DateTimeOffset(
date.Year,
date.Month,
date.Day,
0,
0,
0,
TimeZoneInfo.Local.GetUtcOffset(
new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, DateTimeKind.Local)))
.Add(itemStartTime);
// DateTimeOffset result = startTime.Date + itemStartTime;
// need to wrap to the next day if appropriate
startTime = startTime.TimeOfDay > itemStartTime ? result.AddDays(1) : result;
}
return startTime;
}
public abstract Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
T scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop);
protected Tuple<PlayoutBuilderState, List<PlayoutItem>> AddTailFiller(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItem scheduleItem,
List<PlayoutItem> playoutItems,
DateTimeOffset nextItemStart)
{
var newItems = new List<PlayoutItem>(playoutItems);
PlayoutBuilderState nextState = playoutBuilderState;
if (scheduleItem.TailFiller != null)
{
IMediaCollectionEnumerator enumerator =
collectionEnumerators[CollectionKey.ForFillerPreset(scheduleItem.TailFiller)];
while (enumerator.Current.IsSome && nextState.CurrentTime < nextItemStart)
{
MediaItem mediaItem = enumerator.Current.ValueUnsafe();
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
if (nextState.CurrentTime + itemDuration > nextItemStart)
{
_logger.LogDebug(
"Filler with duration {Duration} will go past next item start {NextItemStart}",
itemDuration,
nextItemStart);
break;
}
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = nextState.CurrentTime.UtcDateTime,
Finish = nextState.CurrentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
FillerKind = FillerKind.Tail,
GuideGroup = nextState.NextGuideGroup
};
newItems.Add(playoutItem);
nextState = nextState with
{
CurrentTime = nextState.CurrentTime + itemDuration
};
enumerator.MoveNext();
}
}
return Tuple(nextState, newItems);
}
protected Tuple<PlayoutBuilderState, List<PlayoutItem>> AddFallbackFiller(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItem scheduleItem,
List<PlayoutItem> playoutItems,
DateTimeOffset nextItemStart)
{
var newItems = new List<PlayoutItem>(playoutItems);
PlayoutBuilderState nextState = playoutBuilderState;
if (scheduleItem.FallbackFiller != null && playoutBuilderState.CurrentTime < nextItemStart)
{
IMediaCollectionEnumerator enumerator =
collectionEnumerators[CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller)];
foreach (MediaItem mediaItem in enumerator.Current)
{
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = nextState.CurrentTime.UtcDateTime,
Finish = nextItemStart.UtcDateTime,
InPoint = TimeSpan.Zero,
OutPoint = TimeSpan.Zero,
GuideGroup = nextState.NextGuideGroup,
FillerKind = FillerKind.Fallback
};
newItems.Add(playoutItem);
nextState = nextState with
{
CurrentTime = nextItemStart.UtcDateTime
};
enumerator.MoveNext();
}
}
return Tuple(nextState, newItems);
}
protected static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{
MediaVersion version = mediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
OtherVideo mv => mv.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
return version.Duration;
}
protected static List<MediaChapter> ChaptersForMediaItem(MediaItem mediaItem)
{
MediaVersion version = mediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
MusicVideo mv => mv.MediaVersions.Head(),
OtherVideo mv => mv.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
return version.Chapters;
}
protected void LogScheduledItem(
ProgramScheduleItem scheduleItem,
MediaItem mediaItem,
DateTimeOffset startTime) =>
_logger.LogDebug(
"Scheduling media item: {ScheduleItemNumber} / {CollectionType} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
scheduleItem.Index,
scheduleItem.CollectionType,
mediaItem.Id,
PlayoutBuilder.DisplayTitle(mediaItem),
startTime);
internal static DateTimeOffset CalculateEndTimeWithFiller(
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
ProgramScheduleItem scheduleItem,
DateTimeOffset itemStartTime,
TimeSpan itemDuration,
List<MediaChapter> chapters)
{
var allFiller = Optional(scheduleItem.PreRollFiller)
.Append(Optional(scheduleItem.MidRollFiller))
.Append(Optional(scheduleItem.PostRollFiller))
.ToList();
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1)
{
// multiple pad-to-nearest-minute values are invalid; use no filler
// TODO: log error?
return itemStartTime + itemDuration;
}
TimeSpan totalDuration = itemDuration;
foreach (FillerPreset filler in allFiller)
{
switch (filler.FillerKind, filler.FillerMode)
{
case (FillerKind.MidRoll, FillerMode.Duration) when filler.Duration.HasValue:
IMediaCollectionEnumerator mrde = enumerators[CollectionKey.ForFillerPreset(filler)];
var mrdePeekOffset = 0;
for (var i = 0; i < chapters.Count - 1; i++)
{
TimeSpan midRollDuration = filler.Duration.Value;
while (mrde.Peek(mrdePeekOffset))
{
foreach (MediaItem mediaItem in mrde.Peek(mrdePeekOffset))
{
TimeSpan currentDuration = DurationForMediaItem(mediaItem);
midRollDuration -= currentDuration;
if (midRollDuration >= TimeSpan.Zero)
{
totalDuration += currentDuration;
mrdePeekOffset++;
}
}
if (midRollDuration < TimeSpan.Zero)
{
break;
}
}
}
break;
case (FillerKind.MidRoll, FillerMode.Count) when filler.Count.HasValue:
IMediaCollectionEnumerator mrce = enumerators[CollectionKey.ForFillerPreset(filler)];
var mrcePeekOffset = 0;
for (var i = 0; i < chapters.Count - 1; i++)
{
for (var j = 0; j < filler.Count.Value; j++)
{
foreach (MediaItem mediaItem in mrce.Peek(mrcePeekOffset))
{
totalDuration += DurationForMediaItem(mediaItem);
mrcePeekOffset++;
}
}
}
break;
case (_, FillerMode.Duration) when filler.Duration.HasValue:
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
var peekOffset1 = 0;
TimeSpan duration = filler.Duration.Value;
while (e1.Peek(peekOffset1).IsSome)
{
foreach (MediaItem mediaItem in e1.Peek(peekOffset1))
{
TimeSpan currentDuration = DurationForMediaItem(mediaItem);
duration -= currentDuration;
if (duration >= TimeSpan.Zero)
{
totalDuration += currentDuration;
peekOffset1++;
}
}
if (duration < TimeSpan.Zero)
{
break;
}
}
break;
case (_, FillerMode.Count) when filler.Count.HasValue:
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
var peekOffset2 = 0;
for (var i = 0; i < filler.Count.Value; i++)
{
foreach (MediaItem mediaItem in e2.Peek(peekOffset2))
{
totalDuration += DurationForMediaItem(mediaItem);
peekOffset2++;
}
}
break;
}
}
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue)))
{
int currentMinute = (itemStartTime + totalDuration).Minute;
// ReSharper disable once PossibleInvalidOperationException
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value;
DateTimeOffset targetTime = itemStartTime + totalDuration - TimeSpan.FromMinutes(currentMinute) +
TimeSpan.FromMinutes(targetMinute);
return new DateTimeOffset(
targetTime.Year,
targetTime.Month,
targetTime.Day,
targetTime.Hour,
targetTime.Minute,
0,
targetTime.Offset);
}
return itemStartTime + totalDuration;
}
protected List<PlayoutItem> AddFiller(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
ProgramScheduleItem scheduleItem,
PlayoutItem playoutItem,
List<MediaChapter> chapters)
{
var result = new List<PlayoutItem>();
var allFiller = Optional(scheduleItem.PreRollFiller)
.Append(Optional(scheduleItem.MidRollFiller))
.Append(Optional(scheduleItem.PostRollFiller))
.ToList();
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1)
{
// multiple pad-to-nearest-minute values are invalid; use no filler
// TODO: log error?
return new List<PlayoutItem> { playoutItem };
}
foreach (FillerPreset filler in allFiller.Filter(
f => f.FillerKind == FillerKind.PreRoll && f.FillerMode != FillerMode.Pad))
{
switch (filler.FillerMode)
{
case FillerMode.Duration when filler.Duration.HasValue:
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
result.AddRange(
AddDurationFiller(playoutBuilderState, e1, filler.Duration.Value, FillerKind.PreRoll));
break;
case FillerMode.Count when filler.Count.HasValue:
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
result.AddRange(
AddCountFiller(playoutBuilderState, e2, filler.Count.Value, FillerKind.PreRoll));
break;
}
}
if (allFiller.All(f => f.FillerKind != FillerKind.MidRoll) || !chapters.Any())
{
result.Add(playoutItem);
}
else
{
foreach (FillerPreset filler in allFiller.Filter(
f => f.FillerKind == FillerKind.MidRoll && f.FillerMode != FillerMode.Pad))
{
switch (filler.FillerMode)
{
case FillerMode.Duration when filler.Duration.HasValue:
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
for (var i = 0; i < chapters.Count; i++)
{
result.Add(playoutItem.ForChapter(chapters[i]));
if (i < chapters.Count - 1)
{
result.AddRange(
AddDurationFiller(
playoutBuilderState,
e1,
filler.Duration.Value,
FillerKind.MidRoll));
}
}
break;
case FillerMode.Count when filler.Count.HasValue:
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
for (var i = 0; i < chapters.Count - 1; i++)
{
result.Add(playoutItem.ForChapter(chapters[i]));
result.AddRange(
AddCountFiller(
playoutBuilderState,
e2,
filler.Count.Value,
FillerKind.MidRoll));
}
break;
}
}
}
foreach (FillerPreset filler in allFiller.Filter(
f => f.FillerKind == FillerKind.PostRoll && f.FillerMode != FillerMode.Pad))
{
switch (filler.FillerMode)
{
case FillerMode.Duration when filler.Duration.HasValue:
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
result.AddRange(
AddDurationFiller(playoutBuilderState, e1, filler.Duration.Value, FillerKind.PostRoll));
break;
case FillerMode.Count when filler.Count.HasValue:
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
result.AddRange(
AddCountFiller(playoutBuilderState, e2, filler.Count.Value, FillerKind.PostRoll));
break;
}
}
// after all non-padded filler has been added, figure out padding
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue)))
{
var totalDuration =
TimeSpan.FromMilliseconds(
result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds) +
chapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds));
int currentMinute = (playoutItem.StartOffset + totalDuration).Minute;
// ReSharper disable once PossibleInvalidOperationException
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value;
DateTimeOffset almostTargetTime = playoutItem.StartOffset + totalDuration -
TimeSpan.FromMinutes(currentMinute) +
TimeSpan.FromMinutes(targetMinute);
var targetTime = new DateTimeOffset(
almostTargetTime.Year,
almostTargetTime.Month,
almostTargetTime.Day,
almostTargetTime.Hour,
almostTargetTime.Minute,
0,
almostTargetTime.Offset);
TimeSpan remainingToFill = targetTime - totalDuration - playoutItem.StartOffset;
// _logger.LogInformation(
// "Total duration {TotalDuration}; need to fill {TimeSpan} to pad properly to {TargetTime}",
// totalDuration,
// remainingToFill,
// targetTime);
switch (padFiller.FillerKind)
{
case FillerKind.PreRoll:
IMediaCollectionEnumerator pre1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
result.InsertRange(
0,
AddDurationFiller(
playoutBuilderState,
pre1,
remainingToFill,
FillerKind.PreRoll));
totalDuration =
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
remainingToFill = targetTime - totalDuration - playoutItem.StartOffset;
if (remainingToFill > TimeSpan.Zero)
{
result.InsertRange(
0,
FallbackFillerForPad(
playoutBuilderState,
enumerators,
scheduleItem,
remainingToFill));
}
break;
case FillerKind.MidRoll:
IMediaCollectionEnumerator mid1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
var fillerQueue = new Queue<PlayoutItem>(
AddDurationFiller(
playoutBuilderState,
mid1,
remainingToFill,
FillerKind.MidRoll));
TimeSpan average = chapters.Count == 0
? remainingToFill
: remainingToFill / (chapters.Count - 1);
TimeSpan filled = TimeSpan.Zero;
for (var i = 0; i < chapters.Count; i++)
{
result.Add(playoutItem.ForChapter(chapters[i]));
if (i < chapters.Count - 1)
{
TimeSpan current = TimeSpan.Zero;
while (current < average && filled < remainingToFill)
{
if (fillerQueue.TryDequeue(out PlayoutItem fillerItem))
{
result.Add(fillerItem);
current += fillerItem.Finish - fillerItem.Start;
filled += fillerItem.Finish - fillerItem.Start;
}
else
{
TimeSpan leftInThisBreak = average - current;
TimeSpan leftOverall = remainingToFill - filled;
TimeSpan maxThisBreak = leftOverall < leftInThisBreak
? leftOverall
: leftInThisBreak;
Option<PlayoutItem> maybeFallback = FallbackFillerForPad(
playoutBuilderState,
enumerators,
scheduleItem,
i < chapters.Count - 1 ? maxThisBreak : leftOverall);
foreach (PlayoutItem fallback in maybeFallback)
{
current += fallback.Finish - fallback.Start;
filled += fallback.Finish - fallback.Start;
result.Add(fallback);
}
}
}
}
}
break;
case FillerKind.PostRoll:
IMediaCollectionEnumerator post1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
result.AddRange(
AddDurationFiller(
playoutBuilderState,
post1,
remainingToFill,
FillerKind.PostRoll));
totalDuration =
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
remainingToFill = targetTime - totalDuration - playoutItem.StartOffset;
if (remainingToFill > TimeSpan.Zero)
{
result.AddRange(
FallbackFillerForPad(
playoutBuilderState,
enumerators,
scheduleItem,
remainingToFill));
}
break;
}
}
// fix times on each playout item
DateTimeOffset currentTime = playoutItem.StartOffset;
for (var i = 0; i < result.Count; i++)
{
PlayoutItem item = result[i];
TimeSpan duration = item.Finish - item.Start;
item.Start = currentTime.UtcDateTime;
item.Finish = (currentTime + duration).UtcDateTime;
currentTime = item.FinishOffset;
}
return result;
}
private static List<PlayoutItem> AddCountFiller(
PlayoutBuilderState playoutBuilderState,
IMediaCollectionEnumerator enumerator,
int count,
FillerKind fillerKind)
{
var result = new List<PlayoutItem>();
for (var i = 0; i < count; i++)
{
foreach (MediaItem mediaItem in enumerator.Current)
{
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = playoutBuilderState.NextGuideGroup,
FillerKind = fillerKind
};
result.Add(playoutItem);
enumerator.MoveNext();
}
}
return result;
}
private static List<PlayoutItem> AddDurationFiller(
PlayoutBuilderState playoutBuilderState,
IMediaCollectionEnumerator enumerator,
TimeSpan duration,
FillerKind fillerKind)
{
var result = new List<PlayoutItem>();
while (enumerator.Current.IsSome)
{
foreach (MediaItem mediaItem in enumerator.Current)
{
// TODO: retry up to x times when item doesn't fit?
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
duration -= itemDuration;
if (duration >= TimeSpan.Zero)
{
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = playoutBuilderState.NextGuideGroup,
FillerKind = fillerKind
};
result.Add(playoutItem);
enumerator.MoveNext();
}
}
if (duration < TimeSpan.Zero)
{
break;
}
}
return result;
}
private Option<PlayoutItem> FallbackFillerForPad(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
ProgramScheduleItem scheduleItem,
TimeSpan duration)
{
if (scheduleItem.FallbackFiller != null)
{
IMediaCollectionEnumerator enumerator =
enumerators[CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller)];
foreach (MediaItem mediaItem in enumerator.Current)
{
var result = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + duration,
InPoint = TimeSpan.Zero,
OutPoint = TimeSpan.Zero,
GuideGroup = playoutBuilderState.NextGuideGroup,
FillerKind = FillerKind.Fallback
};
enumerator.MoveNext();
return result;
}
}
return None;
}
}
}

182
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -0,0 +1,182 @@ @@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Scheduling
{
public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramScheduleItemDuration>
{
public PlayoutModeSchedulerDuration(ILogger logger) : base(logger)
{
}
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItemDuration scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop)
{
var playoutItems = new List<PlayoutItem>();
PlayoutBuilderState nextState = playoutBuilderState;
var willFinishInTime = true;
Option<DateTimeOffset> durationUntil = None;
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime)
{
MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe();
// find when we should start this item, based on the current time
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
// remember when we need to finish this duration item
if (nextState.DurationFinish.IsNone)
{
nextState = nextState with
{
DurationFinish = itemStartTime + scheduleItem.PlayoutDuration
};
durationUntil = nextState.DurationFinish;
}
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
if (itemDuration > scheduleItem.PlayoutDuration)
{
_logger.LogWarning(
"Skipping playout item {Title} with duration {Duration} that is longer than schedule item duration {PlayoutDuration}",
PlayoutBuilder.DisplayTitle(mediaItem),
itemDuration,
scheduleItem.PlayoutDuration);
contentEnumerator.MoveNext();
continue;
}
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = nextState.NextGuideGroup,
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail
: FillerKind.None
};
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));
nextState = nextState with
{
CurrentTime = itemEndTimeWithFiller
};
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,
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
};
}
}
// this is needed when the duration finish exactly matches the hard stop
if (nextState.DurationFinish.IsSome && nextState.CurrentTime == nextState.DurationFinish)
{
nextState = nextState with
{
DurationFinish = None,
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
};
}
foreach (DateTimeOffset nextItemStart in durationUntil)
{
switch (scheduleItem.TailMode)
{
case TailMode.Filler:
if (scheduleItem.TailFiller != null)
{
(nextState, playoutItems) = AddTailFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
nextItemStart);
}
if (scheduleItem.FallbackFiller != null)
{
(nextState, playoutItems) = AddFallbackFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
nextItemStart);
}
nextState = nextState with { CurrentTime = nextItemStart };
break;
case TailMode.Offline:
if (scheduleItem.FallbackFiller != null)
{
(nextState, playoutItems) = AddFallbackFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
nextItemStart);
}
nextState = nextState with { CurrentTime = nextItemStart };
break;
}
}
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
return Tuple(nextState, playoutItems);
}
}
}

147
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt.UnsafeValueAccess;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Scheduling
{
public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramScheduleItemFlood>
{
private readonly List<ProgramScheduleItem> _sortedScheduleItems;
public PlayoutModeSchedulerFlood(List<ProgramScheduleItem> sortedScheduleItems, ILogger logger)
: base(logger)
{
_sortedScheduleItems = sortedScheduleItems;
}
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItemFlood scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop)
{
var playoutItems = new List<PlayoutItem>();
PlayoutBuilderState nextState = playoutBuilderState;
var willFinishInTime = true;
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime)
{
MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe();
// find when we should start this item, based on the current time
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = nextState.NextGuideGroup,
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail
: FillerKind.None
};
ProgramScheduleItem peekScheduleItem =
_sortedScheduleItems[(nextState.ScheduleItemIndex + 1) % _sortedScheduleItems.Count];
DateTimeOffset peekScheduleItemStart =
peekScheduleItem.StartType == StartType.Fixed
? GetStartTimeAfter(nextState, peekScheduleItem)
: DateTimeOffset.MaxValue;
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
scheduleItem,
itemStartTime,
itemDuration,
itemChapters);
// if the current time is before the next schedule item, but the current finish
// is after, we need to move on to the next schedule item
willFinishInTime = itemStartTime > peekScheduleItemStart ||
itemEndTimeWithFiller <= peekScheduleItemStart;
if (willFinishInTime)
{
playoutItems.AddRange(
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
{
_logger.LogWarning(
"Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}",
itemEndTimeWithFiller,
actualEndTime);
// _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems);
}
nextState = nextState with
{
CurrentTime = itemEndTimeWithFiller,
InFlood = true,
NextGuideGroup = nextState.IncrementGuideGroup
};
contentEnumerator.MoveNext();
}
}
// _logger.LogDebug(
// "Advancing to next schedule item after playout mode {PlayoutMode}",
// "Flood");
nextState = nextState with
{
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
InFlood = nextState.CurrentTime >= hardStop,
NextGuideGroup = nextState.DecrementGuideGroup
};
ProgramScheduleItem peekItem =
_sortedScheduleItems[nextState.ScheduleItemIndex % _sortedScheduleItems.Count];
DateTimeOffset peekItemStart = GetStartTimeAfter(nextState, peekItem);
if (scheduleItem.TailFiller != null)
{
(nextState, playoutItems) = AddTailFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
peekItemStart);
}
if (scheduleItem.FallbackFiller != null)
{
(nextState, playoutItems) = AddFallbackFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
peekItemStart);
}
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
return Tuple(nextState, playoutItems);
}
}
}

131
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Scheduling
{
public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramScheduleItemMultiple>
{
private readonly Map<CollectionKey, List<MediaItem>> _collectionMediaItems;
public PlayoutModeSchedulerMultiple(Map<CollectionKey, List<MediaItem>> collectionMediaItems, ILogger logger)
: base(logger)
{
_collectionMediaItems = collectionMediaItems;
}
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItemMultiple scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop)
{
var playoutItems = new List<PlayoutItem>();
PlayoutBuilderState nextState = playoutBuilderState with
{
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count)
};
if (nextState.MultipleRemaining == 0)
{
nextState = nextState with
{
MultipleRemaining = _collectionMediaItems[CollectionKey.ForScheduleItem(scheduleItem)].Count
};
}
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
while (contentEnumerator.Current.IsSome && nextState.MultipleRemaining > 0 &&
nextState.CurrentTime < hardStop)
{
MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe();
// find when we should start this item, based on the current time
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = nextState.NextGuideGroup,
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail
: FillerKind.None
};
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
scheduleItem,
itemStartTime,
itemDuration,
itemChapters);
playoutItems.AddRange(
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
nextState = nextState with
{
CurrentTime = itemEndTimeWithFiller,
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1)
};
contentEnumerator.MoveNext();
}
if (nextState.MultipleRemaining.IfNone(-1) == 0)
{
// _logger.LogDebug(
// "Advancing to next schedule item after playout mode {PlayoutMode}",
// "Multiple");
nextState = nextState with
{
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
MultipleRemaining = None
};
}
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
if (scheduleItem.TailFiller != null)
{
(nextState, playoutItems) = AddTailFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
nextItemStart);
}
if (scheduleItem.FallbackFiller != null)
{
(nextState, playoutItems) = AddFallbackFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
nextItemStart);
}
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
return Tuple(nextState, playoutItems);
}
}
}

108
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Scheduling
{
public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleItemOne>
{
public PlayoutModeSchedulerOne(ILogger logger) : base(logger)
{
}
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItemOne scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop)
{
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
foreach (MediaItem mediaItem in contentEnumerator.Current)
{
// find when we should start this item, based on the current time
DateTimeOffset itemStartTime = GetStartTimeAfter(
playoutBuilderState,
scheduleItem);
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime,
Finish = itemStartTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = playoutBuilderState.NextGuideGroup,
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
? FillerKind.Tail
: FillerKind.None
};
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
scheduleItem,
itemStartTime,
itemDuration,
itemChapters);
List<PlayoutItem> playoutItems = AddFiller(
playoutBuilderState,
collectionEnumerators,
scheduleItem,
playoutItem,
itemChapters);
// only play one item from collection, so always advance to the next item
// _logger.LogDebug(
// "Advancing to next schedule item after playout mode {PlayoutMode}",
// "One");
PlayoutBuilderState nextState = playoutBuilderState with
{
CurrentTime = itemEndTimeWithFiller,
ScheduleItemIndex = playoutBuilderState.ScheduleItemIndex + 1
};
contentEnumerator.MoveNext();
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
if (scheduleItem.TailFiller != null)
{
(nextState, playoutItems) = AddTailFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
nextItemStart);
}
if (scheduleItem.FallbackFiller != null)
{
(nextState, playoutItems) = AddFallbackFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItems,
nextItemStart);
}
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
return Tuple(nextState, playoutItems);
}
return Tuple(playoutBuilderState, new List<PlayoutItem>());
}
}
}

3
ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs

@ -37,5 +37,8 @@ namespace ErsatzTV.Core.Scheduling @@ -37,5 +37,8 @@ namespace ErsatzTV.Core.Scheduling
_index = _random.Next() % _mediaItems.Count;
State.Index++;
}
public Option<MediaItem> Peek(int offset) =>
throw new NotSupportedException();
}
}

5
ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt;
@ -63,7 +64,9 @@ namespace ErsatzTV.Core.Scheduling @@ -63,7 +64,9 @@ namespace ErsatzTV.Core.Scheduling
State.Index %= _shuffled.Count;
}
public Option<MediaItem> Peek(int offset) => throw new NotSupportedException();
private IList<MediaItem> Shuffle(IList<CollectionWithItems> collections, Random random)
{
// based on https://keyj.emphy.de/balanced-shuffle/

30
ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt;
@ -64,6 +65,35 @@ namespace ErsatzTV.Core.Scheduling @@ -64,6 +65,35 @@ namespace ErsatzTV.Core.Scheduling
State.Index %= _shuffled.Count;
}
public Option<MediaItem> Peek(int offset)
{
if ((State.Index + offset) % _mediaItemCount == 0)
{
IList<MediaItem> shuffled;
Option<MediaItem> tail = Current;
// clone the random
var randomCopy = new Random();
FieldInfo seedArrayInfo = typeof(Random).GetField(
"_seedArray",
BindingFlags.NonPublic | BindingFlags.Instance);
var seedArray = seedArrayInfo.GetValue(_random) as int[];
int[] seedArrayCopy = seedArray.ToArray();
seedArrayInfo.SetValue(randomCopy, seedArrayCopy);
do
{
int newSeed = randomCopy.Next();
randomCopy = new Random(newSeed);
shuffled = Shuffle(_mediaItems, randomCopy);
} while (_mediaItems.Count > 1 && shuffled[0] == tail);
return shuffled.Any() ? shuffled[0] : None;
}
return _shuffled.Any() ? _shuffled[(State.Index + offset) % _mediaItemCount] : None;
}
private IList<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, Random random)
{
GroupedMediaItem[] copy = list.ToArray();

1
ErsatzTV.Core/SystemTime.cs

@ -5,5 +5,6 @@ namespace ErsatzTV.Core @@ -5,5 +5,6 @@ namespace ErsatzTV.Core
public static class SystemTime
{
public static DateTime MinValueUtc = new(0, DateTimeKind.Utc);
public static DateTime MaxValueUtc = new(DateTime.MaxValue.Ticks, DateTimeKind.Utc);
}
}

38
ErsatzTV.Infrastructure/Data/Configurations/Filler/FillerPresetConfiguration.cs

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
using ErsatzTV.Core.Domain.Filler;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Filler
{
public class FillerPresetConfiguration : IEntityTypeConfiguration<FillerPreset>
{
public void Configure(EntityTypeBuilder<FillerPreset> builder)
{
builder.ToTable("FillerPreset");
builder.HasOne(i => i.Collection)
.WithMany()
.HasForeignKey(i => i.CollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.MediaItem)
.WithMany()
.HasForeignKey(i => i.MediaItemId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.MultiCollection)
.WithMany()
.HasForeignKey(i => i.MultiCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.SmartCollection)
.WithMany()
.HasForeignKey(i => i.SmartCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
}
}
}

11
ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaChapterConfiguration.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaChapterConfiguration : IEntityTypeConfiguration<MediaChapter>
{
public void Configure(EntityTypeBuilder<MediaChapter> builder) => builder.ToTable("MediaChapter");
}
}

5
ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaVersionConfiguration.cs

@ -19,6 +19,11 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -19,6 +19,11 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
.WithOne(s => s.MediaVersion)
.HasForeignKey(s => s.MediaVersionId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(v => v.Chapters)
.WithOne(c => c.MediaVersion)
.HasForeignKey(c => c.MediaVersionId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}

36
ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs

@ -27,6 +27,42 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -27,6 +27,42 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
.HasForeignKey(i => i.MultiCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.SmartCollection)
.WithMany()
.HasForeignKey(i => i.SmartCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.PreRollFiller)
.WithMany()
.HasForeignKey(i => i.PreRollFillerId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
builder.HasOne(i => i.MidRollFiller)
.WithMany()
.HasForeignKey(i => i.MidRollFillerId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
builder.HasOne(i => i.PostRollFiller)
.WithMany()
.HasForeignKey(i => i.PostRollFillerId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
builder.HasOne(i => i.TailFiller)
.WithMany()
.HasForeignKey(i => i.TailFillerId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
builder.HasOne(i => i.FallbackFiller)
.WithMany()
.HasForeignKey(i => i.FallbackFillerId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
}
}
}

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

@ -6,27 +6,7 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -6,27 +6,7 @@ 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);
}
}
}

1
ErsatzTV.Infrastructure/Data/Repositories/ArtistRepository.cs

@ -154,6 +154,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -154,6 +154,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.AsNoTracking()
.Include(mv => mv.MusicVideoMetadata)
.Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Include(mv => mv.Artist)
.ThenInclude(a => a.ArtistMetadata)
.Filter(mv => mv.ArtistId == artistId)

7
ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs

@ -362,6 +362,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -362,6 +362,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
dbContext.Movies
.Include(m => m.MovieMetadata)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Filter(m => movieIds.Contains(m.Id))
.ToListAsync();
@ -385,6 +386,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -385,6 +386,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(a => a.ArtistMetadata)
.Include(m => m.MusicVideoMetadata)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Filter(m => musicVideoIds.Contains(m.Id))
.ToListAsync();
@ -416,6 +418,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -416,6 +418,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(a => a.ArtistMetadata)
.Include(m => m.MusicVideoMetadata)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Filter(m => musicVideoIds.Contains(m.Id))
.ToListAsync();
@ -434,6 +437,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -434,6 +437,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
dbContext.OtherVideos
.Include(m => m.OtherVideoMetadata)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Filter(m => otherVideoIds.Contains(m.Id))
.ToListAsync();
@ -454,6 +458,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -454,6 +458,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Include(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
@ -488,6 +493,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -488,6 +493,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Include(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
@ -520,6 +526,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -520,6 +526,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Include(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)

26
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -117,6 +117,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -117,6 +117,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<MediaVersion> maybeVersion = await dbContext.MediaVersions
.Include(v => v.Streams)
.Include(v => v.Chapters)
.OrderBy(v => v.Id)
.SingleOrDefaultAsync(v => v.Id == mediaVersionId)
.Map(Optional);
@ -161,6 +162,31 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -161,6 +162,31 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
existingStream.Default = incomingStream.Default;
existingStream.Forced = incomingStream.Forced;
}
var chaptersToAdd = incoming.Chapters
.Filter(s => existing.Chapters.All(es => es.ChapterId != s.ChapterId))
.ToList();
var chaptersToRemove = existing.Chapters
.Filter(es => incoming.Chapters.All(s => s.ChapterId != es.ChapterId))
.ToList();
var chaptersToUpdate = incoming.Chapters.Except(chaptersToAdd).ToList();
// add
existing.Chapters.AddRange(chaptersToAdd);
// remove
existing.Chapters.RemoveAll(chaptersToRemove.Contains);
// update
foreach (MediaChapter incomingChapter in chaptersToUpdate)
{
MediaChapter existingChapter = existing.Chapters
.First(s => s.ChapterId == incomingChapter.ChapterId);
existingChapter.StartTime = incomingChapter.StartTime;
existingChapter.EndTime = incomingChapter.EndTime;
existingChapter.Title = incomingChapter.Title;
}
return await dbContext.SaveChangesAsync() > 0;
},

4
ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs

@ -78,8 +78,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -78,8 +78,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
foreach (int otherVideoId in ids)
{
OtherVideo othervide = await dbContext.OtherVideos.FindAsync(otherVideoId);
dbContext.OtherVideos.Remove(othervide);
OtherVideo otherVideo = await dbContext.OtherVideos.FindAsync(otherVideoId);
dbContext.OtherVideos.Remove(otherVideo);
}
await dbContext.SaveChangesAsync();

2
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -582,6 +582,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -582,6 +582,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.AsNoTracking()
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Include(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
@ -596,6 +597,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -596,6 +597,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.AsNoTracking()
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.Chapters)
.Include(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)

2
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -72,6 +73,7 @@ namespace ErsatzTV.Infrastructure.Data @@ -72,6 +73,7 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<Resolution> Resolutions { get; set; }
public DbSet<LanguageCode> LanguageCodes { get; set; }
public DbSet<TraktList> TraktLists { get; set; }
public DbSet<FillerPreset> FillerPresets { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder.UseLoggerFactory(_loggerFactory);

3551
ErsatzTV.Infrastructure/Migrations/20211016173400_Add_MediaChapter.Designer.cs generated

File diff suppressed because it is too large Load Diff

58
ErsatzTV.Infrastructure/Migrations/20211016173400_Add_MediaChapter.cs

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MediaChapter : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MediaChapter",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
MediaVersionId = table.Column<int>(type: "INTEGER", nullable: false),
ChapterId = table.Column<long>(type: "INTEGER", nullable: false),
StartTime = table.Column<TimeSpan>(type: "TEXT", nullable: false),
EndTime = table.Column<TimeSpan>(type: "TEXT", nullable: false),
Title = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MediaChapter", x => x.Id);
table.ForeignKey(
name: "FK_MediaChapter_MediaVersion_MediaVersionId",
column: x => x.MediaVersionId,
principalTable: "MediaVersion",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MediaChapter_MediaVersionId",
table: "MediaChapter",
column: "MediaVersionId");
migrationBuilder.Sql("UPDATE MediaVersion SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql("UPDATE LibraryFolder SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL");
migrationBuilder.Sql("UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00'");
migrationBuilder.Sql("UPDATE Library SET LastScan = '0001-01-01 00:00:00'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MediaChapter");
}
}
}

3684
ErsatzTV.Infrastructure/Migrations/20211017110923_Add_FillerPreset.Designer.cs generated

File diff suppressed because it is too large Load Diff

256
ErsatzTV.Infrastructure/Migrations/20211017110923_Add_FillerPreset.cs

@ -0,0 +1,256 @@ @@ -0,0 +1,256 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_FillerPreset : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_SmartCollection_SmartCollectionId",
table: "ProgramScheduleItem");
migrationBuilder.AddColumn<int>(
name: "FallbackFillerId",
table: "ProgramScheduleItem",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "MidRollFillerId",
table: "ProgramScheduleItem",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PostRollFillerId",
table: "ProgramScheduleItem",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PreRollFillerId",
table: "ProgramScheduleItem",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateTable(
name: "FillerPreset",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true),
FillerKind = table.Column<int>(type: "INTEGER", nullable: false),
FillerMode = table.Column<int>(type: "INTEGER", nullable: false),
Duration = table.Column<TimeSpan>(type: "TEXT", nullable: true),
Count = table.Column<int>(type: "INTEGER", nullable: true),
PadToNearestMinute = table.Column<int>(type: "INTEGER", nullable: true),
CollectionType = table.Column<int>(type: "INTEGER", nullable: false),
CollectionId = table.Column<int>(type: "INTEGER", nullable: true),
MediaItemId = table.Column<int>(type: "INTEGER", nullable: true),
MultiCollectionId = table.Column<int>(type: "INTEGER", nullable: true),
SmartCollectionId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_FillerPreset", x => x.Id);
table.ForeignKey(
name: "FK_FillerPreset_Collection_CollectionId",
column: x => x.CollectionId,
principalTable: "Collection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_FillerPreset_MediaItem_MediaItemId",
column: x => x.MediaItemId,
principalTable: "MediaItem",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_FillerPreset_MultiCollection_MultiCollectionId",
column: x => x.MultiCollectionId,
principalTable: "MultiCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_FillerPreset_SmartCollection_SmartCollectionId",
column: x => x.SmartCollectionId,
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItem_FallbackFillerId",
table: "ProgramScheduleItem",
column: "FallbackFillerId");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItem_MidRollFillerId",
table: "ProgramScheduleItem",
column: "MidRollFillerId");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItem_PostRollFillerId",
table: "ProgramScheduleItem",
column: "PostRollFillerId");
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItem_PreRollFillerId",
table: "ProgramScheduleItem",
column: "PreRollFillerId");
migrationBuilder.CreateIndex(
name: "IX_FillerPreset_CollectionId",
table: "FillerPreset",
column: "CollectionId");
migrationBuilder.CreateIndex(
name: "IX_FillerPreset_MediaItemId",
table: "FillerPreset",
column: "MediaItemId");
migrationBuilder.CreateIndex(
name: "IX_FillerPreset_MultiCollectionId",
table: "FillerPreset",
column: "MultiCollectionId");
migrationBuilder.CreateIndex(
name: "IX_FillerPreset_SmartCollectionId",
table: "FillerPreset",
column: "SmartCollectionId");
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailSmartCollectionId",
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_FallbackFillerId",
table: "ProgramScheduleItem",
column: "FallbackFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_MidRollFillerId",
table: "ProgramScheduleItem",
column: "MidRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PostRollFillerId",
table: "ProgramScheduleItem",
column: "PostRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PreRollFillerId",
table: "ProgramScheduleItem",
column: "PreRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_SmartCollection_SmartCollectionId",
table: "ProgramScheduleItem",
column: "SmartCollectionId",
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
table: "ProgramScheduleDurationItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_FallbackFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_MidRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PostRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PreRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_SmartCollection_SmartCollectionId",
table: "ProgramScheduleItem");
migrationBuilder.DropTable(
name: "FillerPreset");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleItem_FallbackFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleItem_MidRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleItem_PostRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleItem_PreRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "FallbackFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "MidRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "PostRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "PreRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
table: "ProgramScheduleDurationItem",
column: "TailSmartCollectionId",
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_SmartCollection_SmartCollectionId",
table: "ProgramScheduleItem",
column: "SmartCollectionId",
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

3696
ErsatzTV.Infrastructure/Migrations/20211019001254_Add_ProgramScheduleItem_TailFiller.Designer.cs generated

File diff suppressed because it is too large Load Diff

140
ErsatzTV.Infrastructure/Migrations/20211019001254_Add_ProgramScheduleItem_TailFiller.cs

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_ProgramScheduleItem_TailFiller : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_FallbackFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_MidRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PostRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PreRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.AddColumn<int>(
name: "TailFillerId",
table: "ProgramScheduleItem",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_ProgramScheduleItem_TailFillerId",
table: "ProgramScheduleItem",
column: "TailFillerId");
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_FallbackFillerId",
table: "ProgramScheduleItem",
column: "FallbackFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_MidRollFillerId",
table: "ProgramScheduleItem",
column: "MidRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PostRollFillerId",
table: "ProgramScheduleItem",
column: "PostRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PreRollFillerId",
table: "ProgramScheduleItem",
column: "PreRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_TailFillerId",
table: "ProgramScheduleItem",
column: "TailFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_FallbackFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_MidRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PostRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PreRollFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_TailFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropIndex(
name: "IX_ProgramScheduleItem_TailFillerId",
table: "ProgramScheduleItem");
migrationBuilder.DropColumn(
name: "TailFillerId",
table: "ProgramScheduleItem");
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_FallbackFillerId",
table: "ProgramScheduleItem",
column: "FallbackFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_MidRollFillerId",
table: "ProgramScheduleItem",
column: "MidRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PostRollFillerId",
table: "ProgramScheduleItem",
column: "PostRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ProgramScheduleItem_FillerPreset_PreRollFillerId",
table: "ProgramScheduleItem",
column: "PreRollFillerId",
principalTable: "FillerPreset",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

3696
ErsatzTV.Infrastructure/Migrations/20211019173552_Migrate_TailFiller_FillerPreset.Designer.cs generated

File diff suppressed because it is too large Load Diff

35
ErsatzTV.Infrastructure/Migrations/20211019173552_Migrate_TailFiller_FillerPreset.cs

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Migrate_TailFiller_FillerPreset : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"
insert into FillerPreset (Name, FillerKind, FillerMode, CollectionType, MediaItemId, CollectionId, MultiCollectionId, SmartCollectionId)
select
'Migrated_Filler_' || TailCollectionType || '_' || ifnull(TailCollectionId, '') || '_' || ifnull(TailMediaItemId, '') || '_' || ifnull(TailMultiCollectionId, '') || '_' || ifnull(TailSmartCollectionId, ''),
4,
0,
TailCollectionType,
TailMediaItemId,
TailCollectionId,
TailMultiCollectionId,
TailSmartCollectionId
from (select distinct TailCollectionType, TailCollectionId, TailMediaItemId, TailMultiCollectionId, TailSmartCollectionId from ProgramScheduleDurationItem)");
migrationBuilder.Sql(
@"
update ProgramScheduleItem
set TailFillerId = FPID
from (select fp.Id as FPID, psdi.Id as PSDIID from FillerPreset fp inner join ProgramScheduleDurationItem psdi where TailCollectionType = CollectionType and TailCollectionId is CollectionId and TailMediaItemId is MediaItemId and TailMultiCollectionId is MultiCollectionId and TailSmartCollectionId is SmartCollectionId) as whatever
where PSDIID = ProgramScheduleItem.Id");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

3645
ErsatzTV.Infrastructure/Migrations/20211019231930_Remove_DurationTailFiller.Designer.cs generated

File diff suppressed because it is too large Load Diff

148
ErsatzTV.Infrastructure/Migrations/20211019231930_Remove_DurationTailFiller.cs

@ -0,0 +1,148 @@ @@ -0,0 +1,148 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Remove_DurationTailFiller : Migration
{
protected override void Up(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");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
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.Cascade);
}
}
}

3648
ErsatzTV.Infrastructure/Migrations/20211020014526_Add_PlayoutItemIsFallback.Designer.cs generated

File diff suppressed because it is too large Load Diff

24
ErsatzTV.Infrastructure/Migrations/20211020014526_Add_PlayoutItemIsFallback.cs

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save