Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5993f23ec5 | ||
|
|
417f35a834 | ||
|
|
a74547997d | ||
|
|
a2f74dd284 | ||
|
|
373daf9ce6 | ||
|
|
68693cffa0 | ||
|
|
6d147de2f3 | ||
|
|
f4a63a1a1a | ||
|
|
bc9d17ca25 | ||
|
|
42e13cbbaf | ||
|
|
6cc61f3212 | ||
|
|
4cf44616a8 | ||
|
|
33aaadae68 | ||
|
|
fe3f8e391e | ||
|
|
1a68dd040a | ||
|
|
67761c1a14 |
+50
-1
@@ -4,8 +4,54 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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
|
||||
- Fix updating Jellyfin and Emby TV seasons
|
||||
- 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
|
||||
|
||||
### 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
|
||||
|
||||
## [0.1.4-alpha] - 2021-10-14
|
||||
### Fixed
|
||||
@@ -714,7 +760,10 @@ 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.4-alpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.1-alpha...HEAD
|
||||
[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
|
||||
[0.1.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.Filler
|
||||
{
|
||||
public record PagedFillerPresetsViewModel(int TotalCount, List<FillerPresetViewModel> Page);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Filler.Queries
|
||||
{
|
||||
public record GetAllFillerPresets : IRequest<List<FillerPresetViewModel>>;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Filler.Queries
|
||||
{
|
||||
public record GetFillerPresetById(int Id) : IRequest<Option<FillerPresetViewModel>>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Filler.Queries
|
||||
{
|
||||
public record GetPagedFillerPresets(int PageNum, int PageSize) : IRequest<PagedFillerPresetsViewModel>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -23,16 +24,22 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
private readonly ILogger<SynchronizePlexMediaSourcesHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||
private readonly IPlexServerApiClient _plexServerApiClient;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
|
||||
public SynchronizePlexMediaSourcesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexTvApiClient plexTvApiClient,
|
||||
IPlexServerApiClient plexServerApiClient,
|
||||
IPlexSecretStore plexSecretStore,
|
||||
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexTvApiClient = plexTvApiClient;
|
||||
_plexServerApiClient = plexServerApiClient;
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_channel = channel;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
@@ -69,32 +76,76 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
return allExisting;
|
||||
}
|
||||
|
||||
private Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
private async Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
{
|
||||
Option<PlexMediaSource> maybeExisting =
|
||||
allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier);
|
||||
return maybeExisting.Match(
|
||||
existing =>
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
return _mediaSourceRepository.Update(existing, server.Connections, toAdd, toRemove);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
if (server.Connections.Any())
|
||||
{
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Add(server);
|
||||
});
|
||||
foreach (PlexMediaSource existing in maybeExisting)
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
await _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||
await FindConnectionToActivate(existing);
|
||||
}
|
||||
|
||||
if (maybeExisting.IsNone)
|
||||
{
|
||||
await _mediaSourceRepository.Add(server);
|
||||
await FindConnectionToActivate(server);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FindConnectionToActivate(PlexMediaSource server)
|
||||
{
|
||||
var prioritized = server.Connections.OrderBy(pc => pc.IsActive ? 0 : 1).ToList();
|
||||
foreach (PlexConnection connection in server.Connections)
|
||||
{
|
||||
connection.IsActive = false;
|
||||
}
|
||||
|
||||
Option<PlexServerAuthToken> maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier);
|
||||
foreach (PlexServerAuthToken token in maybeToken)
|
||||
{
|
||||
foreach (PlexConnection connection in prioritized)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri);
|
||||
if (await _plexServerApiClient.Ping(connection, token))
|
||||
{
|
||||
_logger.LogInformation("Located Plex at {Uri}", connection.Uri);
|
||||
connection.IsActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maybeToken.IsNone)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to activate Plex connection for server {Server} without auth token",
|
||||
server.ServerName);
|
||||
}
|
||||
|
||||
if (server.Connections.All(c => !c.IsActive))
|
||||
{
|
||||
_logger.LogError("Unable to locate Plex");
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Update(server, new List<PlexConnection>(), new List<PlexConnection>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
|
||||
+22
-1
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
};
|
||||
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
+5
-1
@@ -114,11 +114,15 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
version,
|
||||
playoutItemWithPath.Path,
|
||||
playoutItemWithPath.PlayoutItem.StartOffset,
|
||||
playoutItemWithPath.PlayoutItem.FinishOffset,
|
||||
request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now,
|
||||
maybeGlobalWatermark,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
request.HlsRealtime);
|
||||
request.HlsRealtime,
|
||||
playoutItemWithPath.PlayoutItem.FillerKind,
|
||||
playoutItemWithPath.PlayoutItem.InPoint,
|
||||
playoutItemWithPath.PlayoutItem.OutPoint);
|
||||
|
||||
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
@@ -184,11 +185,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1151,6 +1152,218 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionTwo = new Collection
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Duration Items 2",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionThree = new Collection
|
||||
{
|
||||
Id = 3,
|
||||
Name = "Filler Items",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
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
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.Filler,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
},
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 2,
|
||||
Index = 2,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.Filler,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
};
|
||||
|
||||
var configRepo = new Mock<IConfigElementRepository>();
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var artistRepo = new Mock<IArtistRepository>();
|
||||
var builder = new PlayoutBuilder(
|
||||
configRepo.Object,
|
||||
fakeRepository,
|
||||
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();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Duration_Should_Skip_Items_That_Are_Too_Long()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
|
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
|
||||
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
|
||||
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository =
|
||||
new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(1),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.None,
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
};
|
||||
|
||||
var configRepo = new Mock<IConfigElementRepository>();
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var artistRepo = new Mock<IArtistRepository>();
|
||||
var builder = new PlayoutBuilder(
|
||||
configRepo.Object,
|
||||
fakeRepository,
|
||||
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(6);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0));
|
||||
result.Items[0].MediaItemId.Should().Be(2);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
|
||||
result.Items[1].MediaItemId.Should().Be(4);
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
|
||||
result.Items[2].MediaItemId.Should().Be(2);
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[3].MediaItemId.Should().Be(4);
|
||||
result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[4].MediaItemId.Should().Be(2);
|
||||
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
|
||||
result.Items[5].MediaItemId.Should().Be(4);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.DurationFinish.Should().BeNull();
|
||||
}
|
||||
|
||||
private static DateTimeOffset HoursAfterMidnight(int hours)
|
||||
{
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
|
||||
{
|
||||
[Test]
|
||||
public void Should_Fill_Exact_Duration()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
TailMode = TailMode.None,
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_Duration_Tail_Mode_None()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
TailMode = TailMode.None,
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_Duration_Tail_Mode_Offline_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
TailMode = TailMode.Offline,
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
// duration block should end after exact duration, with gap
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_Duration_Tail_Mode_Offline_With_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
TailMode = TailMode.Offline,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_Exact_Duration()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
TailMode = TailMode.Filler,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_Duration_Tail_Mode_Filler_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
TailMode = TailMode.Filler,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_With_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
TailMode = TailMode.Filler,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
scheduleItem.TailFiller,
|
||||
enumerator2,
|
||||
scheduleItem.FallbackFiller,
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
enumerator3.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(7);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[6].MediaItemId.Should().Be(5);
|
||||
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].GuideGroup.Should().Be(1);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
|
||||
{
|
||||
[Test]
|
||||
public void Should_Fill_Exactly_To_Next_Schedule_Item()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Fill_Exactly_To_Next_Schedule_Item_With_Post_Roll_Multiple_One()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
PostRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.PostRoll,
|
||||
FillerMode = FillerMode.Count,
|
||||
Count = 1,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
TailFiller = null,
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(2);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[2].GuideGroup.Should().Be(2);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(4);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 55, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(2);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(1);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_With_No_Tail_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Exact_Tail()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_With_Tail_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Tail_And_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
scheduleItem.TailFiller,
|
||||
enumerator2,
|
||||
scheduleItem.FallbackFiller,
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
enumerator3.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(7);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[6].MediaItemId.Should().Be(5);
|
||||
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].GuideGroup.Should().Be(3);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemFlood
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||
{
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
scheduleItem.TailFiller,
|
||||
enumerator2,
|
||||
scheduleItem.FallbackFiller,
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
enumerator3.State.Index.Should().Be(0);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
|
||||
{
|
||||
StartTime = TimeSpan.FromHours(3)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
|
||||
{
|
||||
[Test]
|
||||
public void Should_Fill_Exactly_To_Next_Schedule_Item()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
CollectionType = ProgramScheduleItemCollectionType.Collection,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_With_No_Tail_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Exact_Tail()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionTwo.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_With_Tail_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Tail_And_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
},
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
scheduleItem.TailFiller,
|
||||
enumerator2,
|
||||
scheduleItem.FallbackFiller,
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
enumerator3.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(7);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[6].MediaItemId.Should().Be(5);
|
||||
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].GuideGroup.Should().Be(1);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
},
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
scheduleItem.TailFiller,
|
||||
enumerator2,
|
||||
scheduleItem.FallbackFiller,
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
enumerator3.State.Index.Should().Be(0);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
|
||||
{
|
||||
StartTime = TimeSpan.FromHours(3)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class PlayoutModeSchedulerOneTests : SchedulerTestBase
|
||||
{
|
||||
[Test]
|
||||
public void Should_Have_Gap_With_No_Tail_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Exact_Tail()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(2);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_With_Tail_No_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Tail_And_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
scheduleItem.TailFiller,
|
||||
enumerator2,
|
||||
scheduleItem.FallbackFiller,
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
enumerator3.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(5);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(5);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(3));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4));
|
||||
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
Collection = collectionThree,
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
scheduleItem.TailFiller,
|
||||
enumerator2,
|
||||
scheduleItem.FallbackFiller,
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
enumerator3.State.Index.Should().Be(0);
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,10 @@
|
||||
public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size");
|
||||
public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size");
|
||||
public static ConfigElementKey PlayoutsDetailPageSize => new("pages.playouts.detail_page_size");
|
||||
public static ConfigElementKey PlayoutsDetailShowFiller => new("pages.playouts.detail_show_filler");
|
||||
public static ConfigElementKey LogsPageSize => new("pages.logs.page_size");
|
||||
public static ConfigElementKey TraktListsPageSize => new("pages.trakt.lists_page_size");
|
||||
public static ConfigElementKey FillerPresetsPageSize => new("pages.filler_presets.page_size");
|
||||
public static ConfigElementKey LibraryRefreshInterval => new("scanner.library_refresh_interval");
|
||||
public static ConfigElementKey PlayoutDaysToBuild => new("playout.days_to_build");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.Core.Domain.Filler
|
||||
{
|
||||
public enum FillerKind
|
||||
{
|
||||
None = 0,
|
||||
PreRoll = 1,
|
||||
MidRoll = 2,
|
||||
PostRoll = 3,
|
||||
Tail = 4,
|
||||
Fallback = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Core.Domain.Filler
|
||||
{
|
||||
public enum FillerMode
|
||||
{
|
||||
None = 0,
|
||||
Duration = 1,
|
||||
Count = 2,
|
||||
Pad = 3
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
[DebuggerDisplay("{MediaItemId} - {Start} - {Finish}")]
|
||||
public class PlayoutItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
@@ -11,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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -230,7 +230,13 @@ namespace ErsatzTV.Core.Emby
|
||||
foreach (EmbySeason updated in await _televisionRepository.Update(incoming))
|
||||
{
|
||||
incoming.Show = show;
|
||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
|
||||
|
||||
foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { toIndex });
|
||||
}
|
||||
}
|
||||
},
|
||||
async () =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,10 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
{
|
||||
public interface IPlexServerApiClient
|
||||
{
|
||||
Task<bool> Ping(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
|
||||
Task Update(
|
||||
PlexMediaSource plexMediaSource,
|
||||
List<PlexConnection> prioritizedConnections,
|
||||
List<PlexConnection> toAdd,
|
||||
List<PlexConnection> toDelete);
|
||||
|
||||
|
||||
@@ -8,5 +8,6 @@ namespace ErsatzTV.Core.Interfaces.Scheduling
|
||||
CollectionEnumeratorState State { get; }
|
||||
Option<MediaItem> Current { get; }
|
||||
void MoveNext();
|
||||
Option<MediaItem> Peek(int offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,40 @@ namespace ErsatzTV.Core.Iptv
|
||||
}
|
||||
}
|
||||
|
||||
if (startItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow))
|
||||
if (!hasCustomTitle && displayItem.MediaItem is MusicVideo musicVideo)
|
||||
{
|
||||
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
|
||||
{
|
||||
if (metadata.Year.HasValue)
|
||||
{
|
||||
xml.WriteStartElement("date");
|
||||
xml.WriteString(metadata.Year.Value.ToString());
|
||||
xml.WriteEndElement(); // date
|
||||
}
|
||||
}
|
||||
|
||||
xml.WriteStartElement("category");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString("Music");
|
||||
xml.WriteEndElement(); // category
|
||||
|
||||
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
|
||||
{
|
||||
string thumbnail = Optional(metadata.Artwork).Flatten()
|
||||
.Filter(a => a.ArtworkKind == ArtworkKind.Thumbnail)
|
||||
.HeadOrNone()
|
||||
.Match(a => GetArtworkUrl(a, ArtworkKind.Thumbnail), () => string.Empty);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(thumbnail))
|
||||
{
|
||||
xml.WriteStartElement("icon");
|
||||
xml.WriteAttributeString("src", thumbnail);
|
||||
xml.WriteEndElement(); // icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (displayItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow))
|
||||
{
|
||||
Option<ShowMetadata> maybeMetadata =
|
||||
Optional(episode.Season?.Show?.ShowMetadata.HeadOrNone()).Flatten();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -230,7 +230,13 @@ namespace ErsatzTV.Core.Jellyfin
|
||||
foreach (JellyfinSeason updated in await _televisionRepository.Update(incoming))
|
||||
{
|
||||
incoming.Show = show;
|
||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
|
||||
|
||||
foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { toIndex });
|
||||
}
|
||||
}
|
||||
},
|
||||
async () =>
|
||||
|
||||
@@ -110,6 +110,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
startInfo.ArgumentList.Add("json");
|
||||
startInfo.ArgumentList.Add("-show_format");
|
||||
startInfo.ArgumentList.Add("-show_streams");
|
||||
startInfo.ArgumentList.Add("-show_chapters");
|
||||
startInfo.ArgumentList.Add("-i");
|
||||
startInfo.ArgumentList.Add(filePath);
|
||||
|
||||
@@ -138,9 +139,18 @@ namespace ErsatzTV.Core.Metadata
|
||||
json =>
|
||||
{
|
||||
var version = new MediaVersion
|
||||
{ Name = "Main", DateAdded = DateTime.UtcNow, Streams = new List<MediaStream>() };
|
||||
{
|
||||
Name = "Main",
|
||||
DateAdded = DateTime.UtcNow,
|
||||
Streams = new List<MediaStream>(),
|
||||
Chapters = new List<MediaChapter>()
|
||||
};
|
||||
|
||||
if (double.TryParse(json.format.duration, NumberStyles.Number, CultureInfo.InvariantCulture, out double duration))
|
||||
if (double.TryParse(
|
||||
json.format.duration,
|
||||
NumberStyles.Number,
|
||||
CultureInfo.InvariantCulture,
|
||||
out double duration))
|
||||
{
|
||||
var seconds = TimeSpan.FromSeconds(duration);
|
||||
version.Duration = seconds;
|
||||
@@ -240,10 +250,56 @@ namespace ErsatzTV.Core.Metadata
|
||||
version.Streams.Add(stream);
|
||||
}
|
||||
|
||||
foreach (FFprobeChapter probedChapter in json.chapters)
|
||||
{
|
||||
if (double.TryParse(
|
||||
probedChapter.start_time,
|
||||
NumberStyles.Number,
|
||||
CultureInfo.InvariantCulture,
|
||||
out double startTime)
|
||||
&& double.TryParse(
|
||||
probedChapter.end_time,
|
||||
NumberStyles.Number,
|
||||
CultureInfo.InvariantCulture,
|
||||
out double endTime))
|
||||
{
|
||||
var chapter = new MediaChapter
|
||||
{
|
||||
MediaVersionId = version.Id,
|
||||
ChapterId = probedChapter.id,
|
||||
StartTime = TimeSpan.FromSeconds(startTime),
|
||||
EndTime = TimeSpan.FromSeconds(endTime),
|
||||
Title = probedChapter?.tags?.title
|
||||
};
|
||||
|
||||
version.Chapters.Add(chapter);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Media item at {Path} has a missing or invalid chapter start/end time",
|
||||
path);
|
||||
}
|
||||
}
|
||||
|
||||
if (version.Chapters.Any())
|
||||
{
|
||||
MediaChapter last = version.Chapters.Last();
|
||||
if (last.EndTime != version.Duration)
|
||||
{
|
||||
last.EndTime = version.Duration;
|
||||
}
|
||||
}
|
||||
|
||||
return version;
|
||||
},
|
||||
_ => new MediaVersion
|
||||
{ Name = "Main", DateAdded = DateTime.UtcNow, Streams = new List<MediaStream>() });
|
||||
{
|
||||
Name = "Main",
|
||||
DateAdded = DateTime.UtcNow,
|
||||
Streams = new List<MediaStream>(),
|
||||
Chapters = new List<MediaChapter>()
|
||||
});
|
||||
|
||||
private VideoScanKind ScanKindFromFieldOrder(string fieldOrder) =>
|
||||
fieldOrder?.ToLowerInvariant() switch
|
||||
@@ -254,13 +310,13 @@ namespace ErsatzTV.Core.Metadata
|
||||
};
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
public record FFprobe(FFprobeFormat format, List<FFprobeStream> streams);
|
||||
public record FFprobe(FFprobeFormat format, List<FFprobeStream> streams, List<FFprobeChapter> chapters);
|
||||
|
||||
public record FFprobeFormat(string duration);
|
||||
|
||||
public record FFprobeDisposition(int @default, int forced);
|
||||
|
||||
public record FFProbeTags(string language, string title);
|
||||
public record FFprobeTags(string language, string title);
|
||||
|
||||
public record FFprobeStream(
|
||||
int index,
|
||||
@@ -277,7 +333,13 @@ namespace ErsatzTV.Core.Metadata
|
||||
string r_frame_rate,
|
||||
string bits_per_raw_sample,
|
||||
FFprobeDisposition disposition,
|
||||
FFProbeTags tags);
|
||||
FFprobeTags tags);
|
||||
|
||||
public record FFprobeChapter(
|
||||
long id,
|
||||
string start_time,
|
||||
string end_time,
|
||||
FFprobeTags tags);
|
||||
// ReSharper restore InconsistentNaming
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,6 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
foreach (string file in allFiles.OrderBy(identity))
|
||||
{
|
||||
_logger.LogDebug("Other video found at {File}", file);
|
||||
|
||||
Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository
|
||||
.GetOrAdd(libraryPath, file)
|
||||
.BindT(video => UpdateStatistics(video, ffprobePath))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
@@ -36,5 +37,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||
|
||||
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public class CollectionKey : Record<CollectionKey>
|
||||
{
|
||||
public static CollectionKey ForScheduleItem(ProgramScheduleItem item) =>
|
||||
item.CollectionType switch
|
||||
{
|
||||
ProgramScheduleItemCollectionType.Collection => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MediaItemId = item.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MediaItemId = item.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.Artist => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MediaItemId = item.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MultiCollectionId = item.MultiCollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
SmartCollectionId = item.SmartCollectionId
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(item))
|
||||
};
|
||||
|
||||
public static CollectionKey ForFillerPreset(FillerPreset filler) =>
|
||||
filler.CollectionType switch
|
||||
{
|
||||
ProgramScheduleItemCollectionType.Collection => new CollectionKey
|
||||
{
|
||||
CollectionType = filler.CollectionType,
|
||||
CollectionId = filler.CollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
|
||||
{
|
||||
CollectionType = filler.CollectionType,
|
||||
MediaItemId = filler.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
|
||||
{
|
||||
CollectionType = filler.CollectionType,
|
||||
MediaItemId = filler.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.Artist => new CollectionKey
|
||||
{
|
||||
CollectionType = filler.CollectionType,
|
||||
MediaItemId = filler.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = filler.CollectionType,
|
||||
MultiCollectionId = filler.MultiCollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = filler.CollectionType,
|
||||
SmartCollectionId = filler.SmartCollectionId
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(filler))
|
||||
};
|
||||
|
||||
public ProgramScheduleItemCollectionType CollectionType { get; set; }
|
||||
public int? CollectionId { get; set; }
|
||||
public int? MultiCollectionId { get; set; }
|
||||
public int? SmartCollectionId { get; set; }
|
||||
public int? MediaItemId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
@@ -9,13 +10,16 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly Collection _collection;
|
||||
private readonly IList<MediaItem> _sortedMediaItems;
|
||||
|
||||
public CustomOrderCollectionEnumerator(
|
||||
Collection collection,
|
||||
List<MediaItem> mediaItems,
|
||||
IList<MediaItem> mediaItems,
|
||||
CollectionEnumeratorState state)
|
||||
{
|
||||
_collection = collection;
|
||||
|
||||
// TODO: this will break if we allow shows and seasons
|
||||
_sortedMediaItems = collection.CollectionItems
|
||||
.OrderBy(ci => ci.CustomIndex)
|
||||
@@ -34,5 +38,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
public Option<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
|
||||
|
||||
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,20 +51,191 @@ namespace ErsatzTV.Core.Scheduling
|
||||
DateTimeOffset playoutFinish,
|
||||
bool rebuild = false)
|
||||
{
|
||||
var collectionKeys = playout.ProgramSchedule.Items
|
||||
.SelectMany(CollectionKeysForItem)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (!collectionKeys.Any())
|
||||
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(playout);
|
||||
if (!collectionMediaItems.Any())
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Playout {Playout} schedule {Schedule} has no items",
|
||||
playout.Channel.Name,
|
||||
playout.ProgramSchedule.Name);
|
||||
|
||||
return playout;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"{Action} playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
|
||||
rebuild ? "Rebuilding" : "Building",
|
||||
playout.Id,
|
||||
playout.Channel.Number,
|
||||
playout.Channel.Name);
|
||||
|
||||
Option<CollectionKey> maybeEmptyCollection = await CheckForEmptyCollections(collectionMediaItems);
|
||||
foreach (CollectionKey emptyCollection in maybeEmptyCollection)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to rebuild playout; collection {@CollectionKey} has no valid items!",
|
||||
emptyCollection);
|
||||
|
||||
return playout;
|
||||
}
|
||||
|
||||
playout.Items ??= new List<PlayoutItem>();
|
||||
playout.ProgramScheduleAnchors ??= new List<PlayoutProgramScheduleAnchor>();
|
||||
|
||||
if (rebuild)
|
||||
{
|
||||
playout.Items.Clear();
|
||||
playout.Anchor = null;
|
||||
playout.ProgramScheduleAnchors.Clear();
|
||||
}
|
||||
|
||||
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
|
||||
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
|
||||
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
|
||||
{
|
||||
// use configured playback order for primary collection, shuffle for filler
|
||||
Option<ProgramScheduleItem> maybeScheduleItem = sortedScheduleItems
|
||||
.FirstOrDefault(item => CollectionKey.ForScheduleItem(item) == collectionKey);
|
||||
PlaybackOrder playbackOrder = maybeScheduleItem
|
||||
.Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle);
|
||||
IMediaCollectionEnumerator enumerator =
|
||||
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems, playbackOrder);
|
||||
collectionEnumerators.Add(collectionKey, enumerator);
|
||||
}
|
||||
|
||||
// find start anchor
|
||||
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems);
|
||||
|
||||
// start at the previously-decided time
|
||||
DateTimeOffset currentTime = startAnchor.NextStartOffset.ToLocalTime();
|
||||
_logger.LogDebug(
|
||||
"Starting playout {PlayoutId} for channel {ChannelNumber} - {ChannelName} at {StartTime}",
|
||||
playout.Id,
|
||||
playout.Channel.Number,
|
||||
playout.Channel.Name,
|
||||
currentTime);
|
||||
|
||||
// removing any items scheduled past the start anchor
|
||||
// this could happen if the app was closed after scheduling items
|
||||
// but before saving the anchor
|
||||
int removed = playout.Items.RemoveAll(pi => pi.StartOffset >= currentTime);
|
||||
if (removed > 0)
|
||||
{
|
||||
_logger.LogWarning("Removed {Count} schedule items beyond current start anchor", removed);
|
||||
}
|
||||
|
||||
// start with the previously-decided schedule item
|
||||
// start with the previous multiple/duration states
|
||||
var playoutBuilderState = new PlayoutBuilderState(
|
||||
sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem),
|
||||
Optional(startAnchor.MultipleRemaining),
|
||||
startAnchor.DurationFinishOffset,
|
||||
startAnchor.InFlood,
|
||||
startAnchor.InDurationFiller,
|
||||
startAnchor.NextGuideGroup,
|
||||
currentTime);
|
||||
|
||||
var schedulerOne = new PlayoutModeSchedulerOne(_logger);
|
||||
var schedulerMultiple = new PlayoutModeSchedulerMultiple(collectionMediaItems, _logger);
|
||||
var schedulerDuration = new PlayoutModeSchedulerDuration(_logger);
|
||||
var schedulerFlood = new PlayoutModeSchedulerFlood(sortedScheduleItems, _logger);
|
||||
|
||||
// loop until we're done filling the desired amount of time
|
||||
while (playoutBuilderState.CurrentTime < playoutFinish)
|
||||
{
|
||||
// get the schedule item out of the sorted list
|
||||
ProgramScheduleItem scheduleItem =
|
||||
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
|
||||
|
||||
ProgramScheduleItem nextScheduleItem =
|
||||
sortedScheduleItems[(playoutBuilderState.ScheduleItemIndex + 1) % sortedScheduleItems.Count];
|
||||
|
||||
Tuple<PlayoutBuilderState, List<PlayoutItem>> result = scheduleItem switch
|
||||
{
|
||||
ProgramScheduleItemMultiple multiple => schedulerMultiple.Schedule(
|
||||
playoutBuilderState,
|
||||
collectionEnumerators,
|
||||
multiple,
|
||||
nextScheduleItem,
|
||||
playoutFinish),
|
||||
ProgramScheduleItemDuration duration => schedulerDuration.Schedule(
|
||||
playoutBuilderState,
|
||||
collectionEnumerators,
|
||||
duration,
|
||||
nextScheduleItem,
|
||||
playoutFinish),
|
||||
ProgramScheduleItemFlood flood => schedulerFlood.Schedule(
|
||||
playoutBuilderState,
|
||||
collectionEnumerators,
|
||||
flood,
|
||||
nextScheduleItem,
|
||||
playoutFinish),
|
||||
ProgramScheduleItemOne one => schedulerOne.Schedule(
|
||||
playoutBuilderState,
|
||||
collectionEnumerators,
|
||||
one,
|
||||
nextScheduleItem,
|
||||
playoutFinish),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(scheduleItem))
|
||||
};
|
||||
|
||||
(PlayoutBuilderState nextState, List<PlayoutItem> playoutItems) = result;
|
||||
|
||||
foreach (PlayoutItem playoutItem in playoutItems)
|
||||
{
|
||||
playout.Items.Add(playoutItem);
|
||||
}
|
||||
|
||||
playoutBuilderState = nextState;
|
||||
}
|
||||
|
||||
// once more to get playout anchor
|
||||
ProgramScheduleItem anchorScheduleItem =
|
||||
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
|
||||
|
||||
// build program schedule anchors
|
||||
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
|
||||
|
||||
// remove any items outside the desired range
|
||||
playout.Items.RemoveAll(
|
||||
old => old.FinishOffset < playoutStart.AddHours(-4) || old.StartOffset > playoutFinish);
|
||||
|
||||
if (playout.Items.Any())
|
||||
{
|
||||
DateTimeOffset maxStartTime = playout.Items.Max(i => i.FinishOffset);
|
||||
if (maxStartTime < playoutBuilderState.CurrentTime)
|
||||
{
|
||||
playoutBuilderState = playoutBuilderState with { CurrentTime = maxStartTime };
|
||||
}
|
||||
}
|
||||
|
||||
playout.Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextScheduleItem = anchorScheduleItem,
|
||||
NextScheduleItemId = anchorScheduleItem.Id,
|
||||
NextStart = PlayoutModeSchedulerBase<ProgramScheduleItem>.GetStartTimeAfter(playoutBuilderState, anchorScheduleItem)
|
||||
.UtcDateTime,
|
||||
MultipleRemaining = playoutBuilderState.MultipleRemaining.IsSome
|
||||
? playoutBuilderState.MultipleRemaining.ValueUnsafe()
|
||||
: null,
|
||||
DurationFinish = playoutBuilderState.DurationFinish.IsSome
|
||||
? playoutBuilderState.DurationFinish.ValueUnsafe().UtcDateTime
|
||||
: null,
|
||||
InFlood = playoutBuilderState.InFlood,
|
||||
InDurationFiller = playoutBuilderState.InDurationFiller,
|
||||
NextGuideGroup = playoutBuilderState.NextGuideGroup
|
||||
};
|
||||
|
||||
return playout;
|
||||
}
|
||||
|
||||
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(Playout playout)
|
||||
{
|
||||
var collectionKeys = playout.ProgramSchedule.Items
|
||||
.SelectMany(CollectionKeysForItem)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
IEnumerable<Tuple<CollectionKey, List<MediaItem>>> tuples = await collectionKeys.Map(
|
||||
async collectionKey =>
|
||||
{
|
||||
@@ -101,15 +272,12 @@ namespace ErsatzTV.Core.Scheduling
|
||||
}
|
||||
}).Sequence();
|
||||
|
||||
var collectionMediaItems = Map.createRange(tuples);
|
||||
|
||||
_logger.LogDebug(
|
||||
"{Action} playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
|
||||
rebuild ? "Rebuilding" : "Building",
|
||||
playout.Id,
|
||||
playout.Channel.Number,
|
||||
playout.Channel.Name);
|
||||
return Map.createRange(tuples);
|
||||
}
|
||||
|
||||
private async Task<Option<CollectionKey>> CheckForEmptyCollections(
|
||||
Map<CollectionKey, List<MediaItem>> collectionMediaItems)
|
||||
{
|
||||
foreach ((CollectionKey _, List<MediaItem> items) in collectionMediaItems)
|
||||
{
|
||||
var zeroItems = new List<MediaItem>();
|
||||
@@ -143,400 +311,9 @@ namespace ErsatzTV.Core.Scheduling
|
||||
items.RemoveAll(i => zeroItems.Contains(i));
|
||||
}
|
||||
|
||||
// this guard needs to be below the place where we modify the collections (by removing zero-duration items)
|
||||
Option<CollectionKey> emptyCollection =
|
||||
collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
|
||||
if (emptyCollection.IsSome)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to rebuild playout; collection {@CollectionKey} has no valid items!",
|
||||
emptyCollection.ValueUnsafe());
|
||||
|
||||
return playout;
|
||||
}
|
||||
|
||||
// leaving this guard in for a while to ensure the zero item removal is working properly
|
||||
Option<CollectionKey> zeroDurationCollection = collectionMediaItems.Find(
|
||||
c => c.Value.Any(
|
||||
mi => mi switch
|
||||
{
|
||||
Movie m => m.MediaVersions.HeadOrNone().Map(mv => mv.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
Episode e => e.MediaVersions.HeadOrNone().Map(mv => mv.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
OtherVideo ov => ov.MediaVersions.HeadOrNone().Map(v => v.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
_ => true
|
||||
})).Map(c => c.Key);
|
||||
if (zeroDurationCollection.IsSome)
|
||||
{
|
||||
_logger.LogError(
|
||||
"BUG: Unable to rebuild playout; collection {@CollectionKey} contains items with zero duration!",
|
||||
zeroDurationCollection.ValueUnsafe());
|
||||
|
||||
return playout;
|
||||
}
|
||||
|
||||
playout.Items ??= new List<PlayoutItem>();
|
||||
playout.ProgramScheduleAnchors ??= new List<PlayoutProgramScheduleAnchor>();
|
||||
|
||||
if (rebuild)
|
||||
{
|
||||
playout.Items.Clear();
|
||||
playout.Anchor = null;
|
||||
playout.ProgramScheduleAnchors.Clear();
|
||||
}
|
||||
|
||||
var sortedScheduleItems =
|
||||
playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
|
||||
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
|
||||
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
|
||||
{
|
||||
// use configured playback order for primary collection, shuffle for filler
|
||||
Option<ProgramScheduleItem> maybeScheduleItem = sortedScheduleItems
|
||||
.FirstOrDefault(item => CollectionKeyForItem(item) == collectionKey);
|
||||
PlaybackOrder playbackOrder = maybeScheduleItem
|
||||
.Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle);
|
||||
IMediaCollectionEnumerator enumerator =
|
||||
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems, playbackOrder);
|
||||
collectionEnumerators.Add(collectionKey, enumerator);
|
||||
}
|
||||
|
||||
// find start anchor
|
||||
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems);
|
||||
|
||||
// start at the previously-decided time
|
||||
DateTimeOffset currentTime = startAnchor.NextStartOffset.ToLocalTime();
|
||||
_logger.LogDebug(
|
||||
"Starting playout {PlayoutId} for channel {ChannelNumber} - {ChannelName} at {StartTime}",
|
||||
playout.Id,
|
||||
playout.Channel.Number,
|
||||
playout.Channel.Name,
|
||||
currentTime);
|
||||
|
||||
// removing any items scheduled past the start anchor
|
||||
// this could happen if the app was closed after scheduling items
|
||||
// but before saving the anchor
|
||||
int removed = playout.Items.RemoveAll(pi => pi.StartOffset >= currentTime);
|
||||
if (removed > 0)
|
||||
{
|
||||
_logger.LogWarning("Removed {Count} schedule items beyond current start anchor", removed);
|
||||
}
|
||||
|
||||
// start with the previously-decided schedule item
|
||||
int index = sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem);
|
||||
|
||||
// start with the previous multiple/duration states
|
||||
Option<int> multipleRemaining = Optional(startAnchor.MultipleRemaining);
|
||||
Option<DateTimeOffset> durationFinish = startAnchor.DurationFinishOffset;
|
||||
bool inFlood = startAnchor.InFlood;
|
||||
bool inDurationFiller = startAnchor.InDurationFiller;
|
||||
|
||||
bool customGroup = multipleRemaining.IsSome || durationFinish.IsSome;
|
||||
|
||||
// loop until we're done filling the desired amount of time
|
||||
while (currentTime < playoutFinish)
|
||||
{
|
||||
// get the schedule item out of the sorted list
|
||||
ProgramScheduleItem scheduleItem = sortedScheduleItems[index % sortedScheduleItems.Count];
|
||||
|
||||
// find when we should start this item, based on the current time
|
||||
DateTimeOffset itemStartTime = GetStartTimeAfter(
|
||||
scheduleItem,
|
||||
currentTime,
|
||||
multipleRemaining.IsSome,
|
||||
durationFinish.IsSome,
|
||||
inFlood,
|
||||
inDurationFiller);
|
||||
|
||||
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
|
||||
if (inDurationFiller && scheduleItem is ProgramScheduleItemDuration
|
||||
{
|
||||
TailMode: TailMode.Filler
|
||||
})
|
||||
{
|
||||
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
|
||||
}
|
||||
|
||||
IMediaCollectionEnumerator enumerator = collectionEnumerators[CollectionKeyForItem(scheduleItem)];
|
||||
foreach (CollectionKey tailCollectionKey in maybeTailCollectionKey)
|
||||
{
|
||||
enumerator = collectionEnumerators[tailCollectionKey];
|
||||
}
|
||||
|
||||
await enumerator.Current.IfSomeAsync(
|
||||
mediaItem =>
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Scheduling media item: {ScheduleItemNumber} / {CollectionType} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
|
||||
scheduleItem.Index,
|
||||
inDurationFiller
|
||||
? (scheduleItem as ProgramScheduleItemDuration)?.TailCollectionType
|
||||
: scheduleItem.CollectionType,
|
||||
mediaItem.Id,
|
||||
DisplayTitle(mediaItem),
|
||||
itemStartTime);
|
||||
|
||||
MediaVersion version = mediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + version.Duration,
|
||||
CustomGroup = customGroup,
|
||||
IsFiller = inDurationFiller || scheduleItem.GuideMode == GuideMode.Filler
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(scheduleItem.CustomTitle))
|
||||
{
|
||||
playoutItem.CustomTitle = scheduleItem.CustomTitle;
|
||||
}
|
||||
|
||||
currentTime = itemStartTime + version.Duration;
|
||||
enumerator.MoveNext();
|
||||
|
||||
playout.Items.Add(playoutItem);
|
||||
|
||||
switch (scheduleItem)
|
||||
{
|
||||
case ProgramScheduleItemOne:
|
||||
// only play one item from collection, so always advance to the next item
|
||||
_logger.LogDebug(
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"One");
|
||||
index++;
|
||||
customGroup = false;
|
||||
break;
|
||||
case ProgramScheduleItemMultiple multiple:
|
||||
if (multipleRemaining.IsNone)
|
||||
{
|
||||
if (multiple.Count == 0)
|
||||
{
|
||||
multipleRemaining = collectionMediaItems[CollectionKeyForItem(scheduleItem)]
|
||||
.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
multipleRemaining = multiple.Count;
|
||||
}
|
||||
|
||||
customGroup = true;
|
||||
}
|
||||
|
||||
multipleRemaining = multipleRemaining.Map(i => i - 1);
|
||||
if (multipleRemaining.IfNone(-1) == 0)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"Multiple");
|
||||
index++;
|
||||
multipleRemaining = None;
|
||||
customGroup = false;
|
||||
}
|
||||
|
||||
break;
|
||||
case ProgramScheduleItemFlood:
|
||||
enumerator.Current.Do(
|
||||
peekMediaItem =>
|
||||
{
|
||||
customGroup = true;
|
||||
|
||||
MediaVersion peekVersion = peekMediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
ProgramScheduleItem peekScheduleItem =
|
||||
sortedScheduleItems[(index + 1) % sortedScheduleItems.Count];
|
||||
DateTimeOffset peekScheduleItemStart =
|
||||
peekScheduleItem.StartType == StartType.Fixed
|
||||
? GetStartTimeAfter(peekScheduleItem, currentTime)
|
||||
: DateTimeOffset.MaxValue;
|
||||
|
||||
// if the current time is before the next schedule item, but the current finish
|
||||
// is after, we need to move on to the next schedule item
|
||||
// eventually, spots probably have to fit in this gap
|
||||
bool willNotFinishInTime = currentTime <= peekScheduleItemStart &&
|
||||
currentTime + peekVersion.Duration >
|
||||
peekScheduleItemStart;
|
||||
if (willNotFinishInTime)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"Flood");
|
||||
index++;
|
||||
customGroup = false;
|
||||
inFlood = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
inFlood = true;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ProgramScheduleItemDuration duration:
|
||||
enumerator.Current.Do(
|
||||
peekMediaItem =>
|
||||
{
|
||||
MediaVersion peekVersion = peekMediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
// remember when we need to finish this duration item
|
||||
if (durationFinish.IsNone)
|
||||
{
|
||||
durationFinish = itemStartTime + duration.PlayoutDuration;
|
||||
customGroup = true;
|
||||
}
|
||||
|
||||
bool willNotFinishInTime =
|
||||
currentTime <= durationFinish.IfNone(SystemTime.MinValueUtc) &&
|
||||
currentTime + peekVersion.Duration >
|
||||
durationFinish.IfNone(SystemTime.MinValueUtc);
|
||||
if (willNotFinishInTime)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"Duration");
|
||||
index++;
|
||||
|
||||
if (duration.TailMode == TailMode.Offline)
|
||||
{
|
||||
durationFinish.Do(f => currentTime = f);
|
||||
}
|
||||
|
||||
if (duration.TailMode != TailMode.Filler || inDurationFiller)
|
||||
{
|
||||
if (duration.TailMode != TailMode.None)
|
||||
{
|
||||
durationFinish.Do(f => currentTime = f);
|
||||
}
|
||||
|
||||
durationFinish = None;
|
||||
inDurationFiller = false;
|
||||
customGroup = false;
|
||||
}
|
||||
else if (duration.TailMode == TailMode.Filler &&
|
||||
WillFinishFillerInTime(
|
||||
scheduleItem,
|
||||
currentTime,
|
||||
durationFinish,
|
||||
collectionEnumerators))
|
||||
{
|
||||
inDurationFiller = true;
|
||||
durationFinish.Do(
|
||||
f => playoutItem.GuideFinish = f.UtcDateTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// once more to get playout anchor
|
||||
ProgramScheduleItem nextScheduleItem = sortedScheduleItems[index % sortedScheduleItems.Count];
|
||||
|
||||
// build program schedule anchors
|
||||
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
|
||||
|
||||
// remove any items outside the desired range
|
||||
playout.Items.RemoveAll(
|
||||
old => old.FinishOffset < playoutStart.AddHours(-4) || old.StartOffset > playoutFinish);
|
||||
|
||||
DateTimeOffset minCurrentTime = currentTime;
|
||||
if (playout.Items.Any())
|
||||
{
|
||||
DateTimeOffset maxStartTime = playout.Items.Max(i => i.FinishOffset);
|
||||
if (maxStartTime < currentTime)
|
||||
{
|
||||
minCurrentTime = maxStartTime;
|
||||
}
|
||||
}
|
||||
|
||||
playout.Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextScheduleItem = nextScheduleItem,
|
||||
NextScheduleItemId = nextScheduleItem.Id,
|
||||
NextStart = GetStartTimeAfter(nextScheduleItem, minCurrentTime).UtcDateTime,
|
||||
MultipleRemaining = multipleRemaining.IsSome ? multipleRemaining.ValueUnsafe() : null,
|
||||
DurationFinish = durationFinish.IsSome ? durationFinish.ValueUnsafe().UtcDateTime : null,
|
||||
InFlood = inFlood,
|
||||
InDurationFiller = inDurationFiller
|
||||
};
|
||||
|
||||
return playout;
|
||||
return collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
|
||||
}
|
||||
|
||||
private static bool WillFinishFillerInTime(
|
||||
ProgramScheduleItem scheduleItem,
|
||||
DateTimeOffset currentTime,
|
||||
Option<DateTimeOffset> durationFinish,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
|
||||
{
|
||||
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
|
||||
if (scheduleItem is ProgramScheduleItemDuration
|
||||
{
|
||||
TailMode: TailMode.Filler
|
||||
})
|
||||
{
|
||||
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
|
||||
}
|
||||
|
||||
foreach (CollectionKey collectionKey in maybeTailCollectionKey)
|
||||
{
|
||||
IMediaCollectionEnumerator enumerator = collectionEnumerators[collectionKey];
|
||||
Option<int> firstId = enumerator.Current.Map(i => i.Id);
|
||||
while (true)
|
||||
{
|
||||
foreach (MediaItem peekMediaItem in enumerator.Current)
|
||||
{
|
||||
MediaVersion peekVersion = peekMediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
if (currentTime + peekVersion.Duration <= durationFinish.IfNone(SystemTime.MinValueUtc))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
enumerator.MoveNext();
|
||||
if (enumerator.Current.Map(i => i.Id) == firstId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static PlayoutAnchor FindStartAnchor(
|
||||
Playout playout,
|
||||
DateTimeOffset start,
|
||||
@@ -566,35 +343,6 @@ namespace ErsatzTV.Core.Scheduling
|
||||
}
|
||||
});
|
||||
|
||||
private static DateTimeOffset GetStartTimeAfter(
|
||||
ProgramScheduleItem item,
|
||||
DateTimeOffset start,
|
||||
bool inMultiple = false,
|
||||
bool inDuration = false,
|
||||
bool inFlood = false,
|
||||
bool inDurationFiller = false)
|
||||
{
|
||||
switch (item.StartType)
|
||||
{
|
||||
case StartType.Fixed:
|
||||
if (item is ProgramScheduleItemMultiple && inMultiple ||
|
||||
item is ProgramScheduleItemDuration && inDuration ||
|
||||
item is ProgramScheduleItemFlood && inFlood ||
|
||||
item is ProgramScheduleItemDuration && inDurationFiller)
|
||||
{
|
||||
return start;
|
||||
}
|
||||
|
||||
TimeSpan startTime = item.StartTime.GetValueOrDefault();
|
||||
DateTimeOffset result = start.Date + startTime;
|
||||
// need to wrap to the next day if appropriate
|
||||
return start.TimeOfDay > startTime ? result.AddDays(1) : result;
|
||||
case StartType.Dynamic:
|
||||
default:
|
||||
return start;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
|
||||
Playout playout,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
|
||||
@@ -731,7 +479,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string DisplayTitle(MediaItem mediaItem)
|
||||
internal static string DisplayTitle(MediaItem mediaItem)
|
||||
{
|
||||
switch (mediaItem)
|
||||
{
|
||||
@@ -760,97 +508,37 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
private static List<CollectionKey> CollectionKeysForItem(ProgramScheduleItem item)
|
||||
{
|
||||
var result = new List<CollectionKey> { CollectionKeyForItem(item) };
|
||||
result.AddRange(TailCollectionKeyForItem(item));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CollectionKey CollectionKeyForItem(ProgramScheduleItem item) =>
|
||||
item.CollectionType switch
|
||||
var result = new List<CollectionKey>
|
||||
{
|
||||
ProgramScheduleItemCollectionType.Collection => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MediaItemId = item.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MediaItemId = item.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.Artist => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MediaItemId = item.MediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
MultiCollectionId = item.MultiCollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = item.CollectionType,
|
||||
SmartCollectionId = item.SmartCollectionId
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(item))
|
||||
CollectionKey.ForScheduleItem(item)
|
||||
};
|
||||
|
||||
private static Option<CollectionKey> TailCollectionKeyForItem(ProgramScheduleItem item)
|
||||
{
|
||||
if (item is ProgramScheduleItemDuration { TailMode: TailMode.Filler } duration)
|
||||
if (item.PreRollFiller != null)
|
||||
{
|
||||
return duration.TailCollectionType switch
|
||||
{
|
||||
ProgramScheduleItemCollectionType.Collection => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
CollectionId = duration.TailCollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MediaItemId = duration.TailMediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MediaItemId = duration.TailMediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.Artist => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MediaItemId = duration.TailMediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MultiCollectionId = duration.TailMultiCollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
SmartCollectionId = duration.TailSmartCollectionId
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(item))
|
||||
};
|
||||
result.Add(CollectionKey.ForFillerPreset(item.PreRollFiller));
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
if (item.MidRollFiller != null)
|
||||
{
|
||||
result.Add(CollectionKey.ForFillerPreset(item.MidRollFiller));
|
||||
}
|
||||
|
||||
private class CollectionKey : Record<CollectionKey>
|
||||
{
|
||||
public ProgramScheduleItemCollectionType CollectionType { get; set; }
|
||||
public int? CollectionId { get; set; }
|
||||
public int? MultiCollectionId { get; set; }
|
||||
public int? SmartCollectionId { get; set; }
|
||||
public int? MediaItemId { get; set; }
|
||||
if (item.PostRollFiller != null)
|
||||
{
|
||||
result.Add(CollectionKey.ForFillerPreset(item.PostRollFiller));
|
||||
}
|
||||
|
||||
if (item.TailFiller != null)
|
||||
{
|
||||
result.Add(CollectionKey.ForFillerPreset(item.TailFiller));
|
||||
}
|
||||
|
||||
if (item.FallbackFiller != null)
|
||||
{
|
||||
result.Add(CollectionKey.ForFillerPreset(item.FallbackFiller));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public record PlayoutBuilderState(
|
||||
int ScheduleItemIndex,
|
||||
Option<int> MultipleRemaining,
|
||||
Option<DateTimeOffset> DurationFinish,
|
||||
bool InFlood,
|
||||
bool InDurationFiller,
|
||||
int NextGuideGroup,
|
||||
DateTimeOffset CurrentTime)
|
||||
{
|
||||
public int IncrementGuideGroup => (NextGuideGroup + 1) % 10000;
|
||||
public int DecrementGuideGroup => (NextGuideGroup - 1) % 10000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public record PlayoutModeBlock(DateTimeOffset StartTime, DateTimeOffset FinishTime);
|
||||
}
|
||||
@@ -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.Interfaces.Scheduling;
|
||||
using LanguageExt.UnsafeValueAccess;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> where T : ProgramScheduleItem
|
||||
{
|
||||
protected readonly ILogger _logger;
|
||||
|
||||
protected PlayoutModeSchedulerBase(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static DateTimeOffset GetStartTimeAfter(
|
||||
PlayoutBuilderState state,
|
||||
ProgramScheduleItem scheduleItem)
|
||||
{
|
||||
DateTimeOffset startTime = state.CurrentTime;
|
||||
|
||||
bool isIncomplete = scheduleItem is ProgramScheduleItemMultiple && state.MultipleRemaining.IsSome ||
|
||||
scheduleItem is ProgramScheduleItemDuration && state.DurationFinish.IsSome ||
|
||||
scheduleItem is ProgramScheduleItemFlood && state.InFlood ||
|
||||
scheduleItem is ProgramScheduleItemDuration && state.InDurationFiller;
|
||||
|
||||
if (scheduleItem.StartType == StartType.Fixed && !isIncomplete)
|
||||
{
|
||||
TimeSpan itemStartTime = scheduleItem.StartTime.GetValueOrDefault();
|
||||
DateTime date = startTime.Date;
|
||||
DateTimeOffset result = new DateTimeOffset(
|
||||
date.Year,
|
||||
date.Month,
|
||||
date.Day,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
TimeZoneInfo.Local.GetUtcOffset(
|
||||
new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, DateTimeKind.Local)))
|
||||
.Add(itemStartTime);
|
||||
|
||||
// DateTimeOffset result = startTime.Date + itemStartTime;
|
||||
// need to wrap to the next day if appropriate
|
||||
startTime = startTime.TimeOfDay > itemStartTime ? result.AddDays(1) : result;
|
||||
}
|
||||
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public abstract Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
T scheduleItem,
|
||||
ProgramScheduleItem nextScheduleItem,
|
||||
DateTimeOffset hardStop);
|
||||
|
||||
protected Tuple<PlayoutBuilderState, List<PlayoutItem>> AddTailFiller(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
List<PlayoutItem> playoutItems,
|
||||
DateTimeOffset nextItemStart)
|
||||
{
|
||||
var newItems = new List<PlayoutItem>(playoutItems);
|
||||
PlayoutBuilderState nextState = playoutBuilderState;
|
||||
|
||||
if (scheduleItem.TailFiller != null)
|
||||
{
|
||||
IMediaCollectionEnumerator enumerator =
|
||||
collectionEnumerators[CollectionKey.ForFillerPreset(scheduleItem.TailFiller)];
|
||||
|
||||
while (enumerator.Current.IsSome && nextState.CurrentTime < nextItemStart)
|
||||
{
|
||||
MediaItem mediaItem = enumerator.Current.ValueUnsafe();
|
||||
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
|
||||
if (nextState.CurrentTime + itemDuration > nextItemStart)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Filler with duration {Duration} will go past next item start {NextItemStart}",
|
||||
itemDuration,
|
||||
nextItemStart);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = nextState.CurrentTime.UtcDateTime,
|
||||
Finish = nextState.CurrentTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
FillerKind = FillerKind.Tail,
|
||||
GuideGroup = nextState.NextGuideGroup
|
||||
};
|
||||
|
||||
newItems.Add(playoutItem);
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = nextState.CurrentTime + itemDuration
|
||||
};
|
||||
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
return Tuple(nextState, newItems);
|
||||
}
|
||||
|
||||
protected Tuple<PlayoutBuilderState, List<PlayoutItem>> AddFallbackFiller(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
List<PlayoutItem> playoutItems,
|
||||
DateTimeOffset nextItemStart)
|
||||
{
|
||||
var newItems = new List<PlayoutItem>(playoutItems);
|
||||
PlayoutBuilderState nextState = playoutBuilderState;
|
||||
|
||||
if (scheduleItem.FallbackFiller != null && playoutBuilderState.CurrentTime < nextItemStart)
|
||||
{
|
||||
IMediaCollectionEnumerator enumerator =
|
||||
collectionEnumerators[CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller)];
|
||||
|
||||
foreach (MediaItem mediaItem in enumerator.Current)
|
||||
{
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = nextState.CurrentTime.UtcDateTime,
|
||||
Finish = nextItemStart.UtcDateTime,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = TimeSpan.Zero,
|
||||
GuideGroup = nextState.NextGuideGroup,
|
||||
FillerKind = FillerKind.Fallback
|
||||
};
|
||||
|
||||
newItems.Add(playoutItem);
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = nextItemStart.UtcDateTime
|
||||
};
|
||||
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
return Tuple(nextState, newItems);
|
||||
}
|
||||
|
||||
protected static TimeSpan DurationForMediaItem(MediaItem mediaItem)
|
||||
{
|
||||
MediaVersion version = mediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
return version.Duration;
|
||||
}
|
||||
|
||||
protected static List<MediaChapter> ChaptersForMediaItem(MediaItem mediaItem)
|
||||
{
|
||||
MediaVersion version = mediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
return version.Chapters;
|
||||
}
|
||||
|
||||
protected void LogScheduledItem(
|
||||
ProgramScheduleItem scheduleItem,
|
||||
MediaItem mediaItem,
|
||||
DateTimeOffset startTime) =>
|
||||
_logger.LogDebug(
|
||||
"Scheduling media item: {ScheduleItemNumber} / {CollectionType} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
|
||||
scheduleItem.Index,
|
||||
scheduleItem.CollectionType,
|
||||
mediaItem.Id,
|
||||
PlayoutBuilder.DisplayTitle(mediaItem),
|
||||
startTime);
|
||||
|
||||
internal static DateTimeOffset CalculateEndTimeWithFiller(
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
DateTimeOffset itemStartTime,
|
||||
TimeSpan itemDuration,
|
||||
List<MediaChapter> chapters)
|
||||
{
|
||||
var allFiller = Optional(scheduleItem.PreRollFiller)
|
||||
.Append(Optional(scheduleItem.MidRollFiller))
|
||||
.Append(Optional(scheduleItem.PostRollFiller))
|
||||
.ToList();
|
||||
|
||||
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
|
||||
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1)
|
||||
{
|
||||
// multiple pad-to-nearest-minute values are invalid; use no filler
|
||||
// TODO: log error?
|
||||
return itemStartTime + itemDuration;
|
||||
}
|
||||
|
||||
TimeSpan totalDuration = itemDuration;
|
||||
foreach (FillerPreset filler in allFiller)
|
||||
{
|
||||
switch (filler.FillerKind, filler.FillerMode)
|
||||
{
|
||||
case (FillerKind.MidRoll, FillerMode.Duration) when filler.Duration.HasValue:
|
||||
IMediaCollectionEnumerator mrde = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
var mrdePeekOffset = 0;
|
||||
for (var i = 0; i < chapters.Count - 1; i++)
|
||||
{
|
||||
TimeSpan midRollDuration = filler.Duration.Value;
|
||||
while (mrde.Peek(mrdePeekOffset))
|
||||
{
|
||||
foreach (MediaItem mediaItem in mrde.Peek(mrdePeekOffset))
|
||||
{
|
||||
TimeSpan currentDuration = DurationForMediaItem(mediaItem);
|
||||
midRollDuration -= currentDuration;
|
||||
if (midRollDuration >= TimeSpan.Zero)
|
||||
{
|
||||
totalDuration += currentDuration;
|
||||
mrdePeekOffset++;
|
||||
}
|
||||
}
|
||||
|
||||
if (midRollDuration < TimeSpan.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case (FillerKind.MidRoll, FillerMode.Count) when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator mrce = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
var mrcePeekOffset = 0;
|
||||
for (var i = 0; i < chapters.Count - 1; i++)
|
||||
{
|
||||
for (var j = 0; j < filler.Count.Value; j++)
|
||||
{
|
||||
foreach (MediaItem mediaItem in mrce.Peek(mrcePeekOffset))
|
||||
{
|
||||
totalDuration += DurationForMediaItem(mediaItem);
|
||||
mrcePeekOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case (_, FillerMode.Duration) when filler.Duration.HasValue:
|
||||
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
var peekOffset1 = 0;
|
||||
TimeSpan duration = filler.Duration.Value;
|
||||
while (e1.Peek(peekOffset1).IsSome)
|
||||
{
|
||||
foreach (MediaItem mediaItem in e1.Peek(peekOffset1))
|
||||
{
|
||||
TimeSpan currentDuration = DurationForMediaItem(mediaItem);
|
||||
duration -= currentDuration;
|
||||
if (duration >= TimeSpan.Zero)
|
||||
{
|
||||
totalDuration += currentDuration;
|
||||
peekOffset1++;
|
||||
}
|
||||
}
|
||||
|
||||
if (duration < TimeSpan.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case (_, FillerMode.Count) when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
var peekOffset2 = 0;
|
||||
for (var i = 0; i < filler.Count.Value; i++)
|
||||
{
|
||||
foreach (MediaItem mediaItem in e2.Peek(peekOffset2))
|
||||
{
|
||||
totalDuration += DurationForMediaItem(mediaItem);
|
||||
peekOffset2++;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue)))
|
||||
{
|
||||
int currentMinute = (itemStartTime + totalDuration).Minute;
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
|
||||
padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value;
|
||||
|
||||
DateTimeOffset targetTime = itemStartTime + totalDuration - TimeSpan.FromMinutes(currentMinute) +
|
||||
TimeSpan.FromMinutes(targetMinute);
|
||||
|
||||
return new DateTimeOffset(
|
||||
targetTime.Year,
|
||||
targetTime.Month,
|
||||
targetTime.Day,
|
||||
targetTime.Hour,
|
||||
targetTime.Minute,
|
||||
0,
|
||||
targetTime.Offset);
|
||||
}
|
||||
|
||||
return itemStartTime + totalDuration;
|
||||
}
|
||||
|
||||
protected List<PlayoutItem> AddFiller(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
PlayoutItem playoutItem,
|
||||
List<MediaChapter> chapters)
|
||||
{
|
||||
var result = new List<PlayoutItem>();
|
||||
|
||||
var allFiller = Optional(scheduleItem.PreRollFiller)
|
||||
.Append(Optional(scheduleItem.MidRollFiller))
|
||||
.Append(Optional(scheduleItem.PostRollFiller))
|
||||
.ToList();
|
||||
|
||||
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
|
||||
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1)
|
||||
{
|
||||
// multiple pad-to-nearest-minute values are invalid; use no filler
|
||||
// TODO: log error?
|
||||
return new List<PlayoutItem> { playoutItem };
|
||||
}
|
||||
|
||||
List<MediaChapter> effectiveChapters = chapters;
|
||||
if (allFiller.All(fp => fp.FillerKind != FillerKind.MidRoll))
|
||||
{
|
||||
effectiveChapters = new List<MediaChapter>();
|
||||
}
|
||||
|
||||
foreach (FillerPreset filler in allFiller.Filter(
|
||||
f => f.FillerKind == FillerKind.PreRoll && f.FillerMode != FillerMode.Pad))
|
||||
{
|
||||
switch (filler.FillerMode)
|
||||
{
|
||||
case FillerMode.Duration when filler.Duration.HasValue:
|
||||
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
result.AddRange(
|
||||
AddDurationFiller(playoutBuilderState, e1, filler.Duration.Value, FillerKind.PreRoll));
|
||||
break;
|
||||
case FillerMode.Count when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
result.AddRange(
|
||||
AddCountFiller(playoutBuilderState, e2, filler.Count.Value, FillerKind.PreRoll));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!effectiveChapters.Any())
|
||||
{
|
||||
result.Add(playoutItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (FillerPreset filler in allFiller.Filter(
|
||||
f => f.FillerKind == FillerKind.MidRoll && f.FillerMode != FillerMode.Pad))
|
||||
{
|
||||
switch (filler.FillerMode)
|
||||
{
|
||||
case FillerMode.Duration when filler.Duration.HasValue:
|
||||
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
for (var i = 0; i < effectiveChapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
{
|
||||
result.AddRange(
|
||||
AddDurationFiller(
|
||||
playoutBuilderState,
|
||||
e1,
|
||||
filler.Duration.Value,
|
||||
FillerKind.MidRoll));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case FillerMode.Count when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
for (var i = 0; i < effectiveChapters.Count - 1; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
result.AddRange(
|
||||
AddCountFiller(
|
||||
playoutBuilderState,
|
||||
e2,
|
||||
filler.Count.Value,
|
||||
FillerKind.MidRoll));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (FillerPreset filler in allFiller.Filter(
|
||||
f => f.FillerKind == FillerKind.PostRoll && f.FillerMode != FillerMode.Pad))
|
||||
{
|
||||
switch (filler.FillerMode)
|
||||
{
|
||||
case FillerMode.Duration when filler.Duration.HasValue:
|
||||
IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
result.AddRange(
|
||||
AddDurationFiller(playoutBuilderState, e1, filler.Duration.Value, FillerKind.PostRoll));
|
||||
break;
|
||||
case FillerMode.Count when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
result.AddRange(
|
||||
AddCountFiller(playoutBuilderState, e2, filler.Count.Value, FillerKind.PostRoll));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// after all non-padded filler has been added, figure out padding
|
||||
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue)))
|
||||
{
|
||||
var totalDuration =
|
||||
TimeSpan.FromMilliseconds(
|
||||
result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds) +
|
||||
effectiveChapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds));
|
||||
|
||||
int currentMinute = (playoutItem.StartOffset + totalDuration).Minute;
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
|
||||
padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value;
|
||||
|
||||
DateTimeOffset almostTargetTime = playoutItem.StartOffset + totalDuration -
|
||||
TimeSpan.FromMinutes(currentMinute) +
|
||||
TimeSpan.FromMinutes(targetMinute);
|
||||
|
||||
|
||||
var targetTime = new DateTimeOffset(
|
||||
almostTargetTime.Year,
|
||||
almostTargetTime.Month,
|
||||
almostTargetTime.Day,
|
||||
almostTargetTime.Hour,
|
||||
almostTargetTime.Minute,
|
||||
0,
|
||||
almostTargetTime.Offset);
|
||||
|
||||
TimeSpan remainingToFill = targetTime - totalDuration - playoutItem.StartOffset;
|
||||
|
||||
// _logger.LogInformation(
|
||||
// "Total duration {TotalDuration}; need to fill {TimeSpan} to pad properly to {TargetTime}",
|
||||
// totalDuration,
|
||||
// remainingToFill,
|
||||
// targetTime);
|
||||
|
||||
switch (padFiller.FillerKind)
|
||||
{
|
||||
case FillerKind.PreRoll:
|
||||
IMediaCollectionEnumerator pre1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
|
||||
result.InsertRange(
|
||||
0,
|
||||
AddDurationFiller(
|
||||
playoutBuilderState,
|
||||
pre1,
|
||||
remainingToFill,
|
||||
FillerKind.PreRoll));
|
||||
totalDuration =
|
||||
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
||||
remainingToFill = targetTime - totalDuration - playoutItem.StartOffset;
|
||||
if (remainingToFill > TimeSpan.Zero)
|
||||
{
|
||||
result.InsertRange(
|
||||
0,
|
||||
FallbackFillerForPad(
|
||||
playoutBuilderState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
remainingToFill));
|
||||
}
|
||||
|
||||
break;
|
||||
case FillerKind.MidRoll:
|
||||
IMediaCollectionEnumerator mid1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
|
||||
var fillerQueue = new Queue<PlayoutItem>(
|
||||
AddDurationFiller(
|
||||
playoutBuilderState,
|
||||
mid1,
|
||||
remainingToFill,
|
||||
FillerKind.MidRoll));
|
||||
TimeSpan average = effectiveChapters.Count == 0
|
||||
? remainingToFill
|
||||
: remainingToFill / (effectiveChapters.Count - 1);
|
||||
TimeSpan filled = TimeSpan.Zero;
|
||||
for (var i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
{
|
||||
TimeSpan current = TimeSpan.Zero;
|
||||
while (current < average && filled < remainingToFill)
|
||||
{
|
||||
if (fillerQueue.TryDequeue(out PlayoutItem fillerItem))
|
||||
{
|
||||
result.Add(fillerItem);
|
||||
current += fillerItem.Finish - fillerItem.Start;
|
||||
filled += fillerItem.Finish - fillerItem.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeSpan leftInThisBreak = average - current;
|
||||
TimeSpan leftOverall = remainingToFill - filled;
|
||||
|
||||
TimeSpan maxThisBreak = leftOverall < leftInThisBreak
|
||||
? leftOverall
|
||||
: leftInThisBreak;
|
||||
|
||||
Option<PlayoutItem> maybeFallback = FallbackFillerForPad(
|
||||
playoutBuilderState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
i < effectiveChapters.Count - 1 ? maxThisBreak : leftOverall);
|
||||
|
||||
foreach (PlayoutItem fallback in maybeFallback)
|
||||
{
|
||||
current += fallback.Finish - fallback.Start;
|
||||
filled += fallback.Finish - fallback.Start;
|
||||
result.Add(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case FillerKind.PostRoll:
|
||||
IMediaCollectionEnumerator post1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
|
||||
result.AddRange(
|
||||
AddDurationFiller(
|
||||
playoutBuilderState,
|
||||
post1,
|
||||
remainingToFill,
|
||||
FillerKind.PostRoll));
|
||||
totalDuration =
|
||||
TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds));
|
||||
remainingToFill = targetTime - totalDuration - playoutItem.StartOffset;
|
||||
if (remainingToFill > TimeSpan.Zero)
|
||||
{
|
||||
result.AddRange(
|
||||
FallbackFillerForPad(
|
||||
playoutBuilderState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
remainingToFill));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// fix times on each playout item
|
||||
DateTimeOffset currentTime = playoutItem.StartOffset;
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
{
|
||||
PlayoutItem item = result[i];
|
||||
TimeSpan duration = item.Finish - item.Start;
|
||||
item.Start = currentTime.UtcDateTime;
|
||||
item.Finish = (currentTime + duration).UtcDateTime;
|
||||
currentTime = item.FinishOffset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<PlayoutItem> AddCountFiller(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
IMediaCollectionEnumerator enumerator,
|
||||
int count,
|
||||
FillerKind fillerKind)
|
||||
{
|
||||
var result = new List<PlayoutItem>();
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
foreach (MediaItem mediaItem in enumerator.Current)
|
||||
{
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = playoutBuilderState.NextGuideGroup,
|
||||
FillerKind = fillerKind
|
||||
};
|
||||
|
||||
result.Add(playoutItem);
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<PlayoutItem> AddDurationFiller(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
IMediaCollectionEnumerator enumerator,
|
||||
TimeSpan duration,
|
||||
FillerKind fillerKind)
|
||||
{
|
||||
var result = new List<PlayoutItem>();
|
||||
|
||||
while (enumerator.Current.IsSome)
|
||||
{
|
||||
foreach (MediaItem mediaItem in enumerator.Current)
|
||||
{
|
||||
// TODO: retry up to x times when item doesn't fit?
|
||||
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
duration -= itemDuration;
|
||||
|
||||
if (duration >= TimeSpan.Zero)
|
||||
{
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = playoutBuilderState.NextGuideGroup,
|
||||
FillerKind = fillerKind
|
||||
};
|
||||
|
||||
result.Add(playoutItem);
|
||||
enumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
if (duration < TimeSpan.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Option<PlayoutItem> FallbackFillerForPad(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
TimeSpan duration)
|
||||
{
|
||||
if (scheduleItem.FallbackFiller != null)
|
||||
{
|
||||
IMediaCollectionEnumerator enumerator =
|
||||
enumerators[CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller)];
|
||||
|
||||
foreach (MediaItem mediaItem in enumerator.Current)
|
||||
{
|
||||
var result = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
Finish = new DateTime(2020, 2, 1, 0, 0, 0, DateTimeKind.Utc) + duration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = TimeSpan.Zero,
|
||||
GuideGroup = playoutBuilderState.NextGuideGroup,
|
||||
FillerKind = FillerKind.Fallback
|
||||
};
|
||||
|
||||
enumerator.MoveNext();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt;
|
||||
using LanguageExt.UnsafeValueAccess;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramScheduleItemDuration>
|
||||
{
|
||||
public PlayoutModeSchedulerDuration(ILogger logger) : base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
ProgramScheduleItemDuration scheduleItem,
|
||||
ProgramScheduleItem nextScheduleItem,
|
||||
DateTimeOffset hardStop)
|
||||
{
|
||||
var playoutItems = new List<PlayoutItem>();
|
||||
|
||||
PlayoutBuilderState nextState = playoutBuilderState;
|
||||
|
||||
var willFinishInTime = true;
|
||||
Option<DateTimeOffset> durationUntil = None;
|
||||
|
||||
IMediaCollectionEnumerator contentEnumerator =
|
||||
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
|
||||
while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime)
|
||||
{
|
||||
MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe();
|
||||
|
||||
// find when we should start this item, based on the current time
|
||||
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
|
||||
|
||||
// remember when we need to finish this duration item
|
||||
if (nextState.DurationFinish.IsNone)
|
||||
{
|
||||
nextState = nextState with
|
||||
{
|
||||
DurationFinish = itemStartTime + scheduleItem.PlayoutDuration
|
||||
};
|
||||
|
||||
durationUntil = nextState.DurationFinish;
|
||||
}
|
||||
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
|
||||
|
||||
if (itemDuration > scheduleItem.PlayoutDuration)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Skipping playout item {Title} with duration {Duration} that is longer than schedule item duration {PlayoutDuration}",
|
||||
PlayoutBuilder.DisplayTitle(mediaItem),
|
||||
itemDuration,
|
||||
scheduleItem.PlayoutDuration);
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = nextState.NextGuideGroup,
|
||||
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
|
||||
? FillerKind.Tail
|
||||
: FillerKind.None
|
||||
};
|
||||
|
||||
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
|
||||
|
||||
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
willFinishInTime = itemStartTime > durationFinish ||
|
||||
itemEndTimeWithFiller <= durationFinish;
|
||||
if (willFinishInTime)
|
||||
{
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
playoutItems.AddRange(
|
||||
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller
|
||||
};
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeSpan durationBlock = itemEndTimeWithFiller - itemStartTime;
|
||||
if (itemEndTimeWithFiller - itemStartTime > scheduleItem.PlayoutDuration)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to schedule duration block of {DurationBlock} which is longer than the configured playout duration {PlayoutDuration}",
|
||||
durationBlock,
|
||||
scheduleItem.PlayoutDuration);
|
||||
}
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
DurationFinish = None,
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// this is needed when the duration finish exactly matches the hard stop
|
||||
if (nextState.DurationFinish.IsSome && nextState.CurrentTime == nextState.DurationFinish)
|
||||
{
|
||||
nextState = nextState with
|
||||
{
|
||||
DurationFinish = None,
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
|
||||
};
|
||||
}
|
||||
|
||||
foreach (DateTimeOffset nextItemStart in durationUntil)
|
||||
{
|
||||
switch (scheduleItem.TailMode)
|
||||
{
|
||||
case TailMode.Filler:
|
||||
if (scheduleItem.TailFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddTailFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
nextItemStart);
|
||||
}
|
||||
|
||||
if (scheduleItem.FallbackFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddFallbackFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
nextItemStart);
|
||||
}
|
||||
|
||||
nextState = nextState with { CurrentTime = nextItemStart };
|
||||
break;
|
||||
case TailMode.Offline:
|
||||
if (scheduleItem.FallbackFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddFallbackFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
nextItemStart);
|
||||
}
|
||||
|
||||
nextState = nextState with { CurrentTime = nextItemStart };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
|
||||
|
||||
return Tuple(nextState, playoutItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt.UnsafeValueAccess;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramScheduleItemFlood>
|
||||
{
|
||||
private readonly List<ProgramScheduleItem> _sortedScheduleItems;
|
||||
|
||||
public PlayoutModeSchedulerFlood(List<ProgramScheduleItem> sortedScheduleItems, ILogger logger)
|
||||
: base(logger)
|
||||
{
|
||||
_sortedScheduleItems = sortedScheduleItems;
|
||||
}
|
||||
|
||||
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
ProgramScheduleItemFlood scheduleItem,
|
||||
ProgramScheduleItem nextScheduleItem,
|
||||
DateTimeOffset hardStop)
|
||||
{
|
||||
var playoutItems = new List<PlayoutItem>();
|
||||
|
||||
PlayoutBuilderState nextState = playoutBuilderState;
|
||||
var willFinishInTime = true;
|
||||
|
||||
IMediaCollectionEnumerator contentEnumerator =
|
||||
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
|
||||
|
||||
while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime)
|
||||
{
|
||||
MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe();
|
||||
|
||||
// find when we should start this item, based on the current time
|
||||
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = nextState.NextGuideGroup,
|
||||
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
|
||||
? FillerKind.Tail
|
||||
: FillerKind.None
|
||||
};
|
||||
|
||||
ProgramScheduleItem peekScheduleItem =
|
||||
_sortedScheduleItems[(nextState.ScheduleItemIndex + 1) % _sortedScheduleItems.Count];
|
||||
DateTimeOffset peekScheduleItemStart =
|
||||
peekScheduleItem.StartType == StartType.Fixed
|
||||
? GetStartTimeAfter(nextState, peekScheduleItem)
|
||||
: DateTimeOffset.MaxValue;
|
||||
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
|
||||
// if the current time is before the next schedule item, but the current finish
|
||||
// is after, we need to move on to the next schedule item
|
||||
willFinishInTime = itemStartTime > peekScheduleItemStart ||
|
||||
itemEndTimeWithFiller <= peekScheduleItemStart;
|
||||
|
||||
if (willFinishInTime)
|
||||
{
|
||||
playoutItems.AddRange(
|
||||
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
|
||||
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
|
||||
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}",
|
||||
itemEndTimeWithFiller,
|
||||
actualEndTime);
|
||||
|
||||
// _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems);
|
||||
}
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
InFlood = true,
|
||||
NextGuideGroup = nextState.IncrementGuideGroup
|
||||
};
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
// _logger.LogDebug(
|
||||
// "Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
// "Flood");
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
|
||||
InFlood = nextState.CurrentTime >= hardStop,
|
||||
NextGuideGroup = nextState.DecrementGuideGroup
|
||||
};
|
||||
|
||||
ProgramScheduleItem peekItem =
|
||||
_sortedScheduleItems[nextState.ScheduleItemIndex % _sortedScheduleItems.Count];
|
||||
DateTimeOffset peekItemStart = GetStartTimeAfter(nextState, peekItem);
|
||||
|
||||
if (scheduleItem.TailFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddTailFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
peekItemStart);
|
||||
}
|
||||
|
||||
if (scheduleItem.FallbackFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddFallbackFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
peekItemStart);
|
||||
}
|
||||
|
||||
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
|
||||
|
||||
return Tuple(nextState, playoutItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt;
|
||||
using LanguageExt.UnsafeValueAccess;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramScheduleItemMultiple>
|
||||
{
|
||||
private readonly Map<CollectionKey, List<MediaItem>> _collectionMediaItems;
|
||||
|
||||
public PlayoutModeSchedulerMultiple(Map<CollectionKey, List<MediaItem>> collectionMediaItems, ILogger logger)
|
||||
: base(logger)
|
||||
{
|
||||
_collectionMediaItems = collectionMediaItems;
|
||||
}
|
||||
|
||||
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
ProgramScheduleItemMultiple scheduleItem,
|
||||
ProgramScheduleItem nextScheduleItem,
|
||||
DateTimeOffset hardStop)
|
||||
{
|
||||
var playoutItems = new List<PlayoutItem>();
|
||||
|
||||
PlayoutBuilderState nextState = playoutBuilderState with
|
||||
{
|
||||
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count)
|
||||
};
|
||||
|
||||
if (nextState.MultipleRemaining == 0)
|
||||
{
|
||||
nextState = nextState with
|
||||
{
|
||||
MultipleRemaining = _collectionMediaItems[CollectionKey.ForScheduleItem(scheduleItem)].Count
|
||||
};
|
||||
}
|
||||
|
||||
IMediaCollectionEnumerator contentEnumerator =
|
||||
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
|
||||
while (contentEnumerator.Current.IsSome && nextState.MultipleRemaining > 0 &&
|
||||
nextState.CurrentTime < hardStop)
|
||||
{
|
||||
MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe();
|
||||
|
||||
// find when we should start this item, based on the current time
|
||||
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
|
||||
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = nextState.NextGuideGroup,
|
||||
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
|
||||
? FillerKind.Tail
|
||||
: FillerKind.None
|
||||
};
|
||||
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
|
||||
playoutItems.AddRange(
|
||||
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1)
|
||||
};
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
}
|
||||
|
||||
if (nextState.MultipleRemaining.IfNone(-1) == 0)
|
||||
{
|
||||
// _logger.LogDebug(
|
||||
// "Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
// "Multiple");
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
|
||||
MultipleRemaining = None
|
||||
};
|
||||
}
|
||||
|
||||
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
|
||||
|
||||
if (scheduleItem.TailFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddTailFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
nextItemStart);
|
||||
}
|
||||
|
||||
if (scheduleItem.FallbackFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddFallbackFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
nextItemStart);
|
||||
}
|
||||
|
||||
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
|
||||
|
||||
return Tuple(nextState, playoutItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleItemOne>
|
||||
{
|
||||
public PlayoutModeSchedulerOne(ILogger logger) : base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
ProgramScheduleItemOne scheduleItem,
|
||||
ProgramScheduleItem nextScheduleItem,
|
||||
DateTimeOffset hardStop)
|
||||
{
|
||||
IMediaCollectionEnumerator contentEnumerator =
|
||||
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
|
||||
foreach (MediaItem mediaItem in contentEnumerator.Current)
|
||||
{
|
||||
// find when we should start this item, based on the current time
|
||||
DateTimeOffset itemStartTime = GetStartTimeAfter(
|
||||
playoutBuilderState,
|
||||
scheduleItem);
|
||||
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
|
||||
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + itemDuration,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = itemDuration,
|
||||
GuideGroup = playoutBuilderState.NextGuideGroup,
|
||||
FillerKind = scheduleItem.GuideMode == GuideMode.Filler
|
||||
? FillerKind.Tail
|
||||
: FillerKind.None
|
||||
};
|
||||
|
||||
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
itemStartTime,
|
||||
itemDuration,
|
||||
itemChapters);
|
||||
|
||||
List<PlayoutItem> playoutItems = AddFiller(
|
||||
playoutBuilderState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItem,
|
||||
itemChapters);
|
||||
|
||||
// only play one item from collection, so always advance to the next item
|
||||
// _logger.LogDebug(
|
||||
// "Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
// "One");
|
||||
|
||||
PlayoutBuilderState nextState = playoutBuilderState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
ScheduleItemIndex = playoutBuilderState.ScheduleItemIndex + 1
|
||||
};
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
|
||||
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
|
||||
|
||||
if (scheduleItem.TailFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddTailFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
nextItemStart);
|
||||
}
|
||||
|
||||
if (scheduleItem.FallbackFiller != null)
|
||||
{
|
||||
(nextState, playoutItems) = AddFallbackFiller(
|
||||
nextState,
|
||||
collectionEnumerators,
|
||||
scheduleItem,
|
||||
playoutItems,
|
||||
nextItemStart);
|
||||
}
|
||||
|
||||
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
|
||||
|
||||
return Tuple(nextState, playoutItems);
|
||||
}
|
||||
|
||||
return Tuple(playoutBuilderState, new List<PlayoutItem>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,5 +37,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
_index = _random.Next() % _mediaItems.Count;
|
||||
State.Index++;
|
||||
}
|
||||
|
||||
public Option<MediaItem> Peek(int offset) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt;
|
||||
@@ -63,7 +64,9 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
State.Index %= _shuffled.Count;
|
||||
}
|
||||
|
||||
|
||||
public Option<MediaItem> Peek(int offset) => throw new NotSupportedException();
|
||||
|
||||
private IList<MediaItem> Shuffle(IList<CollectionWithItems> collections, Random random)
|
||||
{
|
||||
// based on https://keyj.emphy.de/balanced-shuffle/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt;
|
||||
@@ -64,6 +65,35 @@ namespace ErsatzTV.Core.Scheduling
|
||||
State.Index %= _shuffled.Count;
|
||||
}
|
||||
|
||||
public Option<MediaItem> Peek(int offset)
|
||||
{
|
||||
if ((State.Index + offset) % _mediaItemCount == 0)
|
||||
{
|
||||
IList<MediaItem> shuffled;
|
||||
Option<MediaItem> tail = Current;
|
||||
|
||||
// clone the random
|
||||
var randomCopy = new Random();
|
||||
FieldInfo seedArrayInfo = typeof(Random).GetField(
|
||||
"_seedArray",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var seedArray = seedArrayInfo.GetValue(_random) as int[];
|
||||
int[] seedArrayCopy = seedArray.ToArray();
|
||||
seedArrayInfo.SetValue(randomCopy, seedArrayCopy);
|
||||
|
||||
do
|
||||
{
|
||||
int newSeed = randomCopy.Next();
|
||||
randomCopy = new Random(newSeed);
|
||||
shuffled = Shuffle(_mediaItems, randomCopy);
|
||||
} while (_mediaItems.Count > 1 && shuffled[0] == tail);
|
||||
|
||||
return shuffled.Any() ? shuffled[0] : None;
|
||||
}
|
||||
|
||||
return _shuffled.Any() ? _shuffled[(State.Index + offset) % _mediaItemCount] : None;
|
||||
}
|
||||
|
||||
private IList<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, Random random)
|
||||
{
|
||||
GroupedMediaItem[] copy = list.ToArray();
|
||||
|
||||
@@ -5,5 +5,6 @@ namespace ErsatzTV.Core
|
||||
public static class SystemTime
|
||||
{
|
||||
public static DateTime MinValueUtc = new(0, DateTimeKind.Utc);
|
||||
public static DateTime MaxValueUtc = new(DateTime.MaxValue.Ticks, DateTimeKind.Utc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Filler
|
||||
{
|
||||
public class FillerPresetConfiguration : IEntityTypeConfiguration<FillerPreset>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<FillerPreset> builder)
|
||||
{
|
||||
builder.ToTable("FillerPreset");
|
||||
|
||||
builder.HasOne(i => i.Collection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.CollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.MediaItem)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.MediaItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.MultiCollection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.MultiCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.SmartCollection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.SmartCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class MediaChapterConfiguration : IEntityTypeConfiguration<MediaChapter>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MediaChapter> builder) => builder.ToTable("MediaChapter");
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
.WithOne(s => s.MediaVersion)
|
||||
.HasForeignKey(s => s.MediaVersionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(v => v.Chapters)
|
||||
.WithOne(c => c.MediaVersion)
|
||||
.HasForeignKey(c => c.MediaVersionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,42 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
.HasForeignKey(i => i.MultiCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.SmartCollection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.SmartCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.PreRollFiller)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.PreRollFillerId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.MidRollFiller)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.MidRollFillerId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.PostRollFiller)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.PostRollFillerId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.TailFiller)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.TailFillerId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.FallbackFiller)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.FallbackFillerId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-21
@@ -6,27 +6,7 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class ProgramScheduleItemDurationConfiguration : IEntityTypeConfiguration<ProgramScheduleItemDuration>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProgramScheduleItemDuration> builder)
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProgramScheduleItemDuration> builder) =>
|
||||
builder.ToTable("ProgramScheduleDurationItem");
|
||||
|
||||
builder.HasOne(i => i.TailCollection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.TailCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.TailMediaItem)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.TailMediaItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.TailMultiCollection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.TailMultiCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.AsNoTracking()
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.Include(mv => mv.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Include(mv => mv.Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Filter(mv => mv.ArtistId == artistId)
|
||||
|
||||
@@ -233,7 +233,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -370,7 +372,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -526,7 +530,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -558,12 +564,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, ShowIds = showIds }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM MediaItem WHERE Id IN
|
||||
(SELECT m.Id FROM MediaItem m
|
||||
INNER JOIN EmbyShow js ON js.Id = m.Id
|
||||
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||
WHERE lp.LibraryId = @LibraryId AND js.ItemId IN @ShowIds)",
|
||||
new { LibraryId = library.Id, ShowIds = showIds });
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
}
|
||||
@@ -587,12 +589,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM MediaItem WHERE Id IN
|
||||
(SELECT m.Id FROM MediaItem m
|
||||
INNER JOIN EmbyEpisode ee ON ee.Id = m.Id
|
||||
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
|
||||
WHERE LP.LibraryId = @LibraryId AND ee.ItemId IN @EpisodeIds)",
|
||||
new { LibraryId = library.Id, EpisodeIds = episodeIds });
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -250,7 +250,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -370,7 +372,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -527,7 +531,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -559,12 +565,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, ShowIds = showIds }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM MediaItem WHERE Id IN
|
||||
(SELECT m.Id FROM MediaItem m
|
||||
INNER JOIN JellyfinShow js ON js.Id = m.Id
|
||||
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||
WHERE lp.LibraryId = @LibraryId AND js.ItemId IN @ShowIds)",
|
||||
new { LibraryId = library.Id, ShowIds = showIds });
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
}
|
||||
@@ -588,12 +590,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM MediaItem WHERE Id IN
|
||||
(SELECT m.Id FROM MediaItem m
|
||||
INNER JOIN JellyfinEpisode je ON je.Id = m.Id
|
||||
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
|
||||
WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)",
|
||||
new { LibraryId = library.Id, EpisodeIds = episodeIds });
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -362,6 +362,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
dbContext.Movies
|
||||
.Include(m => m.MovieMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Filter(m => movieIds.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
|
||||
@@ -385,6 +386,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Filter(m => musicVideoIds.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
|
||||
@@ -416,6 +418,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Filter(m => musicVideoIds.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
|
||||
@@ -434,6 +437,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
dbContext.OtherVideos
|
||||
.Include(m => m.OtherVideoMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Filter(m => otherVideoIds.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
|
||||
@@ -454,6 +458,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
dbContext.Episodes
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
@@ -488,6 +493,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
dbContext.Episodes
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
@@ -520,6 +526,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
dbContext.Episodes
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<PlexMediaSource>> GetPlexByLibraryId(int plexLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN PlexLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @PlexLibraryId",
|
||||
@@ -113,55 +113,30 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task Update(
|
||||
PlexMediaSource plexMediaSource,
|
||||
List<PlexConnection> sortedConnections,
|
||||
List<PlexConnection> toAdd,
|
||||
List<PlexConnection> toDelete)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE PlexMediaSource SET
|
||||
ProductVersion = @ProductVersion,
|
||||
Platform = @Platform,
|
||||
PlatformVersion = @PlatformVersion,
|
||||
ServerName = @ServerName
|
||||
WHERE Id = @Id",
|
||||
new
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
dbContext.Entry(plexMediaSource).State = EntityState.Modified;
|
||||
|
||||
if (toAdd.Any() || toDelete.Any())
|
||||
{
|
||||
plexMediaSource.Connections.Clear();
|
||||
await dbContext.Entry(plexMediaSource).Collection(pms => pms.Connections).LoadAsync();
|
||||
|
||||
plexMediaSource.Connections.AddRange(toAdd);
|
||||
plexMediaSource.Connections.RemoveAll(toDelete.Contains);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (PlexConnection connection in plexMediaSource.Connections)
|
||||
{
|
||||
plexMediaSource.ProductVersion,
|
||||
plexMediaSource.Platform,
|
||||
plexMediaSource.PlatformVersion,
|
||||
plexMediaSource.ServerName,
|
||||
plexMediaSource.Id
|
||||
});
|
||||
|
||||
foreach (PlexConnection add in toAdd)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"INSERT INTO PlexConnection (IsActive, Uri, PlexMediaSourceId)
|
||||
VALUES (0, @Uri, @PlexMediaSourceId)",
|
||||
new { add.Uri, PlexMediaSourceId = plexMediaSource.Id });
|
||||
dbContext.Entry(connection).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (PlexConnection delete in toDelete)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM PlexConnection WHERE Id = @Id",
|
||||
new { delete.Id });
|
||||
}
|
||||
|
||||
int activeCount = await _dbConnection.QuerySingleAsync<int>(
|
||||
@"SELECT COUNT(*) FROM PlexConnection WHERE IsActive = 1 AND PlexMediaSourceId = @PlexMediaSourceId",
|
||||
new { PlexMediaSourceId = plexMediaSource.Id });
|
||||
if (activeCount == 0)
|
||||
{
|
||||
Option<PlexConnection> toActivate =
|
||||
sortedConnections.FirstOrDefault(c => toDelete.All(d => d.Id != c.Id));
|
||||
|
||||
// update on uri because connections from Plex API don't have our local ids
|
||||
await toActivate.IfSomeAsync(
|
||||
async c => await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE PlexConnection SET IsActive = 1 WHERE Uri = @Uri",
|
||||
new { c.Uri }));
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<int>> UpdateLibraries(
|
||||
@@ -587,7 +562,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<JellyfinMediaSource>> GetJellyfinByLibraryId(int jellyfinLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN JellyfinLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @JellyfinLibraryId",
|
||||
@@ -770,7 +745,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<EmbyMediaSource>> GetEmbyByLibraryId(int embyLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN EmbyLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @EmbyLibraryId",
|
||||
|
||||
@@ -117,6 +117,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Option<MediaVersion> maybeVersion = await dbContext.MediaVersions
|
||||
.Include(v => v.Streams)
|
||||
.Include(v => v.Chapters)
|
||||
.OrderBy(v => v.Id)
|
||||
.SingleOrDefaultAsync(v => v.Id == mediaVersionId)
|
||||
.Map(Optional);
|
||||
@@ -161,6 +162,31 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
existingStream.Default = incomingStream.Default;
|
||||
existingStream.Forced = incomingStream.Forced;
|
||||
}
|
||||
|
||||
var chaptersToAdd = incoming.Chapters
|
||||
.Filter(s => existing.Chapters.All(es => es.ChapterId != s.ChapterId))
|
||||
.ToList();
|
||||
var chaptersToRemove = existing.Chapters
|
||||
.Filter(es => incoming.Chapters.All(s => s.ChapterId != es.ChapterId))
|
||||
.ToList();
|
||||
var chaptersToUpdate = incoming.Chapters.Except(chaptersToAdd).ToList();
|
||||
|
||||
// add
|
||||
existing.Chapters.AddRange(chaptersToAdd);
|
||||
|
||||
// remove
|
||||
existing.Chapters.RemoveAll(chaptersToRemove.Contains);
|
||||
|
||||
// update
|
||||
foreach (MediaChapter incomingChapter in chaptersToUpdate)
|
||||
{
|
||||
MediaChapter existingChapter = existing.Chapters
|
||||
.First(s => s.ChapterId == incomingChapter.ChapterId);
|
||||
|
||||
existingChapter.StartTime = incomingChapter.StartTime;
|
||||
existingChapter.EndTime = incomingChapter.EndTime;
|
||||
existingChapter.Title = incomingChapter.Title;
|
||||
}
|
||||
|
||||
return await dbContext.SaveChangesAsync() > 0;
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using LanguageExt.UnsafeValueAccess;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -237,12 +238,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, Keys = movieKeys }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM MediaItem WHERE Id IN
|
||||
(SELECT m.Id FROM MediaItem m
|
||||
INNER JOIN PlexMovie pm ON pm.Id = m.Id
|
||||
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||
WHERE lp.LibraryId = @LibraryId AND pm.Key not in @Keys)",
|
||||
new { LibraryId = library.Id, Keys = movieKeys });
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
}
|
||||
@@ -273,7 +270,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, ItemIds = movieIds }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
"DELETE FROM JellyfinMovie WHERE Id IN @Ids",
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
@@ -529,7 +526,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, ItemIds = movieIds }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
"DELETE FROM EmbyMovie WHERE Id IN @Ids",
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
|
||||
@@ -78,8 +78,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
foreach (int otherVideoId in ids)
|
||||
{
|
||||
OtherVideo othervide = await dbContext.OtherVideos.FindAsync(otherVideoId);
|
||||
dbContext.OtherVideos.Remove(othervide);
|
||||
OtherVideo otherVideo = await dbContext.OtherVideos.FindAsync(otherVideoId);
|
||||
dbContext.OtherVideos.Remove(otherVideo);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
@@ -538,13 +538,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { SeasonKey = seasonKey, Keys = episodeKeys }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM MediaItem WHERE Id IN
|
||||
(SELECT m.Id FROM MediaItem m
|
||||
INNER JOIN Episode e ON m.Id = e.Id
|
||||
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
|
||||
INNER JOIN PlexSeason P on P.Id = e.SeasonId
|
||||
WHERE P.Key = @SeasonKey AND pe.Key not in @Keys)",
|
||||
new { SeasonKey = seasonKey, Keys = episodeKeys });
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
}
|
||||
@@ -587,6 +582,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.AsNoTracking()
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
@@ -601,6 +597,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.AsNoTracking()
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.ThenInclude(mv => mv.Chapters)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
@@ -683,12 +680,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { LibraryId = library.Id, Keys = showKeys }).Map(result => result.ToList());
|
||||
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM MediaItem WHERE Id IN
|
||||
(SELECT m.Id FROM MediaItem m
|
||||
INNER JOIN PlexShow ps ON ps.Id = m.Id
|
||||
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||
WHERE lp.LibraryId = @LibraryId AND ps.Key not in @Keys)",
|
||||
new { LibraryId = library.Id, Keys = showKeys });
|
||||
"DELETE FROM MediaItem WHERE Id IN @Ids",
|
||||
new { Ids = ids });
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -72,6 +73,7 @@ namespace ErsatzTV.Infrastructure.Data
|
||||
public DbSet<Resolution> Resolutions { get; set; }
|
||||
public DbSet<LanguageCode> LanguageCodes { get; set; }
|
||||
public DbSet<TraktList> TraktLists { get; set; }
|
||||
public DbSet<FillerPreset> FillerPresets { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
|
||||
optionsBuilder.UseLoggerFactory(_loggerFactory);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user