Compare commits
9 Commits
v0.2.0-alp
...
v0.2.2-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f90f5d44 | ||
|
|
bda4117655 | ||
|
|
3240703840 | ||
|
|
53a7570ba3 | ||
|
|
0e789fd6d8 | ||
|
|
0136de700c | ||
|
|
2ea0e64ac1 | ||
|
|
5993f23ec5 | ||
|
|
417f35a834 |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -5,6 +5,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.2-alpha] - 2021-10-30
|
||||
### Fixed
|
||||
- Fix EPG entries for Duration schedule items that play multiple items
|
||||
- Fix EPG entries for Multiple schedule items that play more than one item
|
||||
|
||||
### Added
|
||||
- Add fallback filler settings to Channel and global FFmpeg Settings
|
||||
- When streaming is attempted during an unscheduled gap, the resulting video will be determined using the following priority:
|
||||
- Channel fallback filler
|
||||
- Global fallback filler
|
||||
- Generated `Channel Is Offline` error message video
|
||||
|
||||
### Changed
|
||||
- Allow per-episode folders for local show libraries
|
||||
- e.g. `Show Name\Season #\Episode #\Show Name - s#e#.mkv`
|
||||
|
||||
## [0.2.1-alpha] - 2021-10-24
|
||||
### Fixed
|
||||
- Fix saving dynamic start time on schedule items
|
||||
|
||||
## [0.2.0-alpha] - 2021-10-23
|
||||
### Fixed
|
||||
- Fix generated streams with mpeg2video
|
||||
@@ -756,7 +776,8 @@ 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.2.0-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
|
||||
|
||||
@@ -10,5 +10,6 @@ namespace ErsatzTV.Application.Channels
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId);
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId);
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
@@ -42,9 +43,10 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
(ValidateName(request), await ValidateNumber(dbContext, request),
|
||||
await FFmpegProfileMustExist(dbContext, request),
|
||||
ValidatePreferredLanguage(request),
|
||||
await WatermarkMustExist(dbContext, request))
|
||||
await WatermarkMustExist(dbContext, request),
|
||||
await FillerPresetMustExist(dbContext, request))
|
||||
.Apply(
|
||||
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId) =>
|
||||
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
|
||||
{
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||
@@ -74,6 +76,11 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
channel.WatermarkId = id;
|
||||
}
|
||||
|
||||
foreach (int id in fillerPresetId)
|
||||
{
|
||||
channel.FallbackFillerId = id;
|
||||
}
|
||||
|
||||
return channel;
|
||||
});
|
||||
|
||||
@@ -131,5 +138,25 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
.MapT(_ => Optional(createChannel.WatermarkId))
|
||||
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
|
||||
}
|
||||
|
||||
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
|
||||
TvContext dbContext,
|
||||
CreateChannel createChannel)
|
||||
{
|
||||
if (createChannel.FallbackFillerId is null)
|
||||
{
|
||||
return Option<int>.None;
|
||||
}
|
||||
|
||||
return await dbContext.FillerPresets
|
||||
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
|
||||
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
|
||||
.Map(Optional)
|
||||
.Filter(c => c > 0)
|
||||
.MapT(_ => Optional(createChannel.FallbackFillerId))
|
||||
.Map(
|
||||
o => o.ToValidation<BaseError>(
|
||||
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
|
||||
c.StreamingMode = update.StreamingMode;
|
||||
c.WatermarkId = update.WatermarkId;
|
||||
c.FallbackFillerId = update.FallbackFillerId;
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ProjectToViewModel(c);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace ErsatzTV.Application.Channels
|
||||
GetLogo(channel),
|
||||
channel.PreferredLanguageCode,
|
||||
channel.StreamingMode,
|
||||
channel.WatermarkId);
|
||||
channel.WatermarkId,
|
||||
channel.FallbackFillerId);
|
||||
|
||||
private static string GetLogo(Channel channel) =>
|
||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace ErsatzTV.Application.Configuration.Commands
|
||||
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
||||
Optional(request.LibraryRefreshInterval)
|
||||
.Filter(lri => lri > 0)
|
||||
.Where(lri => lri > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace ErsatzTV.Application.Configuration.Commands
|
||||
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
|
||||
Optional(request.DaysToBuild)
|
||||
.Filter(days => days > 0)
|
||||
.Where(days => days > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Days to build must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||
.Filter(match => match)
|
||||
.Where(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
||||
{
|
||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||
.Filter(match => match)
|
||||
.Where(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||
}
|
||||
|
||||
@@ -115,6 +115,17 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||
}
|
||||
|
||||
if (request.Settings.GlobalFallbackFillerId is not null)
|
||||
{
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegGlobalFallbackFillerId,
|
||||
request.Settings.GlobalFallbackFillerId.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId);
|
||||
}
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegSegmenterTimeout,
|
||||
request.Settings.HlsSegmenterIdleTimeout);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
public string PreferredLanguageCode { get; set; }
|
||||
public bool SaveReports { get; set; }
|
||||
public int? GlobalWatermarkId { get; set; }
|
||||
public int? GlobalFallbackFillerId { get; set; }
|
||||
public int HlsSegmenterIdleTimeout { get; set; }
|
||||
public int WorkAheadSegmenterLimit { get; set; }
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||
Option<int> watermark =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||
Option<int> fallbackFiller =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
|
||||
Option<int> hlsSegmenterIdleTimeout =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
|
||||
Option<int> workAheadSegmenterLimit =
|
||||
@@ -49,6 +51,11 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
result.GlobalWatermarkId = watermarkId;
|
||||
}
|
||||
|
||||
foreach (int fallbackFillerId in fallbackFiller)
|
||||
{
|
||||
result.GlobalFallbackFillerId = fallbackFillerId;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace ErsatzTV.Application.HDHR.Commands
|
||||
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||
Optional(request.TunerCount)
|
||||
.Filter(tc => tc > 0)
|
||||
.Where(tc => tc > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
{
|
||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||
.Filter(match => match)
|
||||
.Where(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
{
|
||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||
.Filter(match => match)
|
||||
.Where(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
||||
{
|
||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||
.Filter(match => match)
|
||||
.Where(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
.Map(list => list.Map(c => c.Path).ToList());
|
||||
|
||||
return Optional(request.Path)
|
||||
.Filter(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
|
||||
.Where(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
|
||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
.Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList());
|
||||
|
||||
return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path))))
|
||||
.Filter(length => length == 0)
|
||||
.Where(length => length == 0)
|
||||
.Map(_ => localLibrary)
|
||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
var result2 = Optional(createCollection.Name)
|
||||
.Filter(name => !allNames.Contains(name))
|
||||
.Where(name => !allNames.Contains(name))
|
||||
.ToValidation<BaseError>("Collection name must be unique");
|
||||
|
||||
return (result1, result2).Apply((_, _) => createCollection.Name);
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
var result2 = Optional(createMultiCollection.Name)
|
||||
.Filter(name => !allNames.Contains(name))
|
||||
.Where(name => !allNames.Contains(name))
|
||||
.ToValidation<BaseError>("MultiCollection name must be unique");
|
||||
|
||||
return (result1, result2).Apply((_, _) => createMultiCollection.Name);
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
.Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
var result2 = Optional(createSmartCollection.Name)
|
||||
.Filter(name => !allNames.Contains(name))
|
||||
.Where(name => !allNames.Contains(name))
|
||||
.ToValidation<BaseError>("SmartCollection name must be unique");
|
||||
|
||||
return (result1, result2).Apply((_, _) => createSmartCollection.Name);
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
.Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
var result2 = Optional(updateMultiCollection.Name)
|
||||
.Filter(name => !allNames.Contains(name))
|
||||
.Where(name => !allNames.Contains(name))
|
||||
.ToValidation<BaseError>("MultiCollection name must be unique");
|
||||
|
||||
return (result1, result2).Apply((_, _) => updateMultiCollection.Name);
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
.CountAsync(ps => ps.Name == createProgramSchedule.Name);
|
||||
|
||||
var result2 = Optional(duplicateNameCount)
|
||||
.Filter(count => count == 0)
|
||||
.Where(count => count == 0)
|
||||
.ToValidation<BaseError>("Schedule name must be unique");
|
||||
|
||||
return (result1, result2).Apply((_, _) => createProgramSchedule.Name);
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
@@ -188,7 +188,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
@@ -207,7 +207,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
@@ -227,7 +227,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixDuration(item.StartTime.GetValueOrDefault()),
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
@@ -249,5 +249,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
|
||||
private static TimeSpan FixDuration(TimeSpan 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace ErsatzTV.Application.Streaming.Commands
|
||||
private Task<Validation<BaseError, Unit>> SessionMustBeInactive(StartFFmpegSession request)
|
||||
{
|
||||
var result = Optional(_ffmpegSegmenterService.SessionWorkers.TryAdd(request.ChannelNumber, null))
|
||||
.Filter(success => success)
|
||||
.Where(success => success)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>(new ChannelSessionAlreadyActive());
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
@@ -24,6 +28,9 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
|
||||
{
|
||||
private readonly IEmbyPathReplacementService _embyPathReplacementService;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
@@ -37,6 +44,9 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
IJellyfinPathReplacementService jellyfinPathReplacementService,
|
||||
IEmbyPathReplacementService embyPathReplacementService,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
IArtistRepository artistRepository,
|
||||
IRuntimeInfo runtimeInfo)
|
||||
: base(dbContextFactory)
|
||||
{
|
||||
@@ -45,6 +55,9 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
_plexPathReplacementService = plexPathReplacementService;
|
||||
_jellyfinPathReplacementService = jellyfinPathReplacementService;
|
||||
_embyPathReplacementService = embyPathReplacementService;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
_artistRepository = artistRepository;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
}
|
||||
|
||||
@@ -85,6 +98,11 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
|
||||
.BindT(ValidatePlayoutItemPath);
|
||||
|
||||
if (maybePlayoutItem.LeftAsEnumerable().Any(e => e is UnableToLocatePlayoutItem))
|
||||
{
|
||||
maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now);
|
||||
}
|
||||
|
||||
return await maybePlayoutItem.Match(
|
||||
async playoutItemWithPath =>
|
||||
{
|
||||
@@ -134,7 +152,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
$"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'";
|
||||
|
||||
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
|
||||
.Filter(transcode => transcode)
|
||||
.Where(transcode => transcode)
|
||||
.Match(
|
||||
_ => dbContext.PlayoutItems
|
||||
.Filter(pi => pi.Playout.ChannelId == channel.Id)
|
||||
@@ -158,8 +176,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
maybeDuration,
|
||||
"Channel is Offline",
|
||||
request.HlsRealtime);
|
||||
|
||||
|
||||
|
||||
return new PlayoutItemProcessModel(errorProcess, finish);
|
||||
}
|
||||
else
|
||||
@@ -211,6 +228,95 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> CheckForFallbackFiller(
|
||||
TvContext dbContext,
|
||||
Channel channel,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
// check for channel fallback
|
||||
Option<FillerPreset> maybeFallback = await dbContext.FillerPresets
|
||||
.SelectOneAsync(w => w.Id, w => w.Id == channel.FallbackFillerId);
|
||||
|
||||
// then check for global fallback
|
||||
if (maybeFallback.IsNone)
|
||||
{
|
||||
maybeFallback = await dbContext.ConfigElements
|
||||
.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId)
|
||||
.BindT(fillerId => dbContext.FillerPresets.SelectOneAsync(w => w.Id, w => w.Id == fillerId));
|
||||
}
|
||||
|
||||
foreach (FillerPreset fallbackPreset in maybeFallback)
|
||||
{
|
||||
// turn this into a playout item
|
||||
|
||||
var collectionKey = CollectionKey.ForFillerPreset(fallbackPreset);
|
||||
List<MediaItem> items = await MediaItemsForCollection.Collect(
|
||||
_mediaCollectionRepository,
|
||||
_televisionRepository,
|
||||
_artistRepository,
|
||||
collectionKey);
|
||||
|
||||
// TODO: shuffle? does it really matter since we loop anyway
|
||||
MediaItem item = items[new Random().Next(items.Count)];
|
||||
|
||||
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
|
||||
.Where(transcode => transcode)
|
||||
.Match(
|
||||
_ => dbContext.PlayoutItems
|
||||
.Filter(pi => pi.Playout.ChannelId == channel.Id)
|
||||
.Filter(pi => pi.Start > now.UtcDateTime)
|
||||
.OrderBy(pi => pi.Start)
|
||||
.FirstOrDefaultAsync()
|
||||
.Map(Optional)
|
||||
.MapT(pi => pi.StartOffset - now),
|
||||
() => Option<TimeSpan>.None.AsTask());
|
||||
|
||||
MediaVersion version = item switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(item))
|
||||
};
|
||||
|
||||
version.MediaFiles = await dbContext.MediaFiles
|
||||
.AsNoTracking()
|
||||
.Filter(mf => mf.MediaVersionId == version.Id)
|
||||
.ToListAsync();
|
||||
|
||||
version.Streams = await dbContext.MediaStreams
|
||||
.AsNoTracking()
|
||||
.Filter(ms => ms.MediaVersionId == version.Id)
|
||||
.ToListAsync();
|
||||
|
||||
DateTimeOffset finish = maybeDuration.Match(
|
||||
// next playout item exists
|
||||
// loop until it starts
|
||||
now.Add,
|
||||
// no next playout item exists
|
||||
// loop for 5 minutes if less than 30s, otherwise play full item
|
||||
() => version.Duration < TimeSpan.FromSeconds(30)
|
||||
? now.AddMinutes(5)
|
||||
: now.Add(version.Duration));
|
||||
|
||||
var playoutItem = new PlayoutItem
|
||||
{
|
||||
MediaItem = item,
|
||||
MediaItemId = item.Id,
|
||||
Start = now.UtcDateTime,
|
||||
Finish = finish.UtcDateTime,
|
||||
FillerKind = FillerKind.Fallback,
|
||||
InPoint = TimeSpan.Zero,
|
||||
OutPoint = version.Duration
|
||||
};
|
||||
|
||||
return await ValidatePlayoutItemPath(playoutItem);
|
||||
}
|
||||
|
||||
return new UnableToLocatePlayoutItem();
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, PlayoutItemWithPath>> ValidatePlayoutItemPath(PlayoutItem playoutItem)
|
||||
{
|
||||
string path = await GetPlayoutItemPath(playoutItem);
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.2.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.63">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -61,16 +61,19 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -105,7 +108,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
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.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -120,16 +123,19 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -165,7 +171,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
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.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -180,16 +186,19 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -235,7 +244,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -251,21 +260,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -311,7 +324,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -327,31 +340,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -397,7 +416,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
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.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -413,31 +432,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[4].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[5].GuideFinish.HasValue.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -500,7 +525,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -517,36 +542,43 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[4].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(1);
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[5].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[6].MediaItemId.Should().Be(5);
|
||||
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].GuideGroup.Should().Be(1);
|
||||
playoutItems[6].GuideGroup.Should().Be(3);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
playoutItems[6].GuideFinish.HasValue.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -72,12 +72,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(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(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
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.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -137,12 +137,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(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.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -214,27 +214,27 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(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.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
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(1);
|
||||
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(1);
|
||||
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(1);
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -306,17 +306,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(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.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
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(1);
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
@@ -369,7 +369,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
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.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -388,27 +388,27 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(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.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
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(1);
|
||||
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(1);
|
||||
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(1);
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -498,32 +498,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(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.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
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(1);
|
||||
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(1);
|
||||
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(1);
|
||||
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(1);
|
||||
playoutItems[6].GuideGroup.Should().Be(3);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
|
||||
@@ -593,7 +593,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
@@ -613,12 +613,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(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(1);
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,77 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Have_Gap_With_Empty_Tail_Empty_Fallback()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
var collectionTwo = new Collection { Id = 2, Name = "Collection 2", MediaItems = new List<MediaItem>() };
|
||||
var collectionThree = new Collection { Id = 3, Name = "Collection 3", MediaItems = new List<MediaItem>() };
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Tail,
|
||||
CollectionId = collectionTwo.Id,
|
||||
Collection = collectionTwo
|
||||
},
|
||||
FallbackFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.Fallback,
|
||||
CollectionId = collectionThree.Id,
|
||||
Collection = collectionThree
|
||||
}
|
||||
};
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
enumerator3.State.Index.Should().Be(0);
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Have_Gap_With_Exact_Tail()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
@@ -16,6 +17,8 @@ namespace ErsatzTV.Core.Domain
|
||||
public FFmpegProfile FFmpegProfile { get; set; }
|
||||
public int? WatermarkId { get; set; }
|
||||
public ChannelWatermark Watermark { get; set; }
|
||||
public int? FallbackFillerId { get; set; }
|
||||
public FillerPreset FallbackFiller { get; set; }
|
||||
public StreamingMode StreamingMode { get; set; }
|
||||
public List<Playout> Playouts { get; set; }
|
||||
public List<Artwork> Artwork { get; set; }
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
|
||||
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
|
||||
public static ConfigElementKey FFmpegGlobalWatermarkId => new("ffmpeg.global_watermark_id");
|
||||
public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id");
|
||||
public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds");
|
||||
public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit");
|
||||
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Flurl" Version="3.0.2" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
|
||||
@@ -31,5 +31,32 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
return hash.ToString();
|
||||
}
|
||||
|
||||
public static string CalculateWithSubfolders(string folder, ILocalFileSystem localFileSystem)
|
||||
{
|
||||
IEnumerable<string> allFiles = localFileSystem.ListFiles(folder);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (string file in allFiles.OrderBy(identity))
|
||||
{
|
||||
sb.Append(file);
|
||||
sb.Append(localFileSystem.GetLastWriteTime(file).Ticks);
|
||||
}
|
||||
|
||||
foreach (string subfolder in localFileSystem.ListSubdirectories(folder).OrderBy(identity))
|
||||
{
|
||||
sb.Append(subfolder);
|
||||
sb.Append(Calculate(subfolder, localFileSystem));
|
||||
}
|
||||
|
||||
var hash = new StringBuilder();
|
||||
byte[] bytes = Crypto.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()));
|
||||
foreach (byte t in bytes)
|
||||
{
|
||||
hash.Append(t.ToString("x2"));
|
||||
}
|
||||
|
||||
return hash.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showFolder).Filter(ShouldIncludeFolder)
|
||||
.OrderBy(identity))
|
||||
{
|
||||
string etag = FolderEtag.Calculate(seasonFolder, _localFileSystem);
|
||||
string etag = FolderEtag.CalculateWithSubfolders(seasonFolder, _localFileSystem);
|
||||
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
|
||||
.Filter(f => f.Path == seasonFolder)
|
||||
.HeadOrNone();
|
||||
@@ -208,10 +208,16 @@ namespace ErsatzTV.Core.Metadata
|
||||
Season season,
|
||||
string seasonPath)
|
||||
{
|
||||
foreach (string file in _localFileSystem.ListFiles(seasonPath)
|
||||
var allSeasonFiles = _localFileSystem.ListSubdirectories(seasonPath)
|
||||
.Map(_localFileSystem.ListFiles)
|
||||
.Flatten()
|
||||
.Append(_localFileSystem.ListFiles(seasonPath))
|
||||
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
|
||||
.Filter(f => !Path.GetFileName(f).StartsWith("._"))
|
||||
.OrderBy(identity))
|
||||
.OrderBy(identity)
|
||||
.ToList();
|
||||
|
||||
foreach (string file in allSeasonFiles)
|
||||
{
|
||||
// TODO: figure out how to rebuild playlists
|
||||
Either<BaseError, Episode> maybeEpisode = await _televisionRepository
|
||||
|
||||
51
ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs
Normal file
51
ErsatzTV.Core/Scheduling/MediaItemsForCollection.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public static class MediaItemsForCollection
|
||||
{
|
||||
public static async Task<List<MediaItem>> Collect(
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
IArtistRepository artistRepository,
|
||||
CollectionKey collectionKey)
|
||||
{
|
||||
switch (collectionKey.CollectionType)
|
||||
{
|
||||
case ProgramScheduleItemCollectionType.Collection:
|
||||
List<MediaItem> collectionItems =
|
||||
await mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0);
|
||||
return collectionItems;
|
||||
case ProgramScheduleItemCollectionType.TelevisionShow:
|
||||
List<Episode> showItems =
|
||||
await televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0);
|
||||
return showItems.Cast<MediaItem>().ToList();
|
||||
case ProgramScheduleItemCollectionType.TelevisionSeason:
|
||||
List<Episode> seasonItems =
|
||||
await televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0);
|
||||
return seasonItems.Cast<MediaItem>().ToList();
|
||||
case ProgramScheduleItemCollectionType.Artist:
|
||||
List<MusicVideo> artistItems =
|
||||
await artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0);
|
||||
return artistItems.Cast<MediaItem>().ToList();
|
||||
case ProgramScheduleItemCollectionType.MultiCollection:
|
||||
List<MediaItem> multiCollectionItems =
|
||||
await mediaCollectionRepository.GetMultiCollectionItems(
|
||||
collectionKey.MultiCollectionId ?? 0);
|
||||
return multiCollectionItems;
|
||||
case ProgramScheduleItemCollectionType.SmartCollection:
|
||||
List<MediaItem> smartCollectionItems =
|
||||
await mediaCollectionRepository.GetSmartCollectionItems(
|
||||
collectionKey.SmartCollectionId ?? 0);
|
||||
return smartCollectionItems;
|
||||
default:
|
||||
return new List<MediaItem>();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -237,40 +237,13 @@ namespace ErsatzTV.Core.Scheduling
|
||||
.ToList();
|
||||
|
||||
IEnumerable<Tuple<CollectionKey, List<MediaItem>>> tuples = await collectionKeys.Map(
|
||||
async collectionKey =>
|
||||
{
|
||||
switch (collectionKey.CollectionType)
|
||||
{
|
||||
case ProgramScheduleItemCollectionType.Collection:
|
||||
List<MediaItem> collectionItems =
|
||||
await _mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0);
|
||||
return Tuple(collectionKey, collectionItems);
|
||||
case ProgramScheduleItemCollectionType.TelevisionShow:
|
||||
List<Episode> showItems =
|
||||
await _televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0);
|
||||
return Tuple(collectionKey, showItems.Cast<MediaItem>().ToList());
|
||||
case ProgramScheduleItemCollectionType.TelevisionSeason:
|
||||
List<Episode> seasonItems =
|
||||
await _televisionRepository.GetSeasonItems(collectionKey.MediaItemId ?? 0);
|
||||
return Tuple(collectionKey, seasonItems.Cast<MediaItem>().ToList());
|
||||
case ProgramScheduleItemCollectionType.Artist:
|
||||
List<MusicVideo> artistItems =
|
||||
await _artistRepository.GetArtistItems(collectionKey.MediaItemId ?? 0);
|
||||
return Tuple(collectionKey, artistItems.Cast<MediaItem>().ToList());
|
||||
case ProgramScheduleItemCollectionType.MultiCollection:
|
||||
List<MediaItem> multiCollectionItems =
|
||||
await _mediaCollectionRepository.GetMultiCollectionItems(
|
||||
collectionKey.MultiCollectionId ?? 0);
|
||||
return Tuple(collectionKey, multiCollectionItems);
|
||||
case ProgramScheduleItemCollectionType.SmartCollection:
|
||||
List<MediaItem> smartCollectionItems =
|
||||
await _mediaCollectionRepository.GetSmartCollectionItems(
|
||||
collectionKey.SmartCollectionId ?? 0);
|
||||
return Tuple(collectionKey, smartCollectionItems);
|
||||
default:
|
||||
return Tuple(collectionKey, new List<MediaItem>());
|
||||
}
|
||||
}).Sequence();
|
||||
async collectionKey => Tuple(
|
||||
collectionKey,
|
||||
await MediaItemsForCollection.Collect(
|
||||
_mediaCollectionRepository,
|
||||
_televisionRepository,
|
||||
_artistRepository,
|
||||
collectionKey))).SequenceParallel();
|
||||
|
||||
return Map.createRange(tuples);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
@@ -97,7 +98,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
NextGuideGroup = nextState.IncrementGuideGroup
|
||||
};
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
@@ -131,6 +133,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
};
|
||||
}
|
||||
|
||||
nextState = nextState with { NextGuideGroup = nextState.DecrementGuideGroup };
|
||||
|
||||
foreach (DateTimeOffset nextItemStart in durationUntil)
|
||||
{
|
||||
switch (scheduleItem.TailMode)
|
||||
@@ -174,6 +178,14 @@ namespace ErsatzTV.Core.Scheduling
|
||||
}
|
||||
}
|
||||
|
||||
// clear guide finish on all but the last item
|
||||
var all = playoutItems.Filter(pi => pi.FillerKind == FillerKind.None).ToList();
|
||||
PlayoutItem last = all.OrderBy(pi => pi.FinishOffset).LastOrDefault();
|
||||
foreach (PlayoutItem item in all.Filter(pi => pi != last))
|
||||
{
|
||||
item.GuideFinish = null;
|
||||
}
|
||||
|
||||
nextState = nextState with { NextGuideGroup = nextState.IncrementGuideGroup };
|
||||
|
||||
return Tuple(nextState, playoutItems);
|
||||
|
||||
@@ -82,7 +82,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
nextState = nextState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1)
|
||||
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1),
|
||||
NextGuideGroup = nextState.IncrementGuideGroup
|
||||
};
|
||||
|
||||
contentEnumerator.MoveNext();
|
||||
@@ -97,7 +98,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
nextState = nextState with
|
||||
{
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
|
||||
MultipleRemaining = None
|
||||
MultipleRemaining = None,
|
||||
NextGuideGroup = nextState.DecrementGuideGroup
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,18 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
builder.HasMany(c => c.Artwork)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(i => i.Watermark)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.WatermarkId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.FallbackFiller)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.FallbackFillerId)
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace ErsatzTV.Infrastructure.Data
|
||||
public DbSet<MediaItem> MediaItems { get; set; }
|
||||
public DbSet<MediaVersion> MediaVersions { get; set; }
|
||||
public DbSet<MediaFile> MediaFiles { get; set; }
|
||||
public DbSet<MediaStream> MediaStreams { get; set; }
|
||||
public DbSet<Movie> Movies { get; set; }
|
||||
public DbSet<MovieMetadata> MovieMetadata { get; set; }
|
||||
public DbSet<Artist> Artists { get; set; }
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.90" />
|
||||
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00014" />
|
||||
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00014" />
|
||||
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00014" />
|
||||
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00015" />
|
||||
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00015" />
|
||||
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -22,8 +22,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Refit" Version="6.0.94" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.0.94" />
|
||||
<PackageReference Include="Refit" Version="6.1.15" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.1.15" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -34,6 +34,6 @@ namespace ErsatzTV.Infrastructure.Health
|
||||
}
|
||||
|
||||
public Task<List<HealthCheckResult>> PerformHealthChecks() =>
|
||||
_checks.Map(c => c.Check()).Sequence().Map(results => results.ToList());
|
||||
_checks.Map(c => c.Check()).SequenceParallel().Map(results => results.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
3670
ErsatzTV.Infrastructure/Migrations/20211025124549_Add_ChannelFallbackFiller.Designer.cs
generated
Normal file
3670
ErsatzTV.Infrastructure/Migrations/20211025124549_Add_ChannelFallbackFiller.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_ChannelFallbackFiller : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Channel_ChannelWatermark_WatermarkId",
|
||||
table: "Channel");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "FallbackFillerId",
|
||||
table: "Channel",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Channel_FallbackFillerId",
|
||||
table: "Channel",
|
||||
column: "FallbackFillerId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Channel_ChannelWatermark_WatermarkId",
|
||||
table: "Channel",
|
||||
column: "WatermarkId",
|
||||
principalTable: "ChannelWatermark",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Channel_FillerPreset_FallbackFillerId",
|
||||
table: "Channel",
|
||||
column: "FallbackFillerId",
|
||||
principalTable: "FillerPreset",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Channel_ChannelWatermark_WatermarkId",
|
||||
table: "Channel");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Channel_FillerPreset_FallbackFillerId",
|
||||
table: "Channel");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Channel_FallbackFillerId",
|
||||
table: "Channel");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FallbackFillerId",
|
||||
table: "Channel");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Channel_ChannelWatermark_WatermarkId",
|
||||
table: "Channel",
|
||||
column: "WatermarkId",
|
||||
principalTable: "ChannelWatermark",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int>("FFmpegProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("FallbackFillerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -220,6 +223,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("FFmpegProfileId");
|
||||
|
||||
b.HasIndex("FallbackFillerId");
|
||||
|
||||
b.HasIndex("Number")
|
||||
.IsUnique();
|
||||
|
||||
@@ -2293,9 +2298,17 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller")
|
||||
.WithMany()
|
||||
.HasForeignKey("FallbackFillerId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
|
||||
.WithMany()
|
||||
.HasForeignKey("WatermarkId");
|
||||
.HasForeignKey("WatermarkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("FallbackFiller");
|
||||
|
||||
b.Navigation("FFmpegProfile");
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.1.5" />
|
||||
<PackageReference Include="FluentValidation" Version="10.3.3" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.3" />
|
||||
<PackageReference Include="FluentValidation" Version="10.3.4" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.4" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="6.0.441" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="Markdig" Version="0.26.0" />
|
||||
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="3.0.1" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||
@@ -33,13 +33,14 @@
|
||||
<PackageReference Include="MudBlazor" Version="5.1.5" />
|
||||
<PackageReference Include="NaturalSort.Extension" Version="3.1.0" />
|
||||
<PackageReference Include="PPioli.FluentValidation.Blazor" Version="5.0.0" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.94" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="6.1.15" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SQLite" Version="5.0.0" />
|
||||
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
|
||||
<PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" />
|
||||
<PackageReference Include="System.Runtime.Handles" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
@using System.Globalization
|
||||
@using ErsatzTV.Application.Channels
|
||||
@using ErsatzTV.Application.Channels.Queries
|
||||
@using ErsatzTV.Application.Filler
|
||||
@using ErsatzTV.Application.Filler.Queries
|
||||
@using ErsatzTV.Application.Watermarks
|
||||
@using ErsatzTV.Application.Watermarks.Queries
|
||||
@using ErsatzTV.Core.Domain.Filler
|
||||
@inject NavigationManager _navigationManager
|
||||
@inject ILogger<ChannelEditor> _logger
|
||||
@inject ISnackbar _snackbar
|
||||
@@ -41,7 +44,11 @@
|
||||
<MudSelectItem Value="@profile.Id">@profile.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3" Label="Preferred Language" @bind-Value="_model.PreferredLanguageCode" For="@(() => _model.PreferredLanguageCode)">
|
||||
<MudSelect Class="mt-3"
|
||||
Label="Preferred Language"
|
||||
@bind-Value="_model.PreferredLanguageCode"
|
||||
For="@(() => _model.PreferredLanguageCode)"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((string) null)">(none)</MudSelectItem>
|
||||
@foreach (CultureInfo culture in _availableCultures)
|
||||
{
|
||||
@@ -67,13 +74,25 @@
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<MudSelect Class="mt-3" Label="Watermark" @bind-Value="_model.WatermarkId" For="@(() => _model.WatermarkId)"
|
||||
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)">
|
||||
Disabled="@(_model.StreamingMode == StreamingMode.HttpLiveStreamingDirect)"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
|
||||
@foreach (WatermarkViewModel watermark in _watermarks)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3"
|
||||
Label="Fallback Filler"
|
||||
@bind-Value="_model.FallbackFillerId"
|
||||
For="@(() => _model.FallbackFillerId)"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
|
||||
@foreach (FillerPresetViewModel fillerPreset in _fillerPresets)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@fillerPreset.Id">@fillerPreset.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@@ -97,12 +116,14 @@
|
||||
private List<FFmpegProfileViewModel> _ffmpegProfiles;
|
||||
private List<CultureInfo> _availableCultures;
|
||||
private List<WatermarkViewModel> _watermarks;
|
||||
private List<FillerPresetViewModel> _fillerPresets;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await LoadFFmpegProfiles();
|
||||
_availableCultures = await _mediator.Send(new GetAllLanguageCodes());
|
||||
await LoadWatermarks();
|
||||
await LoadFillerPresets();
|
||||
|
||||
if (Id.HasValue)
|
||||
{
|
||||
@@ -118,6 +139,7 @@
|
||||
_model.StreamingMode = channelViewModel.StreamingMode;
|
||||
_model.PreferredLanguageCode = channelViewModel.PreferredLanguageCode;
|
||||
_model.WatermarkId = channelViewModel.WatermarkId;
|
||||
_model.FallbackFillerId = channelViewModel.FallbackFillerId;
|
||||
},
|
||||
() => _navigationManager.NavigateTo("404"));
|
||||
}
|
||||
@@ -150,6 +172,10 @@
|
||||
private async Task LoadWatermarks() =>
|
||||
_watermarks = await _mediator.Send(new GetAllWatermarks());
|
||||
|
||||
private async Task LoadFillerPresets() =>
|
||||
_fillerPresets = await _mediator.Send(new GetAllFillerPresets())
|
||||
.Map(list => list.Filter(vm => vm.FillerKind == FillerKind.Fallback).ToList());
|
||||
|
||||
private async Task HandleSubmitAsync()
|
||||
{
|
||||
_messageStore.Clear();
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
@using ErsatzTV.Application.Configuration.Queries
|
||||
@using Unit = LanguageExt.Unit
|
||||
@using ErsatzTV.Application.Configuration.Commands
|
||||
@using ErsatzTV.Application.Filler
|
||||
@using ErsatzTV.Application.Filler.Queries
|
||||
@using ErsatzTV.Application.Watermarks
|
||||
@using ErsatzTV.Application.Watermarks.Queries
|
||||
@using ErsatzTV.Core.Domain.Filler
|
||||
@using ErsatzTV.Core.FFmpeg
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@inject IMediator _mediator
|
||||
@@ -51,13 +54,28 @@
|
||||
Color="Color.Primary"
|
||||
@bind-Checked="@_ffmpegSettings.SaveReports"/>
|
||||
</MudElement>
|
||||
<MudSelect Class="mt-3" Label="Global Watermark" @bind-Value="_ffmpegSettings.GlobalWatermarkId" For="@(() => _ffmpegSettings.GlobalWatermarkId)">
|
||||
<MudSelect Class="mt-3"
|
||||
Label="Global Watermark"
|
||||
@bind-Value="_ffmpegSettings.GlobalWatermarkId"
|
||||
For="@(() => _ffmpegSettings.GlobalWatermarkId)"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
|
||||
@foreach (WatermarkViewModel watermark in _watermarks)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3"
|
||||
Label="Global Fallback Filler"
|
||||
@bind-Value="_ffmpegSettings.GlobalFallbackFillerId"
|
||||
For="@(() => _ffmpegSettings.GlobalFallbackFillerId)"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="int?" Value="@((int?) null)">(none)</MudSelectItem>
|
||||
@foreach (FillerPresetViewModel fillerPreset in _fillerPresets)
|
||||
{
|
||||
<MudSelectItem T="int?" Value="@fillerPreset.Id">@fillerPreset.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudTextField T="int"
|
||||
Label="HLS Segmenter Idle Timeout"
|
||||
@@ -153,6 +171,7 @@
|
||||
private FFmpegSettingsViewModel _ffmpegSettings;
|
||||
private List<CultureInfo> _availableCultures;
|
||||
private List<WatermarkViewModel> _watermarks;
|
||||
private List<FillerPresetViewModel> _fillerPresets;
|
||||
private int _tunerCount;
|
||||
private int _libraryRefreshInterval;
|
||||
private int _playoutDaysToBuild;
|
||||
@@ -165,6 +184,8 @@
|
||||
_success = File.Exists(_ffmpegSettings.FFmpegPath) && File.Exists(_ffmpegSettings.FFprobePath);
|
||||
_availableCultures = await _mediator.Send(new GetAllLanguageCodes());
|
||||
_watermarks = await _mediator.Send(new GetAllWatermarks());
|
||||
_fillerPresets = await _mediator.Send(new GetAllFillerPresets())
|
||||
.Map(list => list.Filter(fp => fp.FillerKind == FillerKind.Fallback).ToList());
|
||||
_tunerCount = await _mediator.Send(new GetHDHRTunerCount());
|
||||
_hdhrSuccess = string.IsNullOrWhiteSpace(ValidateTunerCount(_tunerCount));
|
||||
_libraryRefreshInterval = await _mediator.Send(new GetLibraryRefreshInterval());
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace ErsatzTV.ViewModels
|
||||
public string Logo { get; set; }
|
||||
public StreamingMode StreamingMode { get; set; }
|
||||
public int? WatermarkId { get; set; }
|
||||
public int? FallbackFillerId { get; set; }
|
||||
|
||||
public UpdateChannel ToUpdate() =>
|
||||
new(
|
||||
@@ -23,7 +24,8 @@ namespace ErsatzTV.ViewModels
|
||||
Logo,
|
||||
PreferredLanguageCode,
|
||||
StreamingMode,
|
||||
WatermarkId);
|
||||
WatermarkId,
|
||||
FallbackFillerId);
|
||||
|
||||
public CreateChannel ToCreate() =>
|
||||
new(
|
||||
@@ -33,6 +35,7 @@ namespace ErsatzTV.ViewModels
|
||||
Logo,
|
||||
PreferredLanguageCode,
|
||||
StreamingMode,
|
||||
WatermarkId);
|
||||
WatermarkId,
|
||||
FallbackFillerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ The `Shows` library requires show and season subfolders. The following is a (non
|
||||
|
||||
- `Show (1999)\Season 01\Show - S01E01.mp4`
|
||||
- `Show\Season 1\Show - s1e1.mp4`
|
||||
- `Show\Season 1\Episode 1\Show - s1e1.mp4`
|
||||
|
||||
### Show NFO Metadata
|
||||
|
||||
|
||||
Reference in New Issue
Block a user