Compare commits

...

32 Commits

Author SHA1 Message Date
Jason Dove
1b29e252ff update changelog for release v0.2.5-alpha [no ci] 2021-11-21 07:24:20 -06:00
Jason Dove
a4dc9bfb31 Ignore local plex guids (#488)
* ignore local plex guids

* update dependencies
2021-11-21 06:25:56 -06:00
Jason Dove
184c21a91b optimize trakt matching (#487) 2021-11-21 06:13:28 -06:00
Jason Dove
6ea3191cf8 fix playout building (#486) 2021-11-20 22:36:15 -06:00
Jason Dove
d487bbca08 include other video title in channel guide (#483) 2021-11-16 08:46:07 -06:00
Jason Dove
05034b47e2 update changelog for release v0.2.4-alpha [no ci] 2021-11-13 12:54:41 -06:00
Jason Dove
b0c85b6478 use scale_cuda instead of scale_npp (#481) 2021-11-13 09:06:02 -06:00
Jason Dove
f1356563da fix ef shared table warnings (#480) 2021-11-10 18:03:28 -06:00
Jason Dove
c0aad028a8 more dotnet 6 updates (#479) 2021-11-09 13:09:57 -06:00
Jason Dove
dae06ec0ef upgrade to dotnet 6 (#475) 2021-11-09 07:44:34 -06:00
Jason Dove
72f452fd36 update dependencies (#474) 2021-11-09 06:16:26 -06:00
Jason Dove
aaf832c0b6 update changelog for release v0.2.3-alpha [no ci] 2021-11-03 13:53:06 -05:00
Jason Dove
08a18daf23 movie scanner should respect .etvignore files (#468) 2021-11-03 05:47:29 -05:00
Jason Dove
90c1c61a09 fix bug with flood playout mode (#467) 2021-11-02 21:47:46 -05:00
Jason Dove
053db71d44 fix decimal separator in ffmpeg apad filter syntax (#464) 2021-11-01 22:18:06 -05:00
Jason Dove
11f90f5d44 update changelog for release v0.2.2-alpha [no ci] 2021-10-30 17:49:30 -05:00
Jason Dove
bda4117655 allow per-episode folders in local show libraries (#462)
* allow per-episode folders in local show libraries

* fix subfolder etag generation
2021-10-30 12:54:07 -05:00
Jason Dove
3240703840 fix build 2021-10-30 12:24:27 -05:00
Jason Dove
53a7570ba3 fix epg for multiple playout mode (#461) 2021-10-30 12:16:39 -05:00
Jason Dove
0e789fd6d8 update dependencies and fix languageext deprecation warnings (#460) 2021-10-30 11:57:50 -05:00
Jason Dove
0136de700c add global and channel fallback filler (#459)
* configure channel and global fallback filler

* play random item from configured channel/global fallback filler as needed
2021-10-30 11:45:40 -05:00
Jason Dove
2ea0e64ac1 fix duration schedule item epg (#455) 2021-10-24 22:00:21 -05:00
Jason Dove
5993f23ec5 update changelog for release v0.2.1-alpha [no ci] 2021-10-24 19:46:33 -05:00
Jason Dove
417f35a834 fix saving dynamic start time (#453) 2021-10-24 18:18:22 -05:00
Jason Dove
a74547997d scheduling fixes (#451)
* scheduling fixes

* restore plex service

* restore plex service part 2
2021-10-24 06:49:35 -05:00
Jason Dove
a2f74dd284 update changelog for release v0.2.0-alpha [no ci] 2021-10-23 20:18:01 -05:00
Jason Dove
373daf9ce6 add basic filler docs [no docker] 2021-10-23 20:14:10 -05:00
Jason Dove
68693cffa0 use info log level for search index migration 2021-10-21 20:43:17 -05:00
Jason Dove
6d147de2f3 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
2021-10-21 20:23:14 -05:00
Jason Dove
f4a63a1a1a fix deleting jellyfin and emby movies (#447)
* fix deleting jellyfin and emby movies

* revert jf service change
2021-10-19 14:33:48 -05:00
Jason Dove
bc9d17ca25 show path replacement logs by default (#445) 2021-10-19 06:32:00 -05:00
Jason Dove
42e13cbbaf fix generated streams with mpeg2video (#444) 2021-10-18 19:51:50 -05:00
207 changed files with 63124 additions and 1185 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2021.1.3",
"version": "2021.2.2",
"commands": [
"jb"
]

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear

View File

@@ -18,8 +18,11 @@ jobs:
kind: windows
target: win-x64
- os: macos-latest
kind: maxOS
kind: macOS
target: osx-x64
- os: macos-latest
kind: macOS
target: osx-arm64
runs-on: ${{ matrix.os }}
steps:
- name: Get the sources
@@ -28,7 +31,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@@ -44,7 +47,7 @@ jobs:
release_name="ErsatzTV-$tag-${{ matrix.target }}"
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:PublishSingleFile=true --self-contained true
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:EnableCompressionInSingleFile=true /property:PublishSingleFile=true --self-contained true
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then

View File

@@ -5,6 +5,78 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.2.5-alpha] - 2021-11-21
### Fixed
- Include other video title in channel guide (xmltv)
- Fix bug introduced with 0.2.4-alpha that caused some playouts to build from year 0
- Use less memory matching Trakt list items
### Added
- Build osx-arm64 packages on release
### Changed
- No longer warn about local Plex guids; they aren't used for Trakt matching and can be ignored
## [0.2.4-alpha] - 2021-11-13
### Changed
- Upgrade to dotnet 6
- Use `scale_cuda` instead of `scale_npp` for NVIDIA scaling in all cases
## [0.2.3-alpha] - 2021-11-03
### Fixed
- Fix bug with audio filter in cultures where `.` is a group/thousands separator
- Fix bug where flood playout mode would only schedule one item
- This would happen if the flood was followed by another flood with a fixed start time
### Added
- Support empty `.etvignore` file to instruct local movie scanner to ignore the containing folder
## [0.2.2-alpha] - 2021-10-30
### Fixed
- Fix EPG entries for Duration schedule items that play multiple items
- Fix EPG entries for Multiple schedule items that play more than one item
### Added
- Add fallback filler settings to Channel and global FFmpeg Settings
- When streaming is attempted during an unscheduled gap, the resulting video will be determined using the following priority:
- Channel fallback filler
- Global fallback filler
- Generated `Channel Is Offline` error message video
### Changed
- Allow per-episode folders for local show libraries
- e.g. `Show Name\Season #\Episode #\Show Name - s#e#.mkv`
## [0.2.1-alpha] - 2021-10-24
### Fixed
- Fix saving dynamic start time on schedule items
## [0.2.0-alpha] - 2021-10-23
### 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
- Fix bug that caused large unscheduled gaps in playouts
- This was caused by schedule items with a fixed start of midnight
### 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
- Fix double scheduling; this could happen if the app was shutdown during a playout build
@@ -12,13 +84,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
@@ -730,7 +802,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.5-alpha...HEAD
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.5-alpha...HEAD
[0.2.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.4-alpha...v0.2.5-alpha
[0.2.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.3-alpha...v0.2.4-alpha
[0.2.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.2-alpha...v0.2.3-alpha
[0.2.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.1-alpha...v0.2.2-alpha
[0.2.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.0-alpha...v0.2.1-alpha
[0.2.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.5-alpha...v0.2.0-alpha
[0.1.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...v0.1.5-alpha
[0.1.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.3-alpha...v0.1.4-alpha
[0.1.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.2-alpha...v0.1.3-alpha

View File

@@ -10,5 +10,6 @@ namespace ErsatzTV.Application.Channels
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId);
int? WatermarkId,
int? FallbackFillerId);
}

View File

@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.Channels.Commands
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId) : IRequest<Either<BaseError, CreateChannelResult>>;
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
}

View File

@@ -7,6 +7,7 @@ using System.Threading;
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;
@@ -42,9 +43,10 @@ namespace ErsatzTV.Application.Channels.Commands
(ValidateName(request), await ValidateNumber(dbContext, request),
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredLanguage(request),
await WatermarkMustExist(dbContext, request))
await WatermarkMustExist(dbContext, request),
await FillerPresetMustExist(dbContext, request))
.Apply(
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId) =>
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
@@ -74,6 +76,11 @@ namespace ErsatzTV.Application.Channels.Commands
channel.WatermarkId = id;
}
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
@@ -131,5 +138,25 @@ namespace ErsatzTV.Application.Channels.Commands
.MapT(_ => Optional(createChannel.WatermarkId))
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
}
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.FallbackFillerId is null)
{
return Option<int>.None;
}
return await dbContext.FillerPresets
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId))
.Map(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
}
}
}

View File

@@ -14,5 +14,6 @@ namespace ErsatzTV.Application.Channels.Commands
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId) : IRequest<Either<BaseError, ChannelViewModel>>;
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
}

View File

@@ -67,6 +67,7 @@ namespace ErsatzTV.Application.Channels.Commands
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}

View File

@@ -15,7 +15,8 @@ namespace ErsatzTV.Application.Channels
GetLogo(channel),
channel.PreferredLanguageCode,
channel.StreamingMode,
channel.WatermarkId);
channel.WatermarkId,
channel.FallbackFillerId);
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))

View File

@@ -25,7 +25,7 @@ namespace ErsatzTV.Application.Configuration.Commands
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Filter(lri => lri > 0)
.Where(lri => lri > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();

View File

@@ -58,7 +58,7 @@ namespace ErsatzTV.Application.Configuration.Commands
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
.Filter(days => days > 0)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();

View File

@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Emby.Commands
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Filter(match => match)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}

View File

@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Emby.Commands
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Filter(match => match)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}

View File

@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.63">
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -115,6 +115,17 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
}
if (request.Settings.GlobalFallbackFillerId is not null)
{
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegGlobalFallbackFillerId,
request.Settings.GlobalFallbackFillerId.Value);
}
else
{
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId);
}
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSegmenterTimeout,
request.Settings.HlsSegmenterIdleTimeout);

View File

@@ -8,6 +8,7 @@
public string PreferredLanguageCode { get; set; }
public bool SaveReports { get; set; }
public int? GlobalWatermarkId { get; set; }
public int? GlobalFallbackFillerId { get; set; }
public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; }
}

View File

@@ -28,6 +28,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
Option<int> watermark =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
Option<int> fallbackFiller =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
Option<int> hlsSegmenterIdleTimeout =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
Option<int> workAheadSegmenterLimit =
@@ -49,6 +51,11 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
result.GlobalWatermarkId = watermarkId;
}
foreach (int fallbackFillerId in fallbackFiller)
{
result.GlobalFallbackFillerId = fallbackFillerId;
}
return result;
}
}

View File

@@ -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>>;
}

View File

@@ -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);
}
}
}
}

View File

@@ -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>>;
}

View File

@@ -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."));
}
}

View File

@@ -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>>;
}

View File

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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

@@ -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());
}
}
}

View File

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

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -24,7 +24,7 @@ namespace ErsatzTV.Application.HDHR.Commands
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
Optional(request.TunerCount)
.Filter(tc => tc > 0)
.Where(tc => tc > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();

View File

@@ -97,7 +97,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
{
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Filter(match => match)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Jellyfin media source requires an api key");
}

View File

@@ -69,7 +69,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
{
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Filter(match => match)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Jellyfin media source requires an api key");
}

View File

@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
{
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Filter(match => match)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Jellyfin media source requires an api key");
}

View File

@@ -46,7 +46,7 @@ namespace ErsatzTV.Application.Libraries.Commands
.Map(list => list.Map(c => c.Path).ToList());
return Optional(request.Path)
.Filter(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
.Where(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
.ToValidation<BaseError>("Path must not belong to another library path");
}

View File

@@ -33,7 +33,7 @@ namespace ErsatzTV.Application.Libraries.Commands
.Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList());
return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path))))
.Filter(length => length == 0)
.Where(length => length == 0)
.Map(_ => localLibrary)
.ToValidation<BaseError>("Path must not belong to another library path");
}

View File

@@ -60,7 +60,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
var result2 = Optional(createCollection.Name)
.Filter(name => !allNames.Contains(name))
.Where(name => !allNames.Contains(name))
.ToValidation<BaseError>("Collection name must be unique");
return (result1, result2).Apply((_, _) => createCollection.Name);

View File

@@ -104,7 +104,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name));
var result2 = Optional(createMultiCollection.Name)
.Filter(name => !allNames.Contains(name))
.Where(name => !allNames.Contains(name))
.ToValidation<BaseError>("MultiCollection name must be unique");
return (result1, result2).Apply((_, _) => createMultiCollection.Name);

View File

@@ -61,7 +61,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name));
var result2 = Optional(createSmartCollection.Name)
.Filter(name => !allNames.Contains(name))
.Where(name => !allNames.Contains(name))
.ToValidation<BaseError>("SmartCollection name must be unique");
return (result1, result2).Apply((_, _) => createSmartCollection.Name);

View File

@@ -39,7 +39,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
{
try
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, TraktList> validation = await TraktListMustExist(dbContext, request.TraktListId);
return await validation.Match(

View File

@@ -208,6 +208,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
var guids = item.Guids.Map(g => g.Guid).ToList();
Option<int> maybeMovieByGuid = await dbContext.MovieMetadata
.AsNoTracking()
.Filter(mm => mm.Guids.Any(g => guids.Contains(g.Guid)))
.FirstOrDefaultAsync()
.Map(Optional)
@@ -220,6 +221,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
}
Option<int> maybeMovieByTitleYear = await dbContext.MovieMetadata
.AsNoTracking()
.Filter(mm => mm.Title == item.Title && mm.Year == item.Year)
.FirstOrDefaultAsync()
.Map(Optional)
@@ -241,6 +243,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
var guids = item.Guids.Map(g => g.Guid).ToList();
Option<int> maybeShowByGuid = await dbContext.ShowMetadata
.AsNoTracking()
.Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid)))
.FirstOrDefaultAsync()
.Map(Optional)
@@ -253,6 +256,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
}
Option<int> maybeShowByTitleYear = await dbContext.ShowMetadata
.AsNoTracking()
.Filter(sm => sm.Title == item.Title && sm.Year == item.Year)
.FirstOrDefaultAsync()
.Map(Optional)
@@ -274,6 +278,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
var guids = item.Guids.Map(g => g.Guid).ToList();
Option<int> maybeSeasonByGuid = await dbContext.SeasonMetadata
.AsNoTracking()
.Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid)))
.FirstOrDefaultAsync()
.Map(Optional)
@@ -286,6 +291,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
}
Option<int> maybeSeasonByTitleYear = await dbContext.SeasonMetadata
.AsNoTracking()
.Filter(sm => sm.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year))
.Filter(sm => sm.Season.SeasonNumber == item.Season)
.FirstOrDefaultAsync()
@@ -308,6 +314,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
var guids = item.Guids.Map(g => g.Guid).ToList();
Option<int> maybeEpisodeByGuid = await dbContext.EpisodeMetadata
.AsNoTracking()
.Filter(em => em.Guids.Any(g => guids.Contains(g.Guid)))
.FirstOrDefaultAsync()
.Map(Optional)
@@ -320,6 +327,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
}
Option<int> maybeEpisodeByTitleYear = await dbContext.EpisodeMetadata
.AsNoTracking()
.Filter(sm => sm.Episode.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year))
.Filter(em => em.Episode.Season.SeasonNumber == item.Season)
.Filter(sm => sm.Episode.EpisodeMetadata.Any(e => e.EpisodeNumber == item.Episode))

View File

@@ -156,7 +156,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name));
var result2 = Optional(updateMultiCollection.Name)
.Filter(name => !allNames.Contains(name))
.Where(name => !allNames.Contains(name))
.ToValidation<BaseError>("MultiCollection name must be unique");
return (result1, result2).Apply((_, _) => updateMultiCollection.Name);

View File

@@ -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."));
}

View File

@@ -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
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
.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);
}
}

View File

@@ -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>;
}

View File

@@ -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
{
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
.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)

View File

@@ -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;
}

View File

@@ -65,7 +65,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
.CountAsync(ps => ps.Name == createProgramSchedule.Name);
var result2 = Optional(duplicateNameCount)
.Filter(count => count == 0)
.Where(count => count == 0)
.ToValidation<BaseError>("Schedule name must be unique");
return (result1, result2).Apply((_, _) => createProgramSchedule.Name);

View File

@@ -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; }
}
}

View File

@@ -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
.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
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");
@@ -128,7 +169,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixStartTime(item.StartTime),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@@ -136,13 +177,18 @@ 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
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixStartTime(item.StartTime),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@@ -150,13 +196,18 @@ 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
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixStartTime(item.StartTime),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@@ -165,13 +216,18 @@ 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
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
StartTime = FixStartTime(item.StartTime),
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
@@ -180,18 +236,23 @@ 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}")
};
private static TimeSpan FixDuration(TimeSpan duration) =>
duration > TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
duration >= TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
private static TimeSpan? FixStartTime(TimeSpan? startTime) =>
startTime.HasValue && startTime.Value >= TimeSpan.FromDays(1)
? startTime.Value.Subtract(TimeSpan.FromDays(1))
: startTime;
}
}

View File

@@ -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<

View File

@@ -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
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)

View File

@@ -41,25 +41,23 @@ namespace ErsatzTV.Application.ProgramSchedules
duration.PlaybackOrder,
duration.PlayoutDuration,
duration.TailMode,
duration.TailCollectionType,
duration.TailCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailCollection)
: null,
duration.TailMultiCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailMultiCollection)
: null,
duration.TailSmartCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.TailSmartCollection)
: null,
duration.TailMediaItem switch
{
Show show => MediaItems.Mapper.ProjectToViewModel(show),
Season season => MediaItems.Mapper.ProjectToViewModel(season),
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
_ => null
},
duration.CustomTitle,
duration.GuideMode),
duration.GuideMode,
duration.PreRollFiller != null
? Filler.Mapper.ProjectToViewModel(duration.PreRollFiller)
: null,
duration.MidRollFiller != null
? Filler.Mapper.ProjectToViewModel(duration.MidRollFiller)
: null,
duration.PostRollFiller != null
? Filler.Mapper.ProjectToViewModel(duration.PostRollFiller)
: null,
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
},
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
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
},
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}")
};

View File

@@ -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
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
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; }
}
}

View File

@@ -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
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
mediaItem,
playbackOrder,
customTitle,
guideMode)
guideMode,
preRollFiller,
midRollFiller,
postRollFiller,
tailFiller,
fallbackFiller)
{
}
}

View File

@@ -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
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
mediaItem,
playbackOrder,
customTitle,
guideMode) =>
guideMode,
preRollFiller,
midRollFiller,
postRollFiller,
tailFiller,
fallbackFiller) =>
Count = count;
public int Count { get; }

View File

@@ -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
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
mediaItem,
playbackOrder,
customTitle,
guideMode)
guideMode,
preRollFiller,
midRollFiller,
postRollFiller,
tailFiller,
fallbackFiller)
{
}
}

View File

@@ -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
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
{

View File

@@ -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
.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());
}

View File

@@ -44,18 +44,18 @@ namespace ErsatzTV.Application.Search.Commands
await _configElementRepository.GetValue<int>(ConfigElementKey.SearchIndexVersion) <
_searchIndex.Version)
{
_logger.LogDebug("Migrating search index to version {Version}", _searchIndex.Version);
_logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version);
List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
await _searchIndex.Rebuild(_searchRepository, itemIds);
await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version);
_logger.LogDebug("Done migrating search index");
_logger.LogInformation("Done migrating search index");
}
else
{
_logger.LogDebug("Search index is already version {Version}", _searchIndex.Version);
_logger.LogInformation("Search index is already version {Version}", _searchIndex.Version);
}
return Unit.Default;

View File

@@ -83,7 +83,7 @@ namespace ErsatzTV.Application.Streaming.Commands
private Task<Validation<BaseError, Unit>> SessionMustBeInactive(StartFFmpegSession request)
{
var result = Optional(_ffmpegSegmenterService.SessionWorkers.TryAdd(request.ChannelNumber, null))
.Filter(success => success)
.Where(success => success)
.Map(_ => Unit.Default)
.ToValidation<BaseError>(new ChannelSessionAlreadyActive());

View File

@@ -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));

View File

@@ -1,17 +1,21 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
@@ -24,6 +28,9 @@ namespace ErsatzTV.Application.Streaming.Queries
FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
{
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
private readonly IArtistRepository _artistRepository;
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILocalFileSystem _localFileSystem;
@@ -37,6 +44,9 @@ namespace ErsatzTV.Application.Streaming.Queries
IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService,
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory)
{
@@ -45,6 +55,9 @@ namespace ErsatzTV.Application.Streaming.Queries
_plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService;
_embyPathReplacementService = embyPathReplacementService;
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_artistRepository = artistRepository;
_runtimeInfo = runtimeInfo;
}
@@ -85,6 +98,11 @@ namespace ErsatzTV.Application.Streaming.Queries
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
.BindT(ValidatePlayoutItemPath);
if (maybePlayoutItem.LeftAsEnumerable().Any(e => e is UnableToLocatePlayoutItem))
{
maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now);
}
return await maybePlayoutItem.Match(
async playoutItemWithPath =>
{
@@ -114,11 +132,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);
@@ -130,7 +152,7 @@ namespace ErsatzTV.Application.Streaming.Queries
$"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'";
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
.Filter(transcode => transcode)
.Where(transcode => transcode)
.Match(
_ => dbContext.PlayoutItems
.Filter(pi => pi.Playout.ChannelId == channel.Id)
@@ -154,8 +176,7 @@ namespace ErsatzTV.Application.Streaming.Queries
maybeDuration,
"Channel is Offline",
request.HlsRealtime);
return new PlayoutItemProcessModel(errorProcess, finish);
}
else
@@ -207,6 +228,95 @@ namespace ErsatzTV.Application.Streaming.Queries
});
}
private async Task<Either<BaseError, PlayoutItemWithPath>> CheckForFallbackFiller(
TvContext dbContext,
Channel channel,
DateTimeOffset now)
{
// check for channel fallback
Option<FillerPreset> maybeFallback = await dbContext.FillerPresets
.SelectOneAsync(w => w.Id, w => w.Id == channel.FallbackFillerId);
// then check for global fallback
if (maybeFallback.IsNone)
{
maybeFallback = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId)
.BindT(fillerId => dbContext.FillerPresets.SelectOneAsync(w => w.Id, w => w.Id == fillerId));
}
foreach (FillerPreset fallbackPreset in maybeFallback)
{
// turn this into a playout item
var collectionKey = CollectionKey.ForFillerPreset(fallbackPreset);
List<MediaItem> items = await MediaItemsForCollection.Collect(
_mediaCollectionRepository,
_televisionRepository,
_artistRepository,
collectionKey);
// TODO: shuffle? does it really matter since we loop anyway
MediaItem item = items[new Random().Next(items.Count)];
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
.Where(transcode => transcode)
.Match(
_ => dbContext.PlayoutItems
.Filter(pi => pi.Playout.ChannelId == channel.Id)
.Filter(pi => pi.Start > now.UtcDateTime)
.OrderBy(pi => pi.Start)
.FirstOrDefaultAsync()
.Map(Optional)
.MapT(pi => pi.StartOffset - now),
() => Option<TimeSpan>.None.AsTask());
MediaVersion version = item 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(item))
};
version.MediaFiles = await dbContext.MediaFiles
.AsNoTracking()
.Filter(mf => mf.MediaVersionId == version.Id)
.ToListAsync();
version.Streams = await dbContext.MediaStreams
.AsNoTracking()
.Filter(ms => ms.MediaVersionId == version.Id)
.ToListAsync();
DateTimeOffset finish = maybeDuration.Match(
// next playout item exists
// loop until it starts
now.Add,
// no next playout item exists
// loop for 5 minutes if less than 30s, otherwise play full item
() => version.Duration < TimeSpan.FromSeconds(30)
? now.AddMinutes(5)
: now.Add(version.Duration));
var playoutItem = new PlayoutItem
{
MediaItem = item,
MediaItemId = item.Id,
Start = now.UtcDateTime,
Finish = finish.UtcDateTime,
FillerKind = FillerKind.Fallback,
InPoint = TimeSpan.Zero,
OutPoint = version.Duration
};
return await ValidatePlayoutItemPath(playoutItem);
}
return new UnableToLocatePlayoutItem();
}
private async Task<Either<BaseError, PlayoutItemWithPath>> ValidatePlayoutItemPath(PlayoutItem playoutItem)
{
string path = await GetPlayoutItemPath(playoutItem);

View File

@@ -1,24 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.63">
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
</ItemGroup>

View File

@@ -27,7 +27,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
[Test]
public void Should_Return_Audio_Filter_With_AudioDuration()
{
var duration = TimeSpan.FromMinutes(54);
var duration = TimeSpan.FromMilliseconds(1000.1);
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithAlignedAudio(duration);
@@ -37,7 +37,28 @@ namespace ErsatzTV.Core.Tests.FFmpeg
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be($"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a]");
filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]");
filter.AudioLabel.Should().Be("[a]");
filter.VideoLabel.Should().Be("0:0");
});
}
[Test]
// this needs to be a culture where '.' is a group separator
[SetCulture("it-IT")]
public void Should_Return_Audio_Filter_With_AudioDuration_Decimal()
{
var duration = TimeSpan.FromMilliseconds(1000.1);
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithAlignedAudio(duration);
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]");
filter.AudioLabel.Should().Be("[a]");
filter.VideoLabel.Should().Be("0:0");
});
@@ -346,7 +367,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
true,
true,
false,
"[0:0]yadif_cuda,scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
"[0:0]yadif_cuda,scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
"[v]")]
[TestCase(
true,
@@ -358,13 +379,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
true,
true,
true,
"[0:0]yadif_cuda,scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[0:0]yadif_cuda,scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
false,
true,
false,
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
"[0:0]scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
"[v]")]
[TestCase(
false,
@@ -376,7 +397,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false,
true,
true,
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[0:0]scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
public void Should_Return_NVENC_Video_Filter(
bool deinterlace,

View File

@@ -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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero);
actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv);
}

View File

@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
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 +186,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;
@@ -232,7 +238,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
return string.Empty;
}
using var sha = new System.Security.Cryptography.SHA256Managed();
using var sha = SHA256.Create();
byte[] textData = System.Text.Encoding.UTF8.GetBytes(text);
byte[] hash = sha.ComputeHash(textData);
return BitConverter.ToString(hash).Replace("-", string.Empty);

View File

@@ -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);

View File

@@ -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
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
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
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
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
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
},
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
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();
}

View File

@@ -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 }
}
};
}
}

View File

@@ -0,0 +1,584 @@
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(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[0].GuideFinish.HasValue.Should().BeFalse();
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[1].GuideFinish.HasValue.Should().BeFalse();
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);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
}
[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(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[0].GuideFinish.HasValue.Should().BeFalse();
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[1].GuideFinish.HasValue.Should().BeFalse();
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[2].GuideFinish.HasValue.Should().BeTrue();
}
[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(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[0].GuideFinish.HasValue.Should().BeFalse();
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[1].GuideFinish.HasValue.Should().BeFalse();
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[2].GuideFinish.HasValue.Should().BeTrue();
}
[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(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[0].GuideFinish.HasValue.Should().BeFalse();
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[1].GuideFinish.HasValue.Should().BeFalse();
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[2].GuideFinish.HasValue.Should().BeTrue();
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);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
}
[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(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[0].GuideFinish.HasValue.Should().BeFalse();
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[1].GuideFinish.HasValue.Should().BeFalse();
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[2].GuideFinish.HasValue.Should().BeTrue();
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[3].GuideFinish.HasValue.Should().BeFalse();
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[3].GuideFinish.HasValue.Should().BeFalse();
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);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
}
[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(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[0].GuideFinish.HasValue.Should().BeFalse();
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[1].GuideFinish.HasValue.Should().BeFalse();
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[2].GuideFinish.HasValue.Should().BeTrue();
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[3].GuideFinish.HasValue.Should().BeFalse();
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[4].GuideFinish.HasValue.Should().BeFalse();
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[5].GuideFinish.HasValue.Should().BeFalse();
}
[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(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[0].GuideFinish.HasValue.Should().BeFalse();
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[1].GuideFinish.HasValue.Should().BeFalse();
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[2].GuideFinish.HasValue.Should().BeTrue();
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[3].GuideFinish.HasValue.Should().BeFalse();
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[4].GuideFinish.HasValue.Should().BeFalse();
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[5].GuideFinish.HasValue.Should().BeFalse();
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);
playoutItems[6].GuideFinish.HasValue.Should().BeFalse();
}
}
}

View File

@@ -0,0 +1,784 @@
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_Flood()
{
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,
// this caused trouble with the peek logic and the IsFlood flag
new ProgramScheduleItemFlood
{
StartTime = TimeSpan.FromHours(3),
}
};
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),
};
}
}

View File

@@ -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(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_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(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 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(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 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(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 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(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 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(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 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(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)
};
}
}

View File

@@ -0,0 +1,706 @@
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_Have_Gap_With_Empty_Tail_Empty_Fallback()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
var collectionTwo = new Collection { Id = 2, Name = "Collection 2", MediaItems = new List<MediaItem>() };
var collectionThree = new Collection { Id = 3, Name = "Collection 3", MediaItems = new List<MediaItem>() };
var scheduleItem = new ProgramScheduleItemOne
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlaybackOrder = PlaybackOrder.Chronological,
TailFiller = new FillerPreset
{
FillerKind = FillerKind.Tail,
CollectionId = collectionTwo.Id,
Collection = collectionTwo
},
FallbackFiller = new FillerPreset
{
FillerKind = FillerKind.Fallback,
CollectionId = collectionThree.Id,
Collection = collectionThree
}
};
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(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);
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);
}
[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);
}
[Test]
public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad()
{
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
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,
PostRollFiller = new FillerPreset
{
FillerKind = FillerKind.PostRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30,
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.PostRollFiller, 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);
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.PostRoll);
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.PostRoll);
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.PostRoll);
}
[Test]
public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad_With_Chapters()
{
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0), 2);
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
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,
PostRollFiller = new FillerPreset
{
FillerKind = FillerKind.PostRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30,
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.PostRollFiller, 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(0);
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.PostRoll);
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.PostRoll);
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.PostRoll);
}
protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{
StartTime = TimeSpan.FromHours(3)
};
}
}

View File

@@ -0,0 +1,92 @@
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, int chapterCount = 0)
{
var result = new Movie()
{
Id = id,
MovieMetadata = new List<MovieMetadata> { new() { ReleaseDate = aired } },
MediaVersions = new List<MediaVersion>
{
new() { Duration = duration, Chapters = new List<MediaChapter>() }
}
};
for (var i = 0; i < chapterCount; i++)
{
result.MediaVersions.Head().Chapters.Add(
new MediaChapter
{
StartTime = TimeSpan.FromMilliseconds(i * duration.TotalMilliseconds / chapterCount),
EndTime = TimeSpan.FromMilliseconds(i + 1 * duration.TotalMilliseconds / chapterCount)
});
}
return result;
}
protected static Collection TwoItemCollection(int id1, int id2, TimeSpan duration, int chapterCount = 0) => new()
{
Id = id1,
Name = $"Collection of Items {id1}",
MediaItems = new List<MediaItem>
{
TestMovie(id1, duration, new DateTime(2020, 1, 1), chapterCount),
TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount)
}
};
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator1,
FillerPreset fillerPreset, IMediaCollectionEnumerator enumerator2) =>
new()
{
{ CollectionKey.ForScheduleItem(scheduleItem), enumerator1 },
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator2 }
};
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Core.Domain
{
@@ -16,6 +17,8 @@ namespace ErsatzTV.Core.Domain
public FFmpegProfile FFmpegProfile { get; set; }
public int? WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
public int? FallbackFillerId { get; set; }
public FillerPreset FallbackFiller { get; set; }
public StreamingMode StreamingMode { get; set; }
public List<Playout> Playouts { get; set; }
public List<Artwork> Artwork { get; set; }

View File

@@ -13,6 +13,7 @@
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
public static ConfigElementKey FFmpegGlobalWatermarkId => new("ffmpeg.global_watermark_id");
public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id");
public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds");
public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit");
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
@@ -25,8 +26,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");
}

View File

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

View File

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

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }

View File

@@ -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();

View File

@@ -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
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
};
}
}

View File

@@ -1,4 +1,5 @@
using System;
using ErsatzTV.Core.Domain.Filler;
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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -68,7 +68,7 @@ namespace ErsatzTV.Core.Emby
if (log)
{
_logger.LogDebug(
_logger.LogInformation(
"Replacing emby path {EmbyPath} with {LocalPath} resulting in {FinalPath}",
replacement.EmbyPath,
replacement.LocalPath,

View File

@@ -1,24 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Flurl" Version="3.0.2" />
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.63">
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using ErsatzTV.Core.Domain;
@@ -111,7 +112,11 @@ namespace ErsatzTV.Core.FFmpeg
}
_audioDuration.IfSome(
audioDuration => audioFilterQueue.Add($"apad=whole_dur={audioDuration.TotalMilliseconds}ms"));
audioDuration =>
{
var durationString = audioDuration.TotalMilliseconds.ToString(NumberFormatInfo.InvariantInfo);
audioFilterQueue.Add($"apad=whole_dur={durationString}ms");
});
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None && !isHardwareDecode &&
(_deinterlace || _scaleToSize.IsSome);
@@ -158,7 +163,7 @@ namespace ErsatzTV.Core.FFmpeg
HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}",
HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" =>
$"hwupload_cuda,scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Nvenc => $"scale_npp={size.Width}:{size.Height}",
HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}",
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
};

View File

@@ -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
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
});
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
result.AudioDuration = version.Duration;
result.AudioDuration = outPoint - inPoint;
result.NormalizeLoudness = ffmpegProfile.NormalizeLoudness;
}
else

View File

@@ -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;
}
@@ -375,7 +379,8 @@ namespace ErsatzTV.Core.FFmpeg
{
"-c:v", playbackSettings.VideoCodec,
"-flags", "cgop",
"-sc_threshold", "0" // disable scene change detection
// disable scene change detection except with mpeg2video
"-sc_threshold", playbackSettings.VideoCodec == "mpeg2video" ? "1000000000" : "0"
};
if (!string.IsNullOrWhiteSpace(_outputPixelFormat))

View File

@@ -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
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
videoStream,
maybeAudioStream,
start,
now);
now,
inPoint,
outPoint);
(Option<ChannelWatermark> maybeWatermark, Option<string> maybeWatermarkPath) =
GetWatermarkOptions(channel, globalWatermark);
@@ -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
builder = builder.WithPlaybackArgs(playbackSettings)
.WithMetadata(channel, maybeAudioStream)
.WithDuration(start + version.Duration - now);
.WithDuration(finish - now);
switch (channel.StreamingMode)
{

View File

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

View File

@@ -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);
}
}

View File

@@ -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
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
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
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
}
}
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
}
}
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
}
}
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();
@@ -336,6 +342,8 @@ namespace ErsatzTV.Core.Iptv
.IfNone("[unknown show]"),
MusicVideo mv => mv.Artist.ArtistMetadata.HeadOrNone().Map(am => am.Title ?? string.Empty)
.IfNone("[unknown artist]"),
OtherVideo ov => ov.OtherVideoMetadata.HeadOrNone().Map(vm => vm.Title ?? string.Empty)
.IfNone("[unknown video]"),
_ => "[unknown]"
};
}

View File

@@ -69,7 +69,7 @@ namespace ErsatzTV.Core.Jellyfin
if (log)
{
_logger.LogDebug(
_logger.LogInformation(
"Replacing jellyfin path {JellyfinPath} with {LocalPath} resulting in {FinalPath}",
replacement.JellyfinPath,
replacement.LocalPath,

View File

@@ -9,7 +9,7 @@ namespace ErsatzTV.Core.Metadata
{
public static class FolderEtag
{
private static readonly MD5CryptoServiceProvider Crypto = new();
private static readonly MD5 Crypto = MD5.Create();
public static string Calculate(string folder, ILocalFileSystem localFileSystem)
{
@@ -31,5 +31,32 @@ namespace ErsatzTV.Core.Metadata
return hash.ToString();
}
public static string CalculateWithSubfolders(string folder, ILocalFileSystem localFileSystem)
{
IEnumerable<string> allFiles = localFileSystem.ListFiles(folder);
var sb = new StringBuilder();
foreach (string file in allFiles.OrderBy(identity))
{
sb.Append(file);
sb.Append(localFileSystem.GetLastWriteTime(file).Ticks);
}
foreach (string subfolder in localFileSystem.ListSubdirectories(folder).OrderBy(identity))
{
sb.Append(subfolder);
sb.Append(Calculate(subfolder, localFileSystem));
}
var hash = new StringBuilder();
byte[] bytes = Crypto.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()));
foreach (byte t in bytes)
{
hash.Append(t.ToString("x2"));
}
return hash.ToString();
}
}
}

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