Browse Source

fix editorconfig and run code cleanup (#2324)

* fix formatting rules

* reformat ersatztv

* reformat ersatztv.application

* reformat ersatztv.core

* refactor ersatztv.core.tests

* reformat ersatztv.ffmpeg

* reformat ersatztv.ffmpeg.tests

* reformat ersatztv.infrastructure

* cleanup infra mysql

* cleanup infra sqlite

* cleanup infra tests

* cleanup ersatztv.scanner

* cleanup ersatztv.scanner.tests

* sln cleanup

* update dependencies
pull/2326/head
Jason Dove 5 months ago committed by GitHub
parent
commit
5d081ceeff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .config/dotnet-tools.json
  2. 36
      .editorconfig
  3. 2
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs
  4. 16
      ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs
  5. 2
      ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferencesHandler.cs
  6. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  7. 3
      ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs
  8. 3
      ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs
  9. 10
      ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs
  10. 4
      ErsatzTV.Application/Graphics/Mapper.cs
  11. 16
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs
  12. 2
      ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferencesHandler.cs
  13. 2
      ErsatzTV.Application/Libraries/Commands/ILocalLibraryRequest.cs
  14. 14
      ErsatzTV.Application/Libraries/Commands/QueueShowScanByLibraryIdHandler.cs
  15. 2
      ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs
  16. 6
      ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs
  17. 2
      ErsatzTV.Application/MediaCollections/Queries/GetPlaylistItemsHandler.cs
  18. 2
      ErsatzTV.Application/MediaItems/Queries/GetRemoteStreamById.cs
  19. 55
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  20. 1
      ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs
  21. 1
      ErsatzTV.Application/Playouts/Commands/CreateYamlPlayoutHandler.cs
  22. 3
      ErsatzTV.Application/Playouts/Mapper.cs
  23. 14
      ErsatzTV.Application/Playouts/Queries/CheckForOverlappingPlayoutItemsHandler.cs
  24. 16
      ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs
  25. 2
      ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs
  26. 2
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  27. 12
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  28. 8
      ErsatzTV.Application/Scheduling/BlockViewModel.cs
  29. 4
      ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs
  30. 4
      ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs
  31. 9
      ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs
  32. 15
      ErsatzTV.Application/Scheduling/Mapper.cs
  33. 2
      ErsatzTV.Application/Scheduling/Queries/GetBlockItemsHandler.cs
  34. 1
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs
  35. 1
      ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs
  36. 3
      ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreams.cs
  37. 2
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  38. 29
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  39. 13
      ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs
  40. 1
      ErsatzTV.Application/Streaming/PtsTime.cs
  41. 5
      ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs
  42. 29
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  43. 4
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  44. 2
      ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs
  45. 51
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  46. 19
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs
  47. 10
      ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs
  48. 138
      ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs
  49. 115
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs
  50. 2
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/GetStartTimeAfterTests.cs
  51. 220
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs
  52. 24
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs
  53. 18
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs
  54. 20
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ResetPlayoutTests.cs
  55. 3
      ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs
  56. 6
      ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs
  57. 37
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  58. 2
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  59. 31
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  60. 2
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  61. 5
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  62. 5
      ErsatzTV.Core/Graphics/ImageGraphicsElement.cs
  63. 7
      ErsatzTV.Core/Health/HealthCheckResult.cs
  64. 4
      ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs
  65. 4
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs
  66. 2
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs
  67. 2
      ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
  68. 4
      ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs
  69. 1
      ErsatzTV.Core/PathUtils.cs
  70. 12
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs
  71. 3
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutChangeDetection.cs
  72. 2
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs
  73. 2
      ErsatzTV.Core/Scheduling/BlockScheduling/EffectiveBlock.cs
  74. 32
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  75. 5
      ErsatzTV.Core/Scheduling/FillerExpression.cs
  76. 10
      ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs
  77. 25
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  78. 14
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  79. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  80. 3
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  81. 5
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  82. 3
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  83. 6
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs
  84. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs
  85. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOffHandler.cs
  86. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOnHandler.cs
  87. 3
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutMidRollHandler.cs
  88. 6
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWatermarkHandler.cs
  89. 54
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  90. 10
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs
  91. 15
      ErsatzTV.Core/Troubleshooting/TroubleshootingNotifier.cs
  92. 5
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs
  93. 8
      ErsatzTV.FFmpeg/Capabilities/FourCC.cs
  94. 7
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  95. 6
      ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs
  96. 8
      ErsatzTV.FFmpeg/Capabilities/VideoToolbox/VideoToolboxUtil.cs
  97. 19
      ErsatzTV.FFmpeg/Capabilities/VideoToolboxHardwareCapabilities.cs
  98. 2
      ErsatzTV.FFmpeg/Environment/CudaVisibleDevicesVariable.cs
  99. 5
      ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs
  100. 2
      ErsatzTV.FFmpeg/Filter/ComplexFilter.cs
  101. Some files were not shown because too many files have changed in this diff Show More

2
.config/dotnet-tools.json

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

36
.editorconfig

@ -1,28 +1,11 @@
[*] [*]
charset=utf-8 charset=utf-8
end_of_line=lf end_of_line=lf
trim_trailing_whitespace=true trim_trailing_whitespace=true
insert_final_newline=false insert_final_newline=true
indent_style=space indent_style=space
indent_size=4 indent_size=4
[*.json]
ij_json_array_wrapping = normal
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_keep_trailing_comma = false
ij_json_object_wrapping = normal
ij_json_property_alignment = do_not_align
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = false
ij_json_space_before_comma = false
ij_json_spaces_within_braces = true
ij_json_spaces_within_brackets = true
ij_json_wrap_long_lines = false
# Microsoft .NET properties # Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers=false csharp_new_line_before_members_in_object_initializers=false
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
@ -58,6 +41,8 @@ resharper_braces_for_for=required
resharper_braces_for_foreach=required resharper_braces_for_foreach=required
resharper_braces_for_ifelse=required resharper_braces_for_ifelse=required
resharper_braces_for_while=required resharper_braces_for_while=required
resharper_csharp_arguments_literal=positional
resharper_csharp_arguments_named=positional
resharper_csharp_insert_final_newline=true resharper_csharp_insert_final_newline=true
resharper_csharp_max_attribute_length_for_same_line=0 resharper_csharp_max_attribute_length_for_same_line=0
resharper_csharp_place_accessorholder_attribute_on_same_line=never resharper_csharp_place_accessorholder_attribute_on_same_line=never
@ -100,6 +85,21 @@ tab_width=4
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.json]
ij_json_array_wrapping = normal
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_keep_trailing_comma = false
ij_json_object_wrapping = normal
ij_json_property_alignment = do_not_align
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = false
ij_json_space_before_comma = false
ij_json_spaces_within_braces = true
ij_json_spaces_within_brackets = true
ij_json_wrap_long_lines = false
[*.cs] [*.cs]
# disable CA1848: Use the LoggerMessage delegates` # disable CA1848: Use the LoggerMessage delegates`

2
ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs

@ -27,7 +27,7 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
var result = new List<Channel>(); var result = new List<Channel>();
foreach (Channel channel in channels) foreach (Channel channel in channels)
{ {
if (channel.IsEnabled == false) if (!channel.IsEnabled)
{ {
continue; continue;
} }

16
ErsatzTV.Application/Emby/Commands/CallEmbyShowScannerHandler.cs

@ -1,3 +1,5 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries; using ErsatzTV.Application.Libraries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Errors; using ErsatzTV.Core.Errors;
@ -5,8 +7,6 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Threading.Channels;
namespace ErsatzTV.Application.Emby; namespace ErsatzTV.Application.Emby;
@ -67,16 +67,12 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeE
protected override Task<DateTimeOffset> GetLastScan( protected override Task<DateTimeOffset> GetLastScan(
TvContext dbContext, TvContext dbContext,
SynchronizeEmbyShowById request) SynchronizeEmbyShowById request) =>
{ Task.FromResult(DateTimeOffset.MinValue);
return Task.FromResult(DateTimeOffset.MinValue);
}
protected override bool ScanIsRequired( protected override bool ScanIsRequired(
DateTimeOffset lastScan, DateTimeOffset lastScan,
int libraryRefreshInterval, int libraryRefreshInterval,
SynchronizeEmbyShowById request) SynchronizeEmbyShowById request) =>
{ true;
return true;
}
} }

2
ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferencesHandler.cs

@ -23,7 +23,7 @@ public class
UpdateEmbyLibraryPreferences request, UpdateEmbyLibraryPreferences request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); var toDisable = request.Preferences.Filter(p => !p.ShouldSyncItems).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable); List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids); await _searchIndex.RemoveItems(ids);
_searchIndex.Commit(); _searchIndex.Commit();

2
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -97,7 +97,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
request.Settings.UseEmbeddedSubtitles); request.Settings.UseEmbeddedSubtitles);
// do not extract when subtitles are not used // do not extract when subtitles are not used
if (request.Settings.UseEmbeddedSubtitles == false) if (!request.Settings.UseEmbeddedSubtitles)
{ {
request.Settings.ExtractEmbeddedSubtitles = false; request.Settings.ExtractEmbeddedSubtitles = false;
} }

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

@ -39,7 +39,8 @@ public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Eit
SmartCollectionId = request.SmartCollectionId, SmartCollectionId = request.SmartCollectionId,
PlaylistId = request.PlaylistId, PlaylistId = request.PlaylistId,
Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null, Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null,
UseChaptersAsMediaItems = request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems UseChaptersAsMediaItems =
request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems
}; };
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken); await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);

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

@ -39,7 +39,8 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit
existing.SmartCollectionId = request.SmartCollectionId; existing.SmartCollectionId = request.SmartCollectionId;
existing.PlaylistId = request.PlaylistId; existing.PlaylistId = request.PlaylistId;
existing.Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null; existing.Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null;
existing.UseChaptersAsMediaItems = request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems; existing.UseChaptersAsMediaItems =
request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems;
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();

10
ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs

@ -18,10 +18,10 @@ public class RefreshGraphicsElementsHandler(
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
// cleanup existing elements // cleanup existing elements
var allExisting = await dbContext.GraphicsElements List<GraphicsElement> allExisting = await dbContext.GraphicsElements
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
foreach (var existing in allExisting.Where(e => !localFileSystem.FileExists(e.Path))) foreach (GraphicsElement existing in allExisting.Where(e => !localFileSystem.FileExists(e.Path)))
{ {
logger.LogWarning( logger.LogWarning(
"Removing graphics element that references non-existing file {File}", "Removing graphics element that references non-existing file {File}",
@ -35,7 +35,7 @@ public class RefreshGraphicsElementsHandler(
.Where(f => allExisting.All(e => e.Path != f)) .Where(f => allExisting.All(e => e.Path != f))
.ToList(); .ToList();
foreach (var path in newTextPaths) foreach (string path in newTextPaths)
{ {
logger.LogDebug("Adding new graphics element from file {File}", path); logger.LogDebug("Adding new graphics element from file {File}", path);
@ -53,7 +53,7 @@ public class RefreshGraphicsElementsHandler(
.Where(f => allExisting.All(e => e.Path != f)) .Where(f => allExisting.All(e => e.Path != f))
.ToList(); .ToList();
foreach (var path in newImagePaths) foreach (string path in newImagePaths)
{ {
logger.LogDebug("Adding new graphics element from file {File}", path); logger.LogDebug("Adding new graphics element from file {File}", path);
@ -71,7 +71,7 @@ public class RefreshGraphicsElementsHandler(
.Where(f => allExisting.All(e => e.Path != f)) .Where(f => allExisting.All(e => e.Path != f))
.ToList(); .ToList();
foreach (var path in newSubtitlePaths) foreach (string path in newSubtitlePaths)
{ {
logger.LogDebug("Adding new graphics element from file {File}", path); logger.LogDebug("Adding new graphics element from file {File}", path);

4
ErsatzTV.Application/Graphics/Mapper.cs

@ -6,12 +6,12 @@ public static class Mapper
{ {
public static GraphicsElementViewModel ProjectToViewModel(GraphicsElement graphicsElement) public static GraphicsElementViewModel ProjectToViewModel(GraphicsElement graphicsElement)
{ {
var fileName = Path.GetFileName(graphicsElement.Path); string fileName = Path.GetFileName(graphicsElement.Path);
return graphicsElement.Kind switch return graphicsElement.Kind switch
{ {
GraphicsElementKind.Text => new GraphicsElementViewModel(graphicsElement.Id, $"text/{fileName}"), GraphicsElementKind.Text => new GraphicsElementViewModel(graphicsElement.Id, $"text/{fileName}"),
GraphicsElementKind.Image => new GraphicsElementViewModel(graphicsElement.Id, $"image/{fileName}"), GraphicsElementKind.Image => new GraphicsElementViewModel(graphicsElement.Id, $"image/{fileName}"),
GraphicsElementKind.Subtitle => new GraphicsElementViewModel(graphicsElement.Id, $"subtitle/{fileName}"), GraphicsElementKind.Subtitle => new GraphicsElementViewModel(graphicsElement.Id, $"subtitle/{fileName}"),
_ => new GraphicsElementViewModel(graphicsElement.Id, graphicsElement.Path) _ => new GraphicsElementViewModel(graphicsElement.Id, graphicsElement.Path)
}; };
} }

16
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinShowScannerHandler.cs

@ -1,3 +1,5 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries; using ErsatzTV.Application.Libraries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Errors; using ErsatzTV.Core.Errors;
@ -5,8 +7,6 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Threading.Channels;
namespace ErsatzTV.Application.Jellyfin; namespace ErsatzTV.Application.Jellyfin;
@ -67,16 +67,12 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<Synchron
protected override Task<DateTimeOffset> GetLastScan( protected override Task<DateTimeOffset> GetLastScan(
TvContext dbContext, TvContext dbContext,
SynchronizeJellyfinShowById request) SynchronizeJellyfinShowById request) =>
{ Task.FromResult(DateTimeOffset.MinValue);
return Task.FromResult(DateTimeOffset.MinValue);
}
protected override bool ScanIsRequired( protected override bool ScanIsRequired(
DateTimeOffset lastScan, DateTimeOffset lastScan,
int libraryRefreshInterval, int libraryRefreshInterval,
SynchronizeJellyfinShowById request) SynchronizeJellyfinShowById request) =>
{ true;
return true;
}
} }

2
ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferencesHandler.cs

@ -23,7 +23,7 @@ public class
UpdateJellyfinLibraryPreferences request, UpdateJellyfinLibraryPreferences request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); var toDisable = request.Preferences.Filter(p => !p.ShouldSyncItems).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableJellyfinLibrarySync(toDisable); List<int> ids = await _mediaSourceRepository.DisableJellyfinLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids); await _searchIndex.RemoveItems(ids);
_searchIndex.Commit(); _searchIndex.Commit();

2
ErsatzTV.Application/Libraries/Commands/ILocalLibraryRequest.cs

@ -2,5 +2,5 @@
public interface ILocalLibraryRequest public interface ILocalLibraryRequest
{ {
public string Name { get; } string Name { get; }
} }

14
ErsatzTV.Application/Libraries/Commands/QueueShowScanByLibraryIdHandler.cs

@ -1,6 +1,7 @@
using ErsatzTV.Application.Emby; using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Jellyfin; using ErsatzTV.Application.Jellyfin;
using ErsatzTV.Application.Plex; using ErsatzTV.Application.Plex;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
@ -48,25 +49,28 @@ public class QueueShowScanByLibraryIdHandler(
return false; return false;
} }
logger.LogDebug("Queued show scan for library id {Id}, show: {ShowTitle}, deepScan: {DeepScan}", logger.LogDebug(
library.Id, request.ShowTitle, request.DeepScan); "Queued show scan for library id {Id}, show: {ShowTitle}, deepScan: {DeepScan}",
library.Id,
request.ShowTitle,
request.DeepScan);
try try
{ {
switch (library) switch (library)
{ {
case PlexLibrary: case PlexLibrary:
var plexResult = await mediator.Send( Either<BaseError, string> plexResult = await mediator.Send(
new SynchronizePlexShowById(library.Id, request.ShowId, request.DeepScan), new SynchronizePlexShowById(library.Id, request.ShowId, request.DeepScan),
cancellationToken); cancellationToken);
return plexResult.IsRight; return plexResult.IsRight;
case JellyfinLibrary: case JellyfinLibrary:
var jellyfinResult = await mediator.Send( Either<BaseError, string> jellyfinResult = await mediator.Send(
new SynchronizeJellyfinShowById(library.Id, request.ShowId, request.DeepScan), new SynchronizeJellyfinShowById(library.Id, request.ShowId, request.DeepScan),
cancellationToken); cancellationToken);
return jellyfinResult.IsRight; return jellyfinResult.IsRight;
case EmbyLibrary: case EmbyLibrary:
var embyResult = await mediator.Send( Either<BaseError, string> embyResult = await mediator.Send(
new SynchronizeEmbyShowById(library.Id, request.ShowId, request.DeepScan), new SynchronizeEmbyShowById(library.Id, request.ShowId, request.DeepScan),
cancellationToken); cancellationToken);
return embyResult.IsRight; return embyResult.IsRight;

2
ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs

@ -55,7 +55,7 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
var toRemoveIds = toRemove.Map(lp => lp.Id).ToHashSet(); var toRemoveIds = toRemove.Map(lp => lp.Id).ToHashSet();
int changeCount = 0; var changeCount = 0;
// save item ids first; will need to remove from search index // save item ids first; will need to remove from search index
List<int> itemsToRemove = await dbContext.MediaItems List<int> itemsToRemove = await dbContext.MediaItems

6
ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs

@ -55,7 +55,11 @@ public class PreviewPlaylistPlayoutHandler(
// TODO: make an explicit method to preview, this is ugly // TODO: make an explicit method to preview, this is ugly
playoutBuilder.TrimStart = false; playoutBuilder.TrimStart = false;
playoutBuilder.DebugPlaylist = playout.ProgramSchedule.Items[0].Playlist; playoutBuilder.DebugPlaylist = playout.ProgramSchedule.Items[0].Playlist;
var result = await playoutBuilder.Build(playout, referenceData, PlayoutBuildMode.Reset, cancellationToken); PlayoutBuildResult result = await playoutBuilder.Build(
playout,
referenceData,
PlayoutBuildMode.Reset,
cancellationToken);
var maxItems = 0; var maxItems = 0;
Dictionary<PlaylistItem, List<MediaItem>> map = Dictionary<PlaylistItem, List<MediaItem>> map =

2
ErsatzTV.Application/MediaCollections/Queries/GetPlaylistItemsHandler.cs

@ -57,7 +57,7 @@ public class GetPlaylistItemsHandler(IDbContextFactory<TvContext> dbContextFacto
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (allItems.All(bi => bi.IncludeInProgramGuide == false)) if (allItems.All(bi => !bi.IncludeInProgramGuide))
{ {
foreach (PlaylistItem bi in allItems) foreach (PlaylistItem bi in allItems)
{ {

2
ErsatzTV.Application/MediaItems/Queries/GetRemoteStreamById.cs

@ -1,5 +1,3 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.MediaItems; namespace ErsatzTV.Application.MediaItems;
public record GetRemoteStreamById(int RemoteStreamId) : IRequest<Option<RemoteStreamViewModel>>; public record GetRemoteStreamById(int RemoteStreamId) : IRequest<Option<RemoteStreamViewModel>>;

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

@ -13,6 +13,7 @@ using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
@ -23,10 +24,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IClient _client; private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder; private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder; private readonly IPlayoutBuilder _playoutBuilder;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IYamlPlayoutBuilder _yamlPlayoutBuilder; private readonly IYamlPlayoutBuilder _yamlPlayoutBuilder;
@ -78,9 +79,9 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
} }
// after dbcontext is closed // after dbcontext is closed
foreach (var playoutBuildResult in result.RightToSeq()) foreach (PlayoutBuildResult playoutBuildResult in result.RightToSeq())
{ {
foreach (var timeShiftTo in playoutBuildResult.TimeShiftTo) foreach (DateTimeOffset timeShiftTo in playoutBuildResult.TimeShiftTo)
{ {
await _playoutTimeShifter.TimeShift(request.PlayoutId, timeShiftTo, false); await _playoutTimeShifter.TimeShift(request.PlayoutId, timeShiftTo, false);
} }
@ -100,20 +101,28 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
Playout playout, Playout playout,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
string channelName = "[unknown]"; var channelName = "[unknown]";
try try
{ {
var referenceData = await GetReferenceData(dbContext, playout.Id, playout.ProgramSchedulePlayoutType); PlayoutReferenceData referenceData = await GetReferenceData(
var channelNumber = referenceData.Channel.Number; dbContext,
playout.Id,
playout.ProgramSchedulePlayoutType);
string channelNumber = referenceData.Channel.Number;
channelName = referenceData.Channel.Name; channelName = referenceData.Channel.Name;
var result = PlayoutBuildResult.Empty; PlayoutBuildResult result = PlayoutBuildResult.Empty;
switch (playout.ProgramSchedulePlayoutType) switch (playout.ProgramSchedulePlayoutType)
{ {
case ProgramSchedulePlayoutType.Block: case ProgramSchedulePlayoutType.Block:
result = await _blockPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken); result = await _blockPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
result = await _blockPlayoutFillerBuilder.Build(playout, referenceData, result, request.Mode, cancellationToken); result = await _blockPlayoutFillerBuilder.Build(
playout,
referenceData,
result,
request.Mode,
cancellationToken);
break; break;
case ProgramSchedulePlayoutType.Yaml: case ProgramSchedulePlayoutType.Yaml:
result = await _yamlPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken); result = await _yamlPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
@ -128,7 +137,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
break; break;
} }
int changeCount = 0; var changeCount = 0;
if (result.ClearItems) if (result.ClearItems)
{ {
@ -137,7 +146,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
foreach (var removeBefore in result.RemoveBefore) foreach (DateTimeOffset removeBefore in result.RemoveBefore)
{ {
changeCount += await dbContext.PlayoutItems changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id) .Where(pi => pi.PlayoutId == playout.Id)
@ -145,7 +154,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
} }
foreach (var removeAfter in result.RemoveAfter) foreach (DateTimeOffset removeAfter in result.RemoveAfter)
{ {
changeCount += await dbContext.PlayoutItems changeCount += await dbContext.PlayoutItems
.Where(pi => pi.PlayoutId == playout.Id) .Where(pi => pi.PlayoutId == playout.Id)
@ -163,8 +172,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
if (result.AddedItems.Count > 0) if (result.AddedItems.Count > 0)
{ {
changeCount += 1; changeCount += 1;
bool anyWatermarks = result.AddedItems.Any(i => i.PlayoutItemWatermarks is not null && i.PlayoutItemWatermarks.Count > 0); bool anyWatermarks = result.AddedItems.Any(i =>
bool anyGraphicsElements = result.AddedItems.Any(i => i.PlayoutItemGraphicsElements is not null && i.PlayoutItemGraphicsElements.Count > 0); i.PlayoutItemWatermarks is not null && i.PlayoutItemWatermarks.Count > 0);
bool anyGraphicsElements = result.AddedItems.Any(i =>
i.PlayoutItemGraphicsElements is not null && i.PlayoutItemGraphicsElements.Count > 0);
if (anyWatermarks || anyGraphicsElements) if (anyWatermarks || anyGraphicsElements)
{ {
// need to use slow ef core to also insert watermarks and graphics elements properly // need to use slow ef core to also insert watermarks and graphics elements properly
@ -254,11 +265,11 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
TvContext dbContext, TvContext dbContext,
BuildPlayout buildPlayout) BuildPlayout buildPlayout)
{ {
var maybePlayout = await dbContext.Playouts Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Anchor) .Include(p => p.Anchor)
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId); .SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId);
foreach (var playout in maybePlayout) foreach (Playout playout in maybePlayout)
{ {
switch (playout.ProgramSchedulePlayoutType) switch (playout.ProgramSchedulePlayoutType)
{ {
@ -267,7 +278,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.Collection(p => p.FillGroupIndices) .Collection(p => p.FillGroupIndices)
.LoadAsync(); .LoadAsync();
foreach (var fillGroupIndex in playout.FillGroupIndices) foreach (PlayoutScheduleItemFillGroupIndex fillGroupIndex in playout.FillGroupIndices)
{ {
await dbContext.Entry(fillGroupIndex) await dbContext.Entry(fillGroupIndex)
.Reference(fgi => fgi.EnumeratorState) .Reference(fgi => fgi.EnumeratorState)
@ -278,7 +289,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.Collection(p => p.ProgramScheduleAnchors) .Collection(p => p.ProgramScheduleAnchors)
.LoadAsync(); .LoadAsync();
foreach (var anchor in playout.ProgramScheduleAnchors) foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
{ {
await dbContext.Entry(anchor) await dbContext.Entry(anchor)
.Reference(a => a.EnumeratorState) .Reference(a => a.EnumeratorState)
@ -297,12 +308,12 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
int playoutId, int playoutId,
ProgramSchedulePlayoutType playoutType) ProgramSchedulePlayoutType playoutType)
{ {
var channel = await dbContext.Channels Channel channel = await dbContext.Channels
.AsNoTracking() .AsNoTracking()
.Where(c => c.Playouts.Any(p => p.Id == playoutId)) .Where(c => c.Playouts.Any(p => p.Id == playoutId))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
var deco = Option<Deco>.None; Option<Deco> deco = Option<Deco>.None;
List<PlayoutItem> existingItems = []; List<PlayoutItem> existingItems = [];
List<PlayoutTemplate> playoutTemplates = []; List<PlayoutTemplate> playoutTemplates = [];
@ -332,7 +343,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ToListAsync(); .ToListAsync();
} }
var programSchedule = await dbContext.ProgramSchedules ProgramSchedule programSchedule = await dbContext.ProgramSchedules
.AsNoTracking() .AsNoTracking()
.Where(ps => ps.Playouts.Any(p => p.Id == playoutId)) .Where(ps => ps.Playouts.Any(p => p.Id == playoutId))
.Include(ps => ps.Items) .Include(ps => ps.Items)
@ -354,7 +365,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
var programScheduleAlternates = await dbContext.ProgramScheduleAlternates List<ProgramScheduleAlternate> programScheduleAlternates = await dbContext.ProgramScheduleAlternates
.AsNoTracking() .AsNoTracking()
.Where(pt => pt.PlayoutId == playoutId) .Where(pt => pt.PlayoutId == playoutId)
.Include(a => a.ProgramSchedule) .Include(a => a.ProgramSchedule)
@ -384,7 +395,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.ToListAsync(); .ToListAsync();
var playoutHistory = await dbContext.PlayoutHistory List<PlayoutHistory> playoutHistory = await dbContext.PlayoutHistory
.AsNoTracking() .AsNoTracking()
.Where(h => h.PlayoutId == playoutId) .Where(h => h.PlayoutId == playoutId)
.ToListAsync(); .ToListAsync();

1
ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs

@ -33,6 +33,7 @@ public class CreateBlockPlayoutHandler(
{ {
await channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false)); await channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false));
} }
await channel.WriteAsync(new RefreshChannelList()); await channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id); return new CreatePlayoutResponse(playout.Id);
} }

1
ErsatzTV.Application/Playouts/Commands/CreateYamlPlayoutHandler.cs

@ -46,6 +46,7 @@ public class CreateYamlPlayoutHandler
{ {
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false)); await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false));
} }
await _channel.WriteAsync(new RefreshChannelList()); await _channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id); return new CreatePlayoutResponse(playout.Id);
} }

3
ErsatzTV.Application/Playouts/Mapper.cs

@ -76,7 +76,8 @@ internal static class Mapper
case Image i: case Image i:
return i.ImageMetadata.HeadOrNone().Map(im => im.Title ?? string.Empty).IfNone("[unknown image]"); return i.ImageMetadata.HeadOrNone().Map(im => im.Title ?? string.Empty).IfNone("[unknown image]");
case RemoteStream rs: case RemoteStream rs:
return rs.RemoteStreamMetadata.HeadOrNone().Map(im => im.Title ?? string.Empty).IfNone("[unknown remote stream]"); return rs.RemoteStreamMetadata.HeadOrNone().Map(im => im.Title ?? string.Empty)
.IfNone("[unknown remote stream]");
default: default:
return string.Empty; return string.Empty;
} }

14
ErsatzTV.Application/Playouts/Queries/CheckForOverlappingPlayoutItemsHandler.cs

@ -1,3 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -18,22 +19,21 @@ public class CheckForOverlappingPlayoutItemsHandler(
.AnyAsync( .AnyAsync(
a => dbContext.PlayoutItems a => dbContext.PlayoutItems
.Where(b => b.PlayoutId == a.PlayoutId) .Where(b => b.PlayoutId == a.PlayoutId)
.Any( .Any(b =>
b => a.Id < b.Id &&
a.Id < b.Id && a.Start < b.Finish &&
a.Start < b.Finish && a.Finish > b.Start),
a.Finish > b.Start),
cancellationToken); cancellationToken);
if (hasConflict) if (hasConflict)
{ {
var maybeChannel = await dbContext.Channels Option<Channel> maybeChannel = await dbContext.Channels
.AsNoTracking() .AsNoTracking()
.Where(c => c.Playouts.Any(p => p.Id == request.PlayoutId)) .Where(c => c.Playouts.Any(p => p.Id == request.PlayoutId))
.FirstOrDefaultAsync(cancellationToken) .FirstOrDefaultAsync(cancellationToken)
.Map(Optional); .Map(Optional);
foreach (var channel in maybeChannel) foreach (Channel channel in maybeChannel)
{ {
logger.LogWarning( logger.LogWarning(
"Playout for channel {ChannelName} has overlapping playout items; this may be a bug.", "Playout for channel {ChannelName} has overlapping playout items; this may be a bug.",

16
ErsatzTV.Application/Plex/Commands/CallPlexShowScannerHandler.cs

@ -1,3 +1,5 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries; using ErsatzTV.Application.Libraries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Errors; using ErsatzTV.Core.Errors;
@ -5,8 +7,6 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Threading.Channels;
namespace ErsatzTV.Application.Plex; namespace ErsatzTV.Application.Plex;
@ -67,16 +67,12 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizeP
protected override Task<DateTimeOffset> GetLastScan( protected override Task<DateTimeOffset> GetLastScan(
TvContext dbContext, TvContext dbContext,
SynchronizePlexShowById request) SynchronizePlexShowById request) =>
{ Task.FromResult(DateTimeOffset.MinValue);
return Task.FromResult(DateTimeOffset.MinValue);
}
protected override bool ScanIsRequired( protected override bool ScanIsRequired(
DateTimeOffset lastScan, DateTimeOffset lastScan,
int libraryRefreshInterval, int libraryRefreshInterval,
SynchronizePlexShowById request) SynchronizePlexShowById request) =>
{ true;
return true;
}
} }

2
ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs

@ -23,7 +23,7 @@ public class
UpdatePlexLibraryPreferences request, UpdatePlexLibraryPreferences request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); var toDisable = request.Preferences.Filter(p => !p.ShouldSyncItems).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisablePlexLibrarySync(toDisable); List<int> ids = await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids); await _searchIndex.RemoveItems(ids);
_searchIndex.Commit(); _searchIndex.Commit();

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

@ -302,7 +302,7 @@ public abstract class ProgramScheduleItemCommandBase
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}") _ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
}; };
foreach (var watermarkId in item.WatermarkIds) foreach (int watermarkId in item.WatermarkIds)
{ {
result.ProgramScheduleItemWatermarks ??= []; result.ProgramScheduleItemWatermarks ??= [];
result.ProgramScheduleItemWatermarks.Add( result.ProgramScheduleItemWatermarks.Add(

12
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -66,7 +66,8 @@ internal static class Mapper
duration.FallbackFiller != null duration.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller)
: null, : null,
duration.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(), duration.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark))
.ToList(),
duration.PreferredAudioLanguageCode, duration.PreferredAudioLanguageCode,
duration.PreferredAudioTitle, duration.PreferredAudioTitle,
duration.PreferredSubtitleLanguageCode, duration.PreferredSubtitleLanguageCode,
@ -117,7 +118,8 @@ internal static class Mapper
flood.FallbackFiller != null flood.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller)
: null, : null,
flood.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(), flood.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark))
.ToList(),
flood.PreferredAudioLanguageCode, flood.PreferredAudioLanguageCode,
flood.PreferredAudioTitle, flood.PreferredAudioTitle,
flood.PreferredSubtitleLanguageCode, flood.PreferredSubtitleLanguageCode,
@ -170,7 +172,8 @@ internal static class Mapper
multiple.FallbackFiller != null multiple.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller)
: null, : null,
multiple.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(), multiple.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark))
.ToList(),
multiple.PreferredAudioLanguageCode, multiple.PreferredAudioLanguageCode,
multiple.PreferredAudioTitle, multiple.PreferredAudioTitle,
multiple.PreferredSubtitleLanguageCode, multiple.PreferredSubtitleLanguageCode,
@ -221,7 +224,8 @@ internal static class Mapper
one.FallbackFiller != null one.FallbackFiller != null
? Filler.Mapper.ProjectToViewModel(one.FallbackFiller) ? Filler.Mapper.ProjectToViewModel(one.FallbackFiller)
: null, : null,
one.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(), one.ProgramScheduleItemWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark))
.ToList(),
one.PreferredAudioLanguageCode, one.PreferredAudioLanguageCode,
one.PreferredAudioTitle, one.PreferredAudioTitle,
one.PreferredSubtitleLanguageCode, one.PreferredSubtitleLanguageCode,

8
ErsatzTV.Application/Scheduling/BlockViewModel.cs

@ -2,4 +2,10 @@ using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Application.Scheduling; namespace ErsatzTV.Application.Scheduling;
public record BlockViewModel(int Id, int GroupId, string GroupName, string Name, int Minutes, BlockStopScheduling StopScheduling); public record BlockViewModel(
int Id,
int GroupId,
string GroupName,
string Name,
int Minutes,
BlockStopScheduling StopScheduling);

4
ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs

@ -26,7 +26,9 @@ public class CreateDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbConte
return Mapper.ProjectToViewModel(decoDecoTemplateGroup); return Mapper.ProjectToViewModel(decoDecoTemplateGroup);
} }
private static Task<Validation<BaseError, DecoTemplateGroup>> Validate(TvContext dbContext, CreateDecoTemplateGroup request) => private static Task<Validation<BaseError, DecoTemplateGroup>> Validate(
TvContext dbContext,
CreateDecoTemplateGroup request) =>
ValidateName(dbContext, request).MapT(name => new DecoTemplateGroup { Name = name, DecoTemplates = [] }); ValidateName(dbContext, request).MapT(name => new DecoTemplateGroup { Name = name, DecoTemplates = [] });
private static async Task<Validation<BaseError, string>> ValidateName( private static async Task<Validation<BaseError, string>> ValidateName(

4
ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs

@ -26,7 +26,9 @@ public class CreateTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFa
return Mapper.ProjectToViewModel(templateGroup); return Mapper.ProjectToViewModel(templateGroup);
} }
private static Task<Validation<BaseError, TemplateGroup>> Validate(TvContext dbContext, CreateTemplateGroup request) => private static Task<Validation<BaseError, TemplateGroup>> Validate(
TvContext dbContext,
CreateTemplateGroup request) =>
ValidateName(dbContext, request).MapT(name => new TemplateGroup { Name = name, Templates = [] }); ValidateName(dbContext, request).MapT(name => new TemplateGroup { Name = name, Templates = [] });
private static async Task<Validation<BaseError, string>> ValidateName( private static async Task<Validation<BaseError, string>> ValidateName(

9
ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs

@ -32,10 +32,12 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
if (request.WatermarkMode is DecoMode.Override) if (request.WatermarkMode is DecoMode.Override)
{ {
// this is different than schedule item/playout item because we have to merge watermark ids // this is different than schedule item/playout item because we have to merge watermark ids
var toAdd = request.WatermarkIds.Where(id => existing.DecoWatermarks.All(wm => wm.WatermarkId != id)); IEnumerable<int> toAdd =
var toRemove = existing.DecoWatermarks.Where(wm => !request.WatermarkIds.Contains(wm.WatermarkId)); request.WatermarkIds.Where(id => existing.DecoWatermarks.All(wm => wm.WatermarkId != id));
IEnumerable<DecoWatermark> toRemove =
existing.DecoWatermarks.Where(wm => !request.WatermarkIds.Contains(wm.WatermarkId));
existing.DecoWatermarks.RemoveAll(toRemove.Contains); existing.DecoWatermarks.RemoveAll(toRemove.Contains);
foreach (var watermarkId in toAdd) foreach (int watermarkId in toAdd)
{ {
existing.DecoWatermarks.Add( existing.DecoWatermarks.Add(
new DecoWatermark new DecoWatermark
@ -75,6 +77,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
break; break;
} }
} }
existing.DefaultFillerTrimToFit = request.DefaultFillerTrimToFit; existing.DefaultFillerTrimToFit = request.DefaultFillerTrimToFit;
// dead air fallback // dead air fallback

15
ErsatzTV.Application/Scheduling/Mapper.cs

@ -9,9 +9,10 @@ internal static class Mapper
internal static TreeViewModel ProjectToViewModel(List<DecoTemplateGroup> decoTemplateGroups) => internal static TreeViewModel ProjectToViewModel(List<DecoTemplateGroup> decoTemplateGroups) =>
new( new(
decoTemplateGroups.OrderBy(dtg => dtg.Name).Map(dtg => new TreeGroupViewModel( decoTemplateGroups.OrderBy(dtg => dtg.Name).Map(dtg => new TreeGroupViewModel(
dtg.Id, dtg.Id,
dtg.Name, dtg.Name,
dtg.DecoTemplates.OrderBy(dt => dt.Name).Map(dt => new TreeItemViewModel(dt.Id, dt.Name)).ToList())).ToList()); dtg.DecoTemplates.OrderBy(dt => dt.Name).Map(dt => new TreeItemViewModel(dt.Id, dt.Name)).ToList()))
.ToList());
internal static TreeViewModel ProjectToViewModel(List<DecoGroup> decoGroups) => internal static TreeViewModel ProjectToViewModel(List<DecoGroup> decoGroups) =>
new( new(
@ -30,9 +31,11 @@ internal static class Mapper
internal static BlockTreeViewModel ProjectToViewModel(List<BlockGroup> blockGroups) => internal static BlockTreeViewModel ProjectToViewModel(List<BlockGroup> blockGroups) =>
new( new(
blockGroups.OrderBy(bg => bg.Name).Map(bg => new BlockTreeBlockGroupViewModel( blockGroups.OrderBy(bg => bg.Name).Map(bg => new BlockTreeBlockGroupViewModel(
bg.Id, bg.Id,
bg.Name, bg.Name,
bg.Blocks.OrderBy(b => b.Name).Map(b => new BlockTreeBlockViewModel(b.Id, b.Name, b.Minutes)).ToList())).ToList()); bg.Blocks.OrderBy(b => b.Name).Map(b => new BlockTreeBlockViewModel(b.Id, b.Name, b.Minutes))
.ToList()))
.ToList());
internal static BlockGroupViewModel ProjectToViewModel(BlockGroup blockGroup) => internal static BlockGroupViewModel ProjectToViewModel(BlockGroup blockGroup) =>
new(blockGroup.Id, blockGroup.Name); new(blockGroup.Id, blockGroup.Name);

2
ErsatzTV.Application/Scheduling/Queries/GetBlockItemsHandler.cs

@ -33,7 +33,7 @@ public class GetBlockItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
.ThenInclude(am => am.Artwork) .ThenInclude(am => am.Artwork)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (allItems.All(bi => bi.IncludeInProgramGuide == false)) if (allItems.All(bi => !bi.IncludeInProgramGuide))
{ {
foreach (BlockItem bi in allItems) foreach (BlockItem bi in allItems)
{ {

1
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateTreeHandler.cs

@ -1,5 +1,4 @@
using ErsatzTV.Application.Tree; using ErsatzTV.Application.Tree;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

1
ErsatzTV.Application/Scheduling/Queries/GetTemplateTreeHandler.cs

@ -1,5 +1,4 @@
using ErsatzTV.Application.Tree; using ErsatzTV.Application.Tree;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

3
ErsatzTV.Application/Search/Queries/QuerySearchIndexRemoteStreams.cs

@ -2,4 +2,5 @@
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;
public record QuerySearchIndexRemoteStreams(string Query, int PageNumber, int PageSize) : IRequest<RemoteStreamCardResultsViewModel>; public record QuerySearchIndexRemoteStreams(string Query, int PageNumber, int PageSize)
: IRequest<RemoteStreamCardResultsViewModel>;

2
ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs

@ -21,8 +21,8 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
{ {
private readonly IClient _client; private readonly IClient _client;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IGraphicsEngine _graphicsEngine;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IGraphicsEngine _graphicsEngine;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter; private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly IHostApplicationLifetime _hostApplicationLifetime; private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;

29
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -24,9 +24,9 @@ namespace ErsatzTV.Application.Streaming;
public class HlsSessionWorker : IHlsSessionWorker public class HlsSessionWorker : IHlsSessionWorker
{ {
private static int _workAheadCount; private static int _workAheadCount;
private readonly IGraphicsEngine _graphicsEngine;
private readonly IClient _client; private readonly IClient _client;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IGraphicsEngine _graphicsEngine;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter; private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<HlsSessionWorker> _logger; private readonly ILogger<HlsSessionWorker> _logger;
@ -36,6 +36,7 @@ public class HlsSessionWorker : IHlsSessionWorker
private readonly Option<int> _targetFramerate; private readonly Option<int> _targetFramerate;
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
private string _channelNumber; private string _channelNumber;
private DateTimeOffset _channelStart;
private bool _disposedValue; private bool _disposedValue;
private bool _hasWrittenSegments; private bool _hasWrittenSegments;
private DateTimeOffset _lastAccess; private DateTimeOffset _lastAccess;
@ -44,7 +45,6 @@ public class HlsSessionWorker : IHlsSessionWorker
private HlsSessionState _state; private HlsSessionState _state;
private Timer _timer; private Timer _timer;
private DateTimeOffset _transcodedUntil; private DateTimeOffset _transcodedUntil;
private DateTimeOffset _channelStart;
public HlsSessionWorker( public HlsSessionWorker(
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
@ -152,7 +152,10 @@ public class HlsSessionWorker : IHlsSessionWorker
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public async Task Run(string channelNumber, Option<TimeSpan> idleTimeout, CancellationToken incomingCancellationToken) public async Task Run(
string channelNumber,
Option<TimeSpan> idleTimeout,
CancellationToken incomingCancellationToken)
{ {
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken); _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken);
@ -160,7 +163,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{ {
_channelNumber = channelNumber; _channelNumber = channelNumber;
foreach (var timeout in idleTimeout) foreach (TimeSpan timeout in idleTimeout)
{ {
lock (_sync) lock (_sync)
{ {
@ -181,14 +184,14 @@ public class HlsSessionWorker : IHlsSessionWorker
Touch(); Touch();
_transcodedUntil = DateTimeOffset.Now; _transcodedUntil = DateTimeOffset.Now;
PlaylistStart = _transcodedUntil; PlaylistStart = _transcodedUntil;
_channelStart = _transcodedUntil; _channelStart = _transcodedUntil;
var maybePlayoutId = await _mediator.Send( Option<int> maybePlayoutId = await _mediator.Send(
new GetPlayoutIdByChannelNumber(_channelNumber), new GetPlayoutIdByChannelNumber(_channelNumber),
cancellationToken); cancellationToken);
// time shift on-demand playout if needed // time shift on-demand playout if needed
foreach (var playoutId in maybePlayoutId) foreach (int playoutId in maybePlayoutId)
{ {
await _mediator.Send( await _mediator.Send(
new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true), new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true),
@ -205,7 +208,7 @@ public class HlsSessionWorker : IHlsSessionWorker
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
foreach (var timeout in idleTimeout) foreach (TimeSpan timeout in idleTimeout)
{ {
if (DateTimeOffset.Now - _lastAccess > timeout) if (DateTimeOffset.Now - _lastAccess > timeout)
{ {
@ -461,7 +464,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{ {
await TrimAndDelete(cancellationToken); await TrimAndDelete(cancellationToken);
var maybePipe = Option<Pipe>.None; Option<Pipe> maybePipe = Option<Pipe>.None;
var stdErrBuffer = new StringBuilder(); var stdErrBuffer = new StringBuilder();
Command process = processModel.Process; Command process = processModel.Process;
@ -472,8 +475,8 @@ public class HlsSessionWorker : IHlsSessionWorker
{ {
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var processWithPipe = process; Command processWithPipe = process;
foreach (var graphicsEngineContext in processModel.GraphicsEngineContext) foreach (GraphicsEngineContext graphicsEngineContext in processModel.GraphicsEngineContext)
{ {
var pipe = new Pipe(); var pipe = new Pipe();
maybePipe = pipe; maybePipe = pipe;
@ -505,7 +508,7 @@ public class HlsSessionWorker : IHlsSessionWorker
await linkedCts.CancelAsync(); await linkedCts.CancelAsync();
// detect the non-zero exit code and transcode the ffmpeg error message instead // detect the non-zero exit code and transcode the ffmpeg error message instead
string errorMessage = stdErrBuffer.ToString(); var errorMessage = stdErrBuffer.ToString();
if (string.IsNullOrWhiteSpace(errorMessage)) if (string.IsNullOrWhiteSpace(errorMessage))
{ {
errorMessage = $"Unknown FFMPEG error; exit code {commandResult.ExitCode}"; errorMessage = $"Unknown FFMPEG error; exit code {commandResult.ExitCode}";
@ -563,7 +566,7 @@ public class HlsSessionWorker : IHlsSessionWorker
} }
finally finally
{ {
foreach (var pipe in maybePipe) foreach (Pipe pipe in maybePipe)
{ {
await pipe.Writer.CompleteAsync(); await pipe.Writer.CompleteAsync();
} }

13
ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs

@ -28,6 +28,7 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
private readonly Option<int> _targetFramerate; private readonly Option<int> _targetFramerate;
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
private string _channelNumber; private string _channelNumber;
private DateTimeOffset _channelStart;
private bool _disposedValue; private bool _disposedValue;
private DateTimeOffset _lastAccess; private DateTimeOffset _lastAccess;
private Option<PlayoutItemProcessModel> _lastProcessModel; private Option<PlayoutItemProcessModel> _lastProcessModel;
@ -35,7 +36,6 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
private HlsSessionState _state; private HlsSessionState _state;
private Timer _timer; private Timer _timer;
private DateTimeOffset _transcodedUntil; private DateTimeOffset _transcodedUntil;
private DateTimeOffset _channelStart;
public HlsSessionWorkerV2( public HlsSessionWorkerV2(
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
@ -99,7 +99,10 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public async Task Run(string channelNumber, Option<TimeSpan> idleTimeout, CancellationToken incomingCancellationToken) public async Task Run(
string channelNumber,
Option<TimeSpan> idleTimeout,
CancellationToken incomingCancellationToken)
{ {
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken); _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken);
@ -107,7 +110,7 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
{ {
_channelNumber = channelNumber; _channelNumber = channelNumber;
foreach (var timeout in idleTimeout) foreach (TimeSpan timeout in idleTimeout)
{ {
lock (_sync) lock (_sync)
{ {
@ -130,12 +133,12 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
PlaylistStart = _transcodedUntil; PlaylistStart = _transcodedUntil;
_channelStart = _transcodedUntil; _channelStart = _transcodedUntil;
var maybePlayoutId = await _mediator.Send( Option<int> maybePlayoutId = await _mediator.Send(
new GetPlayoutIdByChannelNumber(_channelNumber), new GetPlayoutIdByChannelNumber(_channelNumber),
cancellationToken); cancellationToken);
// time shift on-demand playout if needed // time shift on-demand playout if needed
foreach (var playoutId in maybePlayoutId) foreach (int playoutId in maybePlayoutId)
{ {
await _mediator.Send( await _mediator.Send(
new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true), new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true),

1
ErsatzTV.Application/Streaming/PtsTime.cs

@ -14,6 +14,7 @@ public record PtsTime(long Value)
{ {
ptsTime += duration; ptsTime += duration;
} }
return new PtsTime(ptsTime); return new PtsTime(ptsTime);
} }
} }

5
ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs

@ -60,7 +60,10 @@ public class GetLastPtsTimeHandler : IRequestHandler<GetLastPtsTime, Either<Base
return BaseError.New($"Failed to determine last pts duration for channel {parameters.ChannelNumber}"); return BaseError.New($"Failed to determine last pts duration for channel {parameters.ChannelNumber}");
} }
private async Task<Option<PtsTime>> GetPts(RequestParameters parameters, FileInfo segment, CancellationToken cancellationToken) private async Task<Option<PtsTime>> GetPts(
RequestParameters parameters,
FileInfo segment,
CancellationToken cancellationToken)
{ {
string[] argumentList = string[] argumentList =
{ {

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

@ -290,18 +290,19 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
disableWatermarks = false; disableWatermarks = false;
playoutItemWatermarks.Clear(); playoutItemWatermarks.Clear();
playoutItemWatermarks.Add(new ChannelWatermark playoutItemWatermarks.Add(
{ new ChannelWatermark
Mode = ChannelWatermarkMode.Permanent, {
Size = WatermarkSize.Scaled, Mode = ChannelWatermarkMode.Permanent,
WidthPercent = 100, Size = WatermarkSize.Scaled,
HorizontalMarginPercent = 0, WidthPercent = 100,
VerticalMarginPercent = 0, HorizontalMarginPercent = 0,
Opacity = 100, VerticalMarginPercent = 0,
Location = WatermarkLocation.TopLeft, Opacity = 100,
ImageSource = ChannelWatermarkImageSource.Resource, Location = WatermarkLocation.TopLeft,
Image = image ImageSource = ChannelWatermarkImageSource.Resource,
}); Image = image
});
} }
} }
@ -355,7 +356,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
channel.FFmpegProfile.VaapiDevice, channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames), Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
hlsRealtime: request.HlsRealtime, hlsRealtime: request.HlsRealtime,
playoutItemWithPath.PlayoutItem.MediaItem is RemoteStream { IsLive: true } ? StreamInputKind.Live : StreamInputKind.Vod, playoutItemWithPath.PlayoutItem.MediaItem is RemoteStream { IsLive: true }
? StreamInputKind.Live
: StreamInputKind.Vod,
playoutItemWithPath.PlayoutItem.FillerKind, playoutItemWithPath.PlayoutItem.FillerKind,
inPoint, inPoint,
outPoint, outPoint,

4
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs

@ -280,7 +280,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(s => s.SubtitleKind == SubtitleKind.Embedded) .Filter(s => s.SubtitleKind == SubtitleKind.Embedded)
.Filter(s => s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && .Filter(s => s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs") s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")
.Filter(s => s.IsExtracted == false || string.IsNullOrWhiteSpace(s.Path) || .Filter(s => !s.IsExtracted || string.IsNullOrWhiteSpace(s.Path) ||
FileDoesntExist(mediaItem.Id, s)); FileDoesntExist(mediaItem.Id, s));
// find cache paths for each subtitle // find cache paths for each subtitle
@ -347,7 +347,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
{ {
foreach (string path in GetRelativeOutputPath(mediaItemId, subtitle)) foreach (string path in GetRelativeOutputPath(mediaItemId, subtitle))
{ {
return _localFileSystem.FileExists(path) == false; return !_localFileSystem.FileExists(path);
} }
return false; return false;

2
ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs

@ -9,8 +9,8 @@ namespace ErsatzTV.Application.Television;
public class GetTelevisionShowByIdHandler : IRequestHandler<GetTelevisionShowById, Option<TelevisionShowViewModel>> public class GetTelevisionShowByIdHandler : IRequestHandler<GetTelevisionShowById, Option<TelevisionShowViewModel>>
{ {
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchRepository _searchRepository; private readonly ISearchRepository _searchRepository;
public GetTelevisionShowByIdHandler( public GetTelevisionShowByIdHandler(

51
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs

@ -34,12 +34,16 @@ public class PrepareTroubleshootingPlaybackHandler(
ILogger<PrepareTroubleshootingPlaybackHandler> logger) ILogger<PrepareTroubleshootingPlaybackHandler> logger)
: IRequestHandler<PrepareTroubleshootingPlayback, Either<BaseError, PlayoutItemResult>> : IRequestHandler<PrepareTroubleshootingPlayback, Either<BaseError, PlayoutItemResult>>
{ {
public async Task<Either<BaseError, PlayoutItemResult>> Handle(PrepareTroubleshootingPlayback request, CancellationToken cancellationToken) public async Task<Either<BaseError, PlayoutItemResult>> Handle(
PrepareTroubleshootingPlayback request,
CancellationToken cancellationToken)
{ {
try try
{ {
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Tuple<MediaItem, string, string, FFmpegProfile>> validation = await Validate(dbContext, request); Validation<BaseError, Tuple<MediaItem, string, string, FFmpegProfile>> validation = await Validate(
dbContext,
request);
return await validation.Match( return await validation.Match(
tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4), tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4),
error => Task.FromResult<Either<BaseError, PlayoutItemResult>>(error.Join())); error => Task.FromResult<Either<BaseError, PlayoutItemResult>>(error.Join()));
@ -96,8 +100,8 @@ public class PrepareTroubleshootingPlaybackHandler(
List<ChannelWatermark> watermarks = []; List<ChannelWatermark> watermarks = [];
if (request.WatermarkIds.Count > 0) if (request.WatermarkIds.Count > 0)
{ {
var channelWatermarks = await dbContext.ChannelWatermarks List<ChannelWatermark> channelWatermarks = await dbContext.ChannelWatermarks
.Where(w => request.WatermarkIds.Contains(w.Id)) .Where(w => request.WatermarkIds.Contains(w.Id))
.ToListAsync(); .ToListAsync();
watermarks.AddRange(channelWatermarks); watermarks.AddRange(channelWatermarks);
@ -126,18 +130,19 @@ public class PrepareTroubleshootingPlaybackHandler(
string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png"; string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png";
watermarks.Clear(); watermarks.Clear();
watermarks.Add(new ChannelWatermark watermarks.Add(
{ new ChannelWatermark
Mode = ChannelWatermarkMode.Permanent, {
Size = WatermarkSize.Scaled, Mode = ChannelWatermarkMode.Permanent,
WidthPercent = 100, Size = WatermarkSize.Scaled,
HorizontalMarginPercent = 0, WidthPercent = 100,
VerticalMarginPercent = 0, HorizontalMarginPercent = 0,
Opacity = 100, VerticalMarginPercent = 0,
Location = WatermarkLocation.TopLeft, Opacity = 100,
ImageSource = ChannelWatermarkImageSource.Resource, Location = WatermarkLocation.TopLeft,
Image = image ImageSource = ChannelWatermarkImageSource.Resource,
}); Image = image
});
} }
} }
@ -156,7 +161,7 @@ public class PrepareTroubleshootingPlaybackHandler(
TimeSpan outPoint = duration; TimeSpan outPoint = duration;
if (!hlsRealtime) if (!hlsRealtime)
{ {
foreach (var seekSeconds in request.SeekSeconds) foreach (int seekSeconds in request.SeekSeconds)
{ {
inPoint = TimeSpan.FromSeconds(seekSeconds); inPoint = TimeSpan.FromSeconds(seekSeconds);
if (inPoint > version.Duration) if (inPoint > version.Duration)
@ -173,7 +178,7 @@ public class PrepareTroubleshootingPlaybackHandler(
} }
} }
var graphicsElements = await dbContext.GraphicsElements List<GraphicsElement> graphicsElements = await dbContext.GraphicsElements
.Where(ge => request.GraphicsElementIds.Contains(ge.Id)) .Where(ge => request.GraphicsElementIds.Contains(ge.Id))
.ToListAsync(); .ToListAsync();
@ -216,7 +221,9 @@ public class PrepareTroubleshootingPlaybackHandler(
return playoutItemResult; return playoutItemResult;
} }
private static async Task<List<Subtitle>> GetSelectedSubtitle(MediaItem mediaItem, PrepareTroubleshootingPlayback request) private static async Task<List<Subtitle>> GetSelectedSubtitle(
MediaItem mediaItem,
PrepareTroubleshootingPlayback request)
{ {
if (request.SubtitleId is not null) if (request.SubtitleId is not null)
{ {
@ -268,9 +275,8 @@ public class PrepareTroubleshootingPlaybackHandler(
private static async Task<Validation<BaseError, MediaItem>> MediaItemMustExist( private static async Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
TvContext dbContext, TvContext dbContext,
PrepareTroubleshootingPlayback request) PrepareTroubleshootingPlayback request) =>
{ await dbContext.MediaItems
return await dbContext.MediaItems
.AsNoTracking() .AsNoTracking()
.Include(mi => (mi as Episode).EpisodeMetadata) .Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Subtitles) .ThenInclude(em => em.Subtitles)
@ -325,7 +331,6 @@ public class PrepareTroubleshootingPlaybackHandler(
.Include(mi => (mi as RemoteStream).RemoteStreamMetadata) .Include(mi => (mi as RemoteStream).RemoteStreamMetadata)
.SelectOneAsync(mi => mi.Id, mi => mi.Id == request.MediaItemId) .SelectOneAsync(mi => mi.Id, mi => mi.Id == request.MediaItemId)
.Map(o => o.ToValidation<BaseError>(new UnableToLocatePlayoutItem())); .Map(o => o.ToValidation<BaseError>(new UnableToLocatePlayoutItem()));
}
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) => private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath) dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath)

19
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs

@ -70,8 +70,8 @@ public class StartTroubleshootingPlaybackHandler(
cancellationToken); cancellationToken);
} }
if (hwAccel is HardwareAccelerationKind.Vaapi || (hwAccel is HardwareAccelerationKind.Qsv && if (hwAccel is HardwareAccelerationKind.Vaapi || hwAccel is HardwareAccelerationKind.Qsv &&
runtimeInfo.IsOSPlatform(OSPlatform.Linux))) runtimeInfo.IsOSPlatform(OSPlatform.Linux))
{ {
await File.WriteAllTextAsync( await File.WriteAllTextAsync(
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "capabilities_vaapi.txt"), Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "capabilities_vaapi.txt"),
@ -87,20 +87,23 @@ public class StartTroubleshootingPlaybackHandler(
cancellationToken); cancellationToken);
} }
logger.LogDebug("ffmpeg troubleshooting arguments {FFmpegArguments}", request.PlayoutItemResult.Process.Arguments); logger.LogDebug(
"ffmpeg troubleshooting arguments {FFmpegArguments}",
request.PlayoutItemResult.Process.Arguments);
var maybePipe = Option<Pipe>.None; Option<Pipe> maybePipe = Option<Pipe>.None;
try try
{ {
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var processWithPipe = request.PlayoutItemResult.Process; Command processWithPipe = request.PlayoutItemResult.Process;
foreach (var graphicsEngineContext in request.PlayoutItemResult.GraphicsEngineContext) foreach (GraphicsEngineContext graphicsEngineContext in request.PlayoutItemResult.GraphicsEngineContext)
{ {
var pipe = new Pipe(); var pipe = new Pipe();
maybePipe = pipe; maybePipe = pipe;
processWithPipe = processWithPipe.WithStandardInputPipe(PipeSource.FromStream(pipe.Reader.AsStream())); processWithPipe =
processWithPipe.WithStandardInputPipe(PipeSource.FromStream(pipe.Reader.AsStream()));
// fire and forget graphics engine task // fire and forget graphics engine task
_ = graphicsEngine.Run( _ = graphicsEngine.Run(
@ -136,7 +139,7 @@ public class StartTroubleshootingPlaybackHandler(
} }
finally finally
{ {
foreach (var pipe in maybePipe) foreach (Pipe pipe in maybePipe)
{ {
await pipe.Writer.CompleteAsync(); await pipe.Writer.CompleteAsync();
} }

10
ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs

@ -135,23 +135,25 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
if (_runtimeInfo.IsOSPlatform(OSPlatform.OSX)) if (_runtimeInfo.IsOSPlatform(OSPlatform.OSX))
{ {
var decoders = _hardwareCapabilitiesFactory.GetVideoToolboxDecoders(); List<string> decoders = _hardwareCapabilitiesFactory.GetVideoToolboxDecoders();
videoToolboxCapabilities.AppendLine("VideoToolbox Decoders: "); videoToolboxCapabilities.AppendLine("VideoToolbox Decoders: ");
videoToolboxCapabilities.AppendLine(); videoToolboxCapabilities.AppendLine();
foreach (var decoder in decoders) foreach (string decoder in decoders)
{ {
videoToolboxCapabilities.AppendLine(CultureInfo.InvariantCulture, $"\t{decoder}"); videoToolboxCapabilities.AppendLine(CultureInfo.InvariantCulture, $"\t{decoder}");
} }
videoToolboxCapabilities.AppendLine(); videoToolboxCapabilities.AppendLine();
videoToolboxCapabilities.AppendLine(); videoToolboxCapabilities.AppendLine();
var encoders = _hardwareCapabilitiesFactory.GetVideoToolboxEncoders(); List<string> encoders = _hardwareCapabilitiesFactory.GetVideoToolboxEncoders();
videoToolboxCapabilities.AppendLine("VideoToolbox Encoders: "); videoToolboxCapabilities.AppendLine("VideoToolbox Encoders: ");
videoToolboxCapabilities.AppendLine(); videoToolboxCapabilities.AppendLine();
foreach (var encoder in encoders) foreach (string encoder in encoders)
{ {
videoToolboxCapabilities.AppendLine(CultureInfo.InvariantCulture, $"\t{encoder}"); videoToolboxCapabilities.AppendLine(CultureInfo.InvariantCulture, $"\t{encoder}");
} }
videoToolboxCapabilities.AppendLine(); videoToolboxCapabilities.AppendLine();
videoToolboxCapabilities.AppendLine(); videoToolboxCapabilities.AppendLine();
} }

138
ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs

@ -58,7 +58,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -83,7 +87,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -108,7 +116,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -139,7 +151,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -165,7 +181,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -192,7 +212,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -219,7 +243,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeFalse(); result.Subtitle.IsSome.ShouldBeFalse();
} }
@ -241,7 +269,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -269,7 +301,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -303,7 +339,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -334,7 +374,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -367,7 +411,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -481,7 +529,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -509,7 +561,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -539,7 +595,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -569,7 +629,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -596,7 +660,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -623,7 +691,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -650,7 +722,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -677,7 +753,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -703,7 +783,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -730,7 +814,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -758,7 +846,11 @@ public class CustomStreamSelectorTests
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = yaml }]),
new NullLogger<CustomStreamSelector>()); new NullLogger<CustomStreamSelector>());
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, DateTimeOffset.Now, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
DateTimeOffset.Now,
_audioVersion,
_subtitles);
result.AudioStream.IsSome.ShouldBeFalse(); result.AudioStream.IsSome.ShouldBeFalse();
result.Subtitle.IsSome.ShouldBeFalse(); result.Subtitle.IsSome.ShouldBeFalse();

115
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs

@ -28,7 +28,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); PlayoutBuildResult result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(1); result.AddedItems.Head().MediaItemId.ShouldBe(1);
@ -73,7 +80,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); PlayoutBuildResult result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(1); result.AddedItems.Head().MediaItemId.ShouldBe(1);
@ -119,7 +133,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromDays(1); DateTimeOffset finish = start + TimeSpan.FromDays(1);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); PlayoutBuildResult result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(4); result.AddedItems.Count.ShouldBe(4);
result.AddedItems.Map(i => i.MediaItemId).ToList().ShouldBe([1, 2, 1, 2]); result.AddedItems.Map(i => i.MediaItemId).ToList().ShouldBe([1, 2, 1, 2]);
@ -156,7 +177,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1);
result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start2,
finish2,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems[0].StartOffset.ShouldBe(finish); result.AddedItems[0].StartOffset.ShouldBe(finish);
@ -171,7 +199,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start3 = HoursAfterMidnight(2); DateTimeOffset start3 = HoursAfterMidnight(2);
DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1); DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1);
result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start3, finish3, CancellationToken); result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start3,
finish3,
CancellationToken);
result.AddedItems.Count.ShouldBe(0); result.AddedItems.Count.ShouldBe(0);
@ -196,8 +231,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
playout.ProgramScheduleAnchors.Count.ShouldBe(1); playout.ProgramScheduleAnchors.Count.ShouldBe(1);
@ -235,11 +276,19 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
} }
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); (PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) =
TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle);
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2); DateTimeOffset finish = start + TimeSpan.FromDays(2);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); PlayoutBuildResult result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(53); result.AddedItems.Count.ShouldBe(53);
playout.ProgramScheduleAnchors.Count.ShouldBe(2); playout.ProgramScheduleAnchors.Count.ShouldBe(2);
@ -301,7 +350,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); PlayoutBuildResult result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
playout.ProgramScheduleAnchors.Count.ShouldBe(2); playout.ProgramScheduleAnchors.Count.ShouldBe(2);
@ -346,7 +402,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2); DateTimeOffset finish = start + TimeSpan.FromDays(2);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); PlayoutBuildResult result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(53); result.AddedItems.Count.ShouldBe(53);
playout.ProgramScheduleAnchors.Count.ShouldBe(4); playout.ProgramScheduleAnchors.Count.ShouldBe(4);
@ -482,8 +545,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(32); DateTimeOffset finish = start + TimeSpan.FromHours(32);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Continue, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(5); result.AddedItems.Count.ShouldBe(5);
@ -595,8 +664,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5); DateTimeOffset finish = start + TimeSpan.FromHours(5);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Continue, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(4); result.AddedItems.Count.ShouldBe(4);
@ -707,8 +782,14 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
localFileSystem, localFileSystem,
Logger); Logger);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Continue, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Continue,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(5); result.AddedItems.Count.ShouldBe(5);

2
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/GetStartTimeAfterTests.cs

@ -1,7 +1,7 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Scheduling;
using Shouldly;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling;

220
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs

@ -46,8 +46,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(2); result.AddedItems.Head().MediaItemId.ShouldBe(2);
@ -102,8 +108,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(2); result.AddedItems.Head().MediaItemId.ShouldBe(2);
@ -158,8 +170,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(2); result.AddedItems.Head().MediaItemId.ShouldBe(2);
@ -189,8 +207,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().StartOffset.ShouldBe(start); result.AddedItems.Head().StartOffset.ShouldBe(start);
@ -219,8 +243,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().StartOffset.ShouldBe(start); result.AddedItems.Head().StartOffset.ShouldBe(start);
@ -242,8 +272,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().StartOffset.ShouldBe(start); result.AddedItems.Head().StartOffset.ShouldBe(start);
@ -266,8 +302,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(1); DateTimeOffset start = HoursAfterMidnight(1);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(2); result.AddedItems.Count.ShouldBe(2);
result.AddedItems[0].StartOffset.ShouldBe(midnight); result.AddedItems[0].StartOffset.ShouldBe(midnight);
@ -291,8 +333,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(4); DateTimeOffset finish = start + TimeSpan.FromHours(4);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(4); result.AddedItems.Count.ShouldBe(4);
result.AddedItems[0].StartOffset.ShouldBe(start); result.AddedItems[0].StartOffset.ShouldBe(start);
@ -321,8 +369,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(1); result.AddedItems.Head().MediaItemId.ShouldBe(1);
@ -367,8 +421,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(1); result.AddedItems.Count.ShouldBe(1);
result.AddedItems.Head().MediaItemId.ShouldBe(1); result.AddedItems.Head().MediaItemId.ShouldBe(1);
@ -482,8 +542,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(5); result.AddedItems.Count.ShouldBe(5);
result.AddedItems[0].StartOffset.ShouldBe(start); result.AddedItems[0].StartOffset.ShouldBe(start);
@ -582,8 +648,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(30); DateTimeOffset finish = start + TimeSpan.FromHours(30);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(28); result.AddedItems.Count.ShouldBe(28);
result.AddedItems[0].StartOffset.ShouldBe(start); result.AddedItems[0].StartOffset.ShouldBe(start);
@ -733,8 +805,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(7); DateTimeOffset finish = start + TimeSpan.FromHours(7);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
@ -843,8 +921,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(7); DateTimeOffset finish = start + TimeSpan.FromHours(7);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
@ -952,8 +1036,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(24); DateTimeOffset finish = start + TimeSpan.FromHours(24);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
@ -1062,8 +1152,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(7); result.AddedItems.Count.ShouldBe(7);
@ -1177,8 +1273,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
@ -1292,8 +1394,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5); DateTimeOffset finish = start + TimeSpan.FromHours(5);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(5); result.AddedItems.Count.ShouldBe(5);
@ -1416,8 +1524,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(12); result.AddedItems.Count.ShouldBe(12);
@ -1533,8 +1647,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(1); DateTimeOffset finish = start + TimeSpan.FromHours(1);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(2); result.AddedItems.Count.ShouldBe(2);
@ -1616,8 +1736,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
@ -1654,8 +1780,14 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromDays(2); DateTimeOffset finish = start + TimeSpan.FromDays(2);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(8); result.AddedItems.Count.ShouldBe(8);
result.AddedItems[0].MediaItemId.ShouldBe(1); result.AddedItems[0].MediaItemId.ShouldBe(1);

24
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs

@ -16,9 +16,6 @@ namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling;
public abstract class PlayoutBuilderTestBase public abstract class PlayoutBuilderTestBase
{ {
[SetUp]
public void SetUp() => CancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
protected readonly ILogger<PlayoutBuilder> Logger; protected readonly ILogger<PlayoutBuilder> Logger;
protected CancellationToken CancellationToken; protected CancellationToken CancellationToken;
@ -35,6 +32,9 @@ public abstract class PlayoutBuilderTestBase
Logger = loggerFactory.CreateLogger<PlayoutBuilder>(); Logger = loggerFactory.CreateLogger<PlayoutBuilder>();
} }
[SetUp]
public void SetUp() => CancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
protected static DateTimeOffset HoursAfterMidnight(int hours) protected static DateTimeOffset HoursAfterMidnight(int hours)
{ {
DateTimeOffset now = DateTimeOffset.Now; DateTimeOffset now = DateTimeOffset.Now;
@ -88,7 +88,14 @@ public abstract class PlayoutBuilderTestBase
FillGroupIndices = [] FillGroupIndices = []
}; };
var referenceData = new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
[],
playout.ProgramSchedule,
[],
[]);
return new TestData(builder, playout, referenceData); return new TestData(builder, playout, referenceData);
} }
@ -194,7 +201,14 @@ public abstract class PlayoutBuilderTestBase
FillGroupIndices = [] FillGroupIndices = []
}; };
var referenceData = new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
[],
playout.ProgramSchedule,
[],
[]);
return new TestData(builder, playout, referenceData); return new TestData(builder, playout, referenceData);
} }

18
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs

@ -86,7 +86,14 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
Playout = playout Playout = playout
}); });
var referenceData = new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); var referenceData = new PlayoutReferenceData(
playout.Channel,
Option<Deco>.None,
[],
[],
playout.ProgramSchedule,
[],
[]);
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>(); IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository(); var televisionRepo = new FakeTelevisionRepository();
@ -106,7 +113,14 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(24); DateTimeOffset start = HoursAfterMidnight(24);
DateTimeOffset finish = start + TimeSpan.FromDays(1); DateTimeOffset finish = start + TimeSpan.FromDays(1);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Refresh, start, finish, CancellationToken); PlayoutBuildResult result = await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Refresh,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(4); result.AddedItems.Count.ShouldBe(4);
result.AddedItems[0].MediaItemId.ShouldBe(2); result.AddedItems[0].MediaItemId.ShouldBe(2);

20
ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ResetPlayoutTests.cs

@ -26,8 +26,14 @@ public class ResetPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6); DateTimeOffset finish = start + TimeSpan.FromHours(6);
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result = await builder.Build(
PlayoutBuildMode.Reset, start, finish, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
CancellationToken);
result.AddedItems.Count.ShouldBe(6); result.AddedItems.Count.ShouldBe(6);
playout.Anchor.NextStartOffset.ShouldBe(finish); playout.Anchor.NextStartOffset.ShouldBe(finish);
@ -40,8 +46,14 @@ public class ResetPlayoutTests : PlayoutBuilderTestBase
DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
PlayoutBuildResult result2 = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildResult result2 = await builder.Build(
PlayoutBuildMode.Reset, start2, finish2, CancellationToken); playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start2,
finish2,
CancellationToken);
result2.AddedItems.Count.ShouldBe(6); result2.AddedItems.Count.ShouldBe(6);
playout.Anchor.NextStartOffset.ShouldBe(finish); playout.Anchor.NextStartOffset.ShouldBe(finish);

3
ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs

@ -64,7 +64,8 @@ public class FillerExpressionTests
var fillerPreset = new FillerPreset var fillerPreset = new FillerPreset
{ {
FillerKind = FillerKind.MidRoll, FillerKind = FillerKind.MidRoll,
Expression = "(total_progress >= 0.2 and matched_points = 0) or (total_progress >= 0.6 and matched_points = 1)" Expression =
"(total_progress >= 0.2 and matched_points = 0) or (total_progress >= 0.6 and matched_points = 1)"
}; };
List<MediaChapter> result = FillerExpression.FilterChapters(fillerPreset.Expression, chapters, playoutItem); List<MediaChapter> result = FillerExpression.FilterChapters(fillerPreset.Expression, chapters, playoutItem);

6
ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs

@ -38,7 +38,6 @@ public class MultiPartEpisodeGrouperTests
[TestCase("Episode 1 Part One", "Episode 2 (II)", "Episode 3")] [TestCase("Episode 1 Part One", "Episode 2 (II)", "Episode 3")]
[TestCase("Episode 1 (Part One)", "Episode 2 (II)", "Episode 3")] [TestCase("Episode 1 (Part One)", "Episode 2 (II)", "Episode 3")]
[TestCase("Episode 1 (1)", "Episode 2 (Part 2)", "Episode 3")] [TestCase("Episode 1 (1)", "Episode 2 (Part 2)", "Episode 3")]
public void MixedNaming_Group(string one, string two, string three) public void MixedNaming_Group(string one, string two, string three)
{ {
var mediaItems = new List<MediaItem> var mediaItems = new List<MediaItem>
@ -58,7 +57,10 @@ public class MultiPartEpisodeGrouperTests
[Test] [Test]
[TestCase("The Meddlers (Part One)", "The Meddlers (Part Two)", "The Meddlers (Part Three)")] [TestCase("The Meddlers (Part One)", "The Meddlers (Part Two)", "The Meddlers (Part Three)")]
[TestCase("The Slaves of Jedikiah, Part 1", "The Slaves of Jedikiah, Part 2", "The Slaves of Jedikiah, Part 3")] [TestCase("The Slaves of Jedikiah, Part 1", "The Slaves of Jedikiah, Part 2", "The Slaves of Jedikiah, Part 3")]
[TestCase("An Unearthly Child: An Unearthly Child (1)", "An Unearthly Child: The Cave of Skulls (2)", "An Unearthly Child: The Forest of Fear (3)")] [TestCase(
"An Unearthly Child: An Unearthly Child (1)",
"An Unearthly Child: The Cave of Skulls (2)",
"An Unearthly Child: The Forest of Fear (3)")]
[TestCase("The Savages (1)", "The Savages (2)", "The Savages (3)")] [TestCase("The Savages (1)", "The Savages (2)", "The Savages (3)")]
public void All_Grouped(string one, string two, string three) public void All_Grouped(string one, string two, string three)
{ {

37
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -130,9 +130,19 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID); Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe(); Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, PLAYOUT_ID, ProgramSchedulePlayoutType.Classic); PlayoutReferenceData referenceData = await GetReferenceData(
context,
PLAYOUT_ID,
ProgramSchedulePlayoutType.Classic);
await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, _cancellationToken); await builder.Build(
playout,
referenceData,
PlayoutBuildResult.Empty,
PlayoutBuildMode.Reset,
start,
finish,
_cancellationToken);
// TODO: would need to apply changes from build result // TODO: would need to apply changes from build result
await context.SaveChangesAsync(_cancellationToken); await context.SaveChangesAsync(_cancellationToken);
@ -144,7 +154,10 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID); Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe(); Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, PLAYOUT_ID, ProgramSchedulePlayoutType.Classic); PlayoutReferenceData referenceData = await GetReferenceData(
context,
PLAYOUT_ID,
ProgramSchedulePlayoutType.Classic);
await builder.Build( await builder.Build(
playout, playout,
@ -165,7 +178,10 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID); Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe(); Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, PLAYOUT_ID, ProgramSchedulePlayoutType.Classic); PlayoutReferenceData referenceData = await GetReferenceData(
context,
PLAYOUT_ID,
ProgramSchedulePlayoutType.Classic);
await builder.Build( await builder.Build(
playout, playout,
@ -310,7 +326,10 @@ public class ScheduleIntegrationTests
Option<Playout> maybePlayout = await GetPlayout(context, playoutId); Option<Playout> maybePlayout = await GetPlayout(context, playoutId);
Playout playout = maybePlayout.ValueUnsafe(); Playout playout = maybePlayout.ValueUnsafe();
var referenceData = await GetReferenceData(context, playoutId, ProgramSchedulePlayoutType.Classic); PlayoutReferenceData referenceData = await GetReferenceData(
context,
playoutId,
ProgramSchedulePlayoutType.Classic);
await builder.Build( await builder.Build(
playout, playout,
@ -380,7 +399,7 @@ public class ScheduleIntegrationTests
int playoutId, int playoutId,
ProgramSchedulePlayoutType playoutType) ProgramSchedulePlayoutType playoutType)
{ {
var channel = await dbContext.Channels Channel channel = await dbContext.Channels
.AsNoTracking() .AsNoTracking()
.Where(c => c.Playouts.Any(p => p.Id == playoutId)) .Where(c => c.Playouts.Any(p => p.Id == playoutId))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
@ -408,7 +427,7 @@ public class ScheduleIntegrationTests
.ToListAsync(); .ToListAsync();
} }
var programSchedule = await dbContext.ProgramSchedules ProgramSchedule programSchedule = await dbContext.ProgramSchedules
.AsNoTracking() .AsNoTracking()
.Where(ps => ps.Playouts.Any(p => p.Id == playoutId)) .Where(ps => ps.Playouts.Any(p => p.Id == playoutId))
.Include(ps => ps.Items) .Include(ps => ps.Items)
@ -430,7 +449,7 @@ public class ScheduleIntegrationTests
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
var programScheduleAlternates = await dbContext.ProgramScheduleAlternates List<ProgramScheduleAlternate> programScheduleAlternates = await dbContext.ProgramScheduleAlternates
.AsNoTracking() .AsNoTracking()
.Where(pt => pt.PlayoutId == playoutId) .Where(pt => pt.PlayoutId == playoutId)
.Include(a => a.ProgramSchedule) .Include(a => a.ProgramSchedule)
@ -460,7 +479,7 @@ public class ScheduleIntegrationTests
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.ToListAsync(); .ToListAsync();
var playoutHistory = await dbContext.PlayoutHistory List<PlayoutHistory> playoutHistory = await dbContext.PlayoutHistory
.AsNoTracking() .AsNoTracking()
.Where(h => h.PlayoutId == playoutId) .Where(h => h.PlayoutId == playoutId)
.ToListAsync(); .ToListAsync();

2
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -199,7 +199,7 @@ public class FFmpegComplexFilterBuilder
} }
}); });
if (scaleOrPad && _boxBlur == false) if (scaleOrPad && !_boxBlur)
{ {
videoFilterQueue.Add("setsar=1"); videoFilterQueue.Add("setsar=1");
} }

31
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -216,7 +216,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoPath != audioPath, // still image when paths are different videoPath != audioPath, // still image when paths are different
videoVersion.VideoScanKind == VideoScanKind.Progressive ? ScanKind.Progressive : ScanKind.Interlaced); videoVersion.VideoScanKind == VideoScanKind.Progressive ? ScanKind.Progressive : ScanKind.Interlaced);
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream }, streamInputKind); var videoInputFile = new VideoInputFile(
videoPath,
new List<VideoStream> { ffmpegVideoStream },
streamInputKind);
Option<AudioInputFile> audioInputFile = maybeAudioStream.Map(audioStream => Option<AudioInputFile> audioInputFile = maybeAudioStream.Map(audioStream =>
{ {
@ -347,7 +350,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
None, None,
None); None);
foreach (var watermark in options.Watermark) foreach (ChannelWatermark watermark in options.Watermark)
{ {
// don't allow duplicates // don't allow duplicates
watermarks.TryAdd(watermark.Id, new WatermarkElementContext(options)); watermarks.TryAdd(watermark.Id, new WatermarkElementContext(options));
@ -355,7 +358,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
} }
// load all playout item watermarks // load all playout item watermarks
foreach (var playoutItemWatermark in playoutItemWatermarks) foreach (ChannelWatermark playoutItemWatermark in playoutItemWatermarks)
{ {
WatermarkOptions options = await _ffmpegProcessService.GetWatermarkOptions( WatermarkOptions options = await _ffmpegProcessService.GetWatermarkOptions(
ffprobePath, ffprobePath,
@ -366,7 +369,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
None, None,
None); None);
foreach (var watermark in options.Watermark) foreach (ChannelWatermark watermark in options.Watermark)
{ {
// don't allow duplicates // don't allow duplicates
watermarks.TryAdd(watermark.Id, new WatermarkElementContext(options)); watermarks.TryAdd(watermark.Id, new WatermarkElementContext(options));
@ -448,13 +451,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playbackSettings.VideoTrackTimeScale, playbackSettings.VideoTrackTimeScale,
playbackSettings.Deinterlace); playbackSettings.Deinterlace);
foreach (var playoutItemGraphicsElement in graphicsElements) foreach (PlayoutItemGraphicsElement playoutItemGraphicsElement in graphicsElements)
{ {
switch (playoutItemGraphicsElement.GraphicsElement.Kind) switch (playoutItemGraphicsElement.GraphicsElement.Kind)
{ {
case GraphicsElementKind.Text: case GraphicsElementKind.Text:
{ {
var maybeElement = Option<TextGraphicsElement> maybeElement =
await TextGraphicsElement.FromFile(playoutItemGraphicsElement.GraphicsElement.Path); await TextGraphicsElement.FromFile(playoutItemGraphicsElement.GraphicsElement.Path);
if (maybeElement.IsNone) if (maybeElement.IsNone)
{ {
@ -463,7 +466,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playoutItemGraphicsElement.GraphicsElement.Path); playoutItemGraphicsElement.GraphicsElement.Path);
} }
foreach (var element in maybeElement) foreach (TextGraphicsElement element in maybeElement)
{ {
var variables = new Dictionary<string, string>(); var variables = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(playoutItemGraphicsElement.Variables)) if (!string.IsNullOrWhiteSpace(playoutItemGraphicsElement.Variables))
@ -479,7 +482,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
} }
case GraphicsElementKind.Image: case GraphicsElementKind.Image:
{ {
var maybeElement = Option<ImageGraphicsElement> maybeElement =
await ImageGraphicsElement.FromFile(playoutItemGraphicsElement.GraphicsElement.Path); await ImageGraphicsElement.FromFile(playoutItemGraphicsElement.GraphicsElement.Path);
if (maybeElement.IsNone) if (maybeElement.IsNone)
{ {
@ -488,7 +491,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playoutItemGraphicsElement.GraphicsElement.Path); playoutItemGraphicsElement.GraphicsElement.Path);
} }
foreach (var element in maybeElement) foreach (ImageGraphicsElement element in maybeElement)
{ {
graphicsElementContexts.Add(new ImageElementContext(element)); graphicsElementContexts.Add(new ImageElementContext(element));
} }
@ -497,7 +500,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
} }
case GraphicsElementKind.Subtitle: case GraphicsElementKind.Subtitle:
{ {
var maybeElement = Option<SubtitlesGraphicsElement> maybeElement =
await SubtitlesGraphicsElement.FromFile(playoutItemGraphicsElement.GraphicsElement.Path); await SubtitlesGraphicsElement.FromFile(playoutItemGraphicsElement.GraphicsElement.Path);
if (maybeElement.IsNone) if (maybeElement.IsNone)
{ {
@ -506,7 +509,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playoutItemGraphicsElement.GraphicsElement.Path); playoutItemGraphicsElement.GraphicsElement.Path);
} }
foreach (var element in maybeElement) foreach (SubtitlesGraphicsElement element in maybeElement)
{ {
var variables = new Dictionary<string, string>(); var variables = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(playoutItemGraphicsElement.Variables)) if (!string.IsNullOrWhiteSpace(playoutItemGraphicsElement.Variables))
@ -540,8 +543,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
new Resolution { Width = desiredState.ScaledSize.Width, Height = desiredState.ScaledSize.Height }, new Resolution { Width = desiredState.ScaledSize.Width, Height = desiredState.ScaledSize.Height },
channel.FFmpegProfile.Resolution, channel.FFmpegProfile.Resolution,
await playbackSettings.FrameRate.IfNoneAsync(24), await playbackSettings.FrameRate.IfNoneAsync(24),
ChannelStartTime: channelStartTime, channelStartTime,
ContentStartTime: start, start,
await playbackSettings.StreamSeek.IfNoneAsync(TimeSpan.Zero), await playbackSettings.StreamSeek.IfNoneAsync(TimeSpan.Zero),
finish - now); finish - now);
} }
@ -592,7 +595,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
pipelineAction?.Invoke(pipeline); pipelineAction?.Invoke(pipeline);
var command = GetCommand( Command command = GetCommand(
ffmpegPath, ffmpegPath,
videoInputFile, videoInputFile,
audioInputFile, audioInputFile,

2
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -114,7 +114,7 @@ public static class FFmpegPlaybackSettingsCalculator
result.VideoTrackTimeScale = 90000; result.VideoTrackTimeScale = 90000;
foreach (MediaStream stream in videoStream.Where(s => s.AttachedPic == false)) foreach (MediaStream stream in videoStream.Where(s => !s.AttachedPic))
{ {
result.VideoFormat = ffmpegProfile.VideoFormat; result.VideoFormat = ffmpegProfile.VideoFormat;
result.VideoBitrate = ffmpegProfile.VideoBitrate; result.VideoBitrate = ffmpegProfile.VideoBitrate;

5
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -153,10 +153,11 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
candidateSubtitles = candidateSubtitles.Filter(s => s.SubtitleKind is not SubtitleKind.Embedded).ToList(); candidateSubtitles = candidateSubtitles.Filter(s => s.SubtitleKind is not SubtitleKind.Embedded).ToList();
} }
foreach (Subtitle subtitle in candidateSubtitles.Filter(s => s.SubtitleKind is SubtitleKind.Embedded && !s.IsImage) foreach (Subtitle subtitle in candidateSubtitles
.Filter(s => s.SubtitleKind is SubtitleKind.Embedded && !s.IsImage)
.ToList()) .ToList())
{ {
if (subtitle.IsExtracted == false) if (!subtitle.IsExtracted)
{ {
_logger.LogDebug( _logger.LogDebug(
"Ignoring embedded subtitle with index {Index} that has not been extracted", "Ignoring embedded subtitle with index {Index} that has not been extracted",

5
ErsatzTV.Core/Graphics/ImageGraphicsElement.cs

@ -15,19 +15,24 @@ public class ImageGraphicsElement
public string OpacityExpression { get; set; } public string OpacityExpression { get; set; }
public WatermarkLocation Location { get; set; } public WatermarkLocation Location { get; set; }
[YamlMember(Alias = "horizontal_margin_percent", ApplyNamingConventions = false)] [YamlMember(Alias = "horizontal_margin_percent", ApplyNamingConventions = false)]
public double? HorizontalMarginPercent { get; set; } public double? HorizontalMarginPercent { get; set; }
[YamlMember(Alias = "vertical_margin_percent", ApplyNamingConventions = false)] [YamlMember(Alias = "vertical_margin_percent", ApplyNamingConventions = false)]
public double? VerticalMarginPercent { get; set; } public double? VerticalMarginPercent { get; set; }
[YamlMember(Alias = "location_x", ApplyNamingConventions = false)] [YamlMember(Alias = "location_x", ApplyNamingConventions = false)]
public double? LocationX { get; set; } public double? LocationX { get; set; }
[YamlMember(Alias = "location_y", ApplyNamingConventions = false)] [YamlMember(Alias = "location_y", ApplyNamingConventions = false)]
public double? LocationY { get; set; } public double? LocationY { get; set; }
[YamlMember(Alias = "z_index", ApplyNamingConventions = false)] [YamlMember(Alias = "z_index", ApplyNamingConventions = false)]
public int? ZIndex { get; set; } public int? ZIndex { get; set; }
public bool Scale { get; set; } public bool Scale { get; set; }
[YamlMember(Alias = "scale_width_percent", ApplyNamingConventions = false)] [YamlMember(Alias = "scale_width_percent", ApplyNamingConventions = false)]
public double? ScaleWidthPercent { get; set; } public double? ScaleWidthPercent { get; set; }

7
ErsatzTV.Core/Health/HealthCheckResult.cs

@ -1,3 +1,8 @@
namespace ErsatzTV.Core.Health; namespace ErsatzTV.Core.Health;
public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message, string BriefMessage, Option<HealthCheckLink> Link); public record HealthCheckResult(
string Title,
HealthCheckStatus Status,
string Message,
string BriefMessage,
Option<HealthCheckLink> Link);

4
ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs

@ -4,9 +4,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories;
public interface ITemplateDataRepository public interface ITemplateDataRepository
{ {
public Task<Option<Dictionary<string, object>>> GetMediaItemTemplateData(MediaItem mediaItem); Task<Option<Dictionary<string, object>>> GetMediaItemTemplateData(MediaItem mediaItem);
public Task<Option<Dictionary<string, object>>> GetEpgTemplateData( Task<Option<Dictionary<string, object>>> GetEpgTemplateData(
string channelNumber, string channelNumber,
DateTimeOffset time, DateTimeOffset time,
int count); int count);

4
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs

@ -5,8 +5,8 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IPlayoutBuilder public interface IPlayoutBuilder
{ {
public bool TrimStart { get; set; } bool TrimStart { get; set; }
public Playlist DebugPlaylist { get; set; } Playlist DebugPlaylist { get; set; }
Task<PlayoutBuildResult> Build( Task<PlayoutBuildResult> Build(
Playout playout, Playout playout,

2
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs

@ -2,5 +2,5 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IPlayoutTimeShifter public interface IPlayoutTimeShifter
{ {
public Task TimeShift(int playoutId, DateTimeOffset now, bool force); Task TimeShift(int playoutId, DateTimeOffset now, bool force);
} }

2
ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs

@ -9,7 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Search;
public interface ISearchIndex : IDisposable public interface ISearchIndex : IDisposable
{ {
public int Version { get; } int Version { get; }
Task<bool> IndexExists(); Task<bool> IndexExists();
Task<bool> Initialize(ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository); Task<bool> Initialize(ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository);
Task<Unit> Rebuild(ICachingSearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider); Task<Unit> Rebuild(ICachingSearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider);

4
ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs

@ -28,7 +28,9 @@ public record TextElementDataContext(TextGraphicsElement TextElement, Dictionary
public record ImageElementContext(ImageGraphicsElement ImageElement) : GraphicsElementContext; public record ImageElementContext(ImageGraphicsElement ImageElement) : GraphicsElementContext;
public record SubtitleElementDataContext(SubtitlesGraphicsElement SubtitlesElement, Dictionary<string, string> Variables) public record SubtitleElementDataContext(
SubtitlesGraphicsElement SubtitlesElement,
Dictionary<string, string> Variables)
: GraphicsElementContext, ITemplateDataContext : GraphicsElementContext, ITemplateDataContext
{ {
public int EpgEntries => SubtitlesElement.EpgEntries; public int EpgEntries => SubtitlesElement.EpgEntries;

1
ErsatzTV.Core/PathUtils.cs

@ -14,6 +14,7 @@ public static class PathUtils
{ {
builder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); builder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
} }
return builder.ToString(); return builder.ToString();
} }
} }

12
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs

@ -33,7 +33,7 @@ public class BlockPlayoutBuilder(
PlayoutBuildMode mode, PlayoutBuildMode mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = PlayoutBuildResult.Empty; PlayoutBuildResult result = PlayoutBuildResult.Empty;
logger.LogDebug( logger.LogDebug(
"Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}", "Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
@ -72,7 +72,8 @@ public class BlockPlayoutBuilder(
BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(referenceData); BlockPlayoutChangeDetection.GetPlayoutItemToBlockKeyMap(referenceData);
// remove items without a block key (shouldn't happen often, just upgrades) // remove items without a block key (shouldn't happen often, just upgrades)
foreach (var item in referenceData.ExistingItems.Where(i => i.FillerKind is not FillerKind.DecoDefault && !itemBlockKeys.ContainsKey(i))) foreach (PlayoutItem item in referenceData.ExistingItems.Where(i =>
i.FillerKind is not FillerKind.DecoDefault && !itemBlockKeys.ContainsKey(i)))
{ {
result.ItemsToRemove.Add(item.Id); result.ItemsToRemove.Add(item.Id);
} }
@ -327,9 +328,12 @@ public class BlockPlayoutBuilder(
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}"; return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
} }
private static PlayoutBuildResult CleanUpHistory(PlayoutReferenceData referenceData, DateTimeOffset start, PlayoutBuildResult result) private static PlayoutBuildResult CleanUpHistory(
PlayoutReferenceData referenceData,
DateTimeOffset start,
PlayoutBuildResult result)
{ {
var allItemsToDelete = referenceData.PlayoutHistory IEnumerable<PlayoutHistory> allItemsToDelete = referenceData.PlayoutHistory
.Append(result.AddedHistory) .Append(result.AddedHistory)
.GroupBy(h => (h.BlockId, h.Key)) .GroupBy(h => (h.BlockId, h.Key))
.SelectMany(group => group .SelectMany(group => group

3
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutChangeDetection.cs

@ -46,7 +46,8 @@ internal static class BlockPlayoutChangeDetection
{ {
foreach (PlayoutItem playoutItem in playoutItems) foreach (PlayoutItem playoutItem in playoutItems)
{ {
if (!itemBlockKeys.TryGetValue(playoutItem, out var blockKey) || effectiveBlock.Block.Id != blockKey.b) if (!itemBlockKeys.TryGetValue(playoutItem, out BlockKey blockKey) ||
effectiveBlock.Block.Id != blockKey.b)
{ {
continue; continue;
} }

2
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutFillerBuilder.cs

@ -37,7 +37,7 @@ public class BlockPlayoutFillerBuilder(
{ {
// remove all playout items with type filler // remove all playout items with type filler
// except block items that are hidden from the guide (guide mode) // except block items that are hidden from the guide (guide mode)
foreach (var item in filteredExistingItems) foreach (PlayoutItem item in filteredExistingItems)
{ {
if (item.FillerKind is FillerKind.None or FillerKind.GuideMode) if (item.FillerKind is FillerKind.None or FillerKind.GuideMode)
{ {

2
ErsatzTV.Core/Scheduling/BlockScheduling/EffectiveBlock.cs

@ -63,7 +63,7 @@ internal record EffectiveBlock(Block Block, BlockKey BlockKey, DateTimeOffset St
private static EffectiveBlock NormalizeGuideMode(EffectiveBlock effectiveBlock) private static EffectiveBlock NormalizeGuideMode(EffectiveBlock effectiveBlock)
{ {
if (effectiveBlock.Block.Items is not null && if (effectiveBlock.Block.Items is not null &&
effectiveBlock.Block.Items.All(bi => bi.IncludeInProgramGuide == false)) effectiveBlock.Block.Items.All(bi => !bi.IncludeInProgramGuide))
{ {
foreach (BlockItem blockItem in effectiveBlock.Block.Items) foreach (BlockItem blockItem in effectiveBlock.Block.Items)
{ {

32
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -6,9 +6,9 @@ namespace ErsatzTV.Core.Scheduling;
public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnumerator public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnumerator
{ {
private readonly Lazy<Dictionary<int, int>> _lazyMediaItemGroupSize;
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration; private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
private readonly List<MediaItem> _sortedMediaItems; private readonly List<MediaItem> _sortedMediaItems;
private readonly Lazy<Dictionary<int, int>> _lazyMediaItemGroupSize;
public ChronologicalMediaCollectionEnumerator( public ChronologicalMediaCollectionEnumerator(
IEnumerable<MediaItem> mediaItems, IEnumerable<MediaItem> mediaItems,
@ -36,6 +36,21 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
} }
} }
public void ResetState(CollectionEnumeratorState state) =>
// seed doesn't matter in chronological
State.Index = state.Index;
public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _sortedMediaItems.Count;
private Dictionary<int, int> CalculateMediaItemGroupSizes() private Dictionary<int, int> CalculateMediaItemGroupSizes()
{ {
var result = new Dictionary<int, int>(); var result = new Dictionary<int, int>();
@ -54,21 +69,6 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu
return result; return result;
} }
public void ResetState(CollectionEnumeratorState state) =>
// seed doesn't matter in chronological
State.Index = state.Index;
public CollectionEnumeratorState State { get; }
public Option<MediaItem> Current => _sortedMediaItems.Count != 0 ? _sortedMediaItems[State.Index] : None;
public Option<bool> CurrentIncludeInProgramGuide { get; }
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _sortedMediaItems.Count;
public int GroupSizeForMediaItem(MediaItem mediaItem) => public int GroupSizeForMediaItem(MediaItem mediaItem) =>
_lazyMediaItemGroupSize.Value.GetValueOrDefault(mediaItem.Id, 1); _lazyMediaItemGroupSize.Value.GetValueOrDefault(mediaItem.Id, 1);
} }

5
ErsatzTV.Core/Scheduling/FillerExpression.cs

@ -5,7 +5,10 @@ namespace ErsatzTV.Core.Scheduling;
public static class FillerExpression public static class FillerExpression
{ {
public static List<MediaChapter> FilterChapters(string fillerExpression, List<MediaChapter> effectiveChapters, PlayoutItem playoutItem) public static List<MediaChapter> FilterChapters(
string fillerExpression,
List<MediaChapter> effectiveChapters,
PlayoutItem playoutItem)
{ {
if (effectiveChapters.Count == 0 || string.IsNullOrWhiteSpace(fillerExpression)) if (effectiveChapters.Count == 0 || string.IsNullOrWhiteSpace(fillerExpression))
{ {

10
ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs

@ -14,5 +14,13 @@ public record PlayoutBuildResult(
Option<DateTimeOffset> TimeShiftTo) Option<DateTimeOffset> TimeShiftTo)
{ {
public static PlayoutBuildResult Empty => public static PlayoutBuildResult Empty =>
new(false, Option<DateTimeOffset>.None, Option<DateTimeOffset>.None, [], [], [], [], Option<DateTimeOffset>.None); new(
false,
Option<DateTimeOffset>.None,
Option<DateTimeOffset>.None,
[],
[],
[],
[],
Option<DateTimeOffset>.None);
} }

25
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -65,7 +65,7 @@ public class PlayoutBuilder : IPlayoutBuilder
PlayoutBuildMode mode, PlayoutBuildMode mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = PlayoutBuildResult.Empty; PlayoutBuildResult result = PlayoutBuildResult.Empty;
if (playout.ProgramSchedulePlayoutType is not ProgramSchedulePlayoutType.Classic) if (playout.ProgramSchedulePlayoutType is not ProgramSchedulePlayoutType.Classic)
{ {
@ -117,7 +117,13 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
foreach (PlayoutParameters parameters in await Validate(playout, referenceData)) foreach (PlayoutParameters parameters in await Validate(playout, referenceData))
{ {
result = await Build(playout, referenceData, result, mode, parameters with { Start = start, Finish = finish }, cancellationToken); result = await Build(
playout,
referenceData,
result,
mode,
parameters with { Start = start, Finish = finish },
cancellationToken);
} }
return result; return result;
@ -443,7 +449,7 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
// check for future items that aren't grouped inside range // check for future items that aren't grouped inside range
var futureItems = result.AddedItems.Filter(i => i.StartOffset > trimAfter).ToList(); var futureItems = result.AddedItems.Filter(i => i.StartOffset > trimAfter).ToList();
var futureItemCount = futureItems.Count(futureItem => int futureItemCount = futureItems.Count(futureItem =>
result.AddedItems.All(i => i == futureItem || i.GuideGroup != futureItem.GuideGroup)); result.AddedItems.All(i => i == futureItem || i.GuideGroup != futureItem.GuideGroup));
// it feels hacky to have to clean up a playlist like this, // it feels hacky to have to clean up a playlist like this,
@ -808,7 +814,8 @@ public class PlayoutBuilder : IPlayoutBuilder
playoutBuilderState.CurrentTime); playoutBuilderState.CurrentTime);
// if we ended in a different alternate schedule, fix the anchor data // if we ended in a different alternate schedule, fix the anchor data
if (playoutBuilderState.CurrentTime > playoutFinish && activeScheduleAtAnchor.Id != activeSchedule.Id && activeScheduleAtAnchor.Items.Count > 0) if (playoutBuilderState.CurrentTime > playoutFinish && activeScheduleAtAnchor.Id != activeSchedule.Id &&
activeScheduleAtAnchor.Items.Count > 0)
{ {
PlayoutBuilderState cleanState = playoutBuilderState with PlayoutBuilderState cleanState = playoutBuilderState with
{ {
@ -875,7 +882,8 @@ public class PlayoutBuilder : IPlayoutBuilder
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(PlayoutReferenceData referenceData) private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(PlayoutReferenceData referenceData)
{ {
IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> collectionKeys = GetAllCollectionKeys(referenceData); IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> collectionKeys =
GetAllCollectionKeys(referenceData);
IEnumerable<Task<KeyValuePair<CollectionKey, List<MediaItem>>>> tasks = collectionKeys.Select(async key => IEnumerable<Task<KeyValuePair<CollectionKey, List<MediaItem>>>> tasks = collectionKeys.Select(async key =>
{ {
@ -886,14 +894,13 @@ public class PlayoutBuilder : IPlayoutBuilder
return Map.createRange(await Task.WhenAll(tasks)); return Map.createRange(await Task.WhenAll(tasks));
} }
private static IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> GetAllCollectionKeys(PlayoutReferenceData referenceData) private static IEnumerable<KeyValuePair<CollectionKey, Option<FillerPreset>>> GetAllCollectionKeys(
{ PlayoutReferenceData referenceData) =>
return referenceData.ProgramSchedule.Items referenceData.ProgramSchedule.Items
.Append(referenceData.ProgramScheduleAlternates.Bind(psa => psa.ProgramSchedule.Items)) .Append(referenceData.ProgramScheduleAlternates.Bind(psa => psa.ProgramSchedule.Items))
.DistinctBy(item => item.Id) .DistinctBy(item => item.Id)
.SelectMany(CollectionKeysForItem) .SelectMany(CollectionKeysForItem)
.DistinctBy(kvp => kvp.Key); .DistinctBy(kvp => kvp.Key);
}
private async Task<List<MediaItem>> FetchMediaItemsForKeyAsync( private async Task<List<MediaItem>> FetchMediaItemsForKeyAsync(
CollectionKey collectionKey, CollectionKey collectionKey,

14
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -286,7 +286,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
// missing pad-to-nearest-minute value is invalid; use no filler // missing pad-to-nearest-minute value is invalid; use no filler
FillerPreset invalidPadFiller = allFiller FillerPreset invalidPadFiller = allFiller
.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue == false); .FirstOrDefault(f => f.FillerMode == FillerMode.Pad && !f.PadToNearestMinute.HasValue);
if (invalidPadFiller is not null) if (invalidPadFiller is not null)
{ {
Logger.LogError( Logger.LogError(
@ -340,7 +340,8 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
// convert playlist filler // convert playlist filler
if (allFiller.Any(f => f.CollectionType is ProgramScheduleItemCollectionType.Playlist)) if (allFiller.Any(f => f.CollectionType is ProgramScheduleItemCollectionType.Playlist))
{ {
var toRemove = allFiller.Filter(f => f.CollectionType is ProgramScheduleItemCollectionType.Playlist).ToList(); var toRemove = allFiller.Filter(f => f.CollectionType is ProgramScheduleItemCollectionType.Playlist)
.ToList();
allFiller.RemoveAll(toRemove.Contains); allFiller.RemoveAll(toRemove.Contains);
foreach (FillerPreset playlistFiller in toRemove) foreach (FillerPreset playlistFiller in toRemove)
@ -367,7 +368,8 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
}; };
// if filler count is 2, we need to schedule 2 * (number of items in one full playlist iteration) // if filler count is 2, we need to schedule 2 * (number of items in one full playlist iteration)
var fillerEnumerator = enumerators[CollectionKey.ForFillerPreset(playlistFiller)]; IMediaCollectionEnumerator fillerEnumerator =
enumerators[CollectionKey.ForFillerPreset(playlistFiller)];
if (fillerEnumerator is PlaylistEnumerator playlistEnumerator) if (fillerEnumerator is PlaylistEnumerator playlistEnumerator)
{ {
clone.Count *= playlistEnumerator.CountForFiller; clone.Count *= playlistEnumerator.CountForFiller;
@ -428,7 +430,10 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
foreach (FillerPreset filler in allFiller.Filter(f => foreach (FillerPreset filler in allFiller.Filter(f =>
f.FillerKind == FillerKind.MidRoll && f.FillerMode != FillerMode.Pad)) f.FillerKind == FillerKind.MidRoll && f.FillerMode != FillerMode.Pad))
{ {
List<MediaChapter> filteredChapters = FillerExpression.FilterChapters(filler.Expression, effectiveChapters, playoutItem); List<MediaChapter> filteredChapters = FillerExpression.FilterChapters(
filler.Expression,
effectiveChapters,
playoutItem);
if (filteredChapters.Count <= 1) if (filteredChapters.Count <= 1)
{ {
result.Add(playoutItem); result.Add(playoutItem);
@ -797,7 +802,6 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
if (remainingToFill - itemDuration >= TimeSpan.Zero) if (remainingToFill - itemDuration >= TimeSpan.Zero)
{ {
var playoutItem = new PlayoutItem var playoutItem = new PlayoutItem
{ {
PlayoutId = playoutBuilderState.PlayoutId, PlayoutId = playoutBuilderState.PlayoutId,

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -55,7 +55,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
if (itemStartTime >= nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc) || if (itemStartTime >= nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc) ||
// don't start if the first item will already be after the hard stop // don't start if the first item will already be after the hard stop
(playoutItems.Count == 0 && itemStartTime >= hardStop)) playoutItems.Count == 0 && itemStartTime >= hardStop)
{ {
nextState = nextState with { CurrentTime = hardStop }; nextState = nextState with { CurrentTime = hardStop };
break; break;
@ -165,7 +165,8 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
PlayoutItemWatermarks = [] PlayoutItemWatermarks = []
}; };
foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? []) foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.PlayoutItemWatermarks.Add( playoutItem.PlayoutItemWatermarks.Add(
new PlayoutItemWatermark new PlayoutItemWatermark

3
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -84,7 +84,8 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
PlayoutItemWatermarks = [] PlayoutItemWatermarks = []
}; };
foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? []) foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.PlayoutItemWatermarks.Add( playoutItem.PlayoutItemWatermarks.Add(
new PlayoutItemWatermark new PlayoutItemWatermark

5
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -60,6 +60,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
.Enumerator.Count .Enumerator.Count
}; };
} }
break; break;
case MultipleMode.MultiEpisodeGroupSize: case MultipleMode.MultiEpisodeGroupSize:
if (contentEnumerator is ChronologicalMediaCollectionEnumerator chronologicalEnumerator) if (contentEnumerator is ChronologicalMediaCollectionEnumerator chronologicalEnumerator)
@ -72,6 +73,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
}; };
} }
} }
break; break;
} }
} }
@ -108,7 +110,8 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
PlayoutItemWatermarks = [] PlayoutItemWatermarks = []
}; };
foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? []) foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.PlayoutItemWatermarks.Add( playoutItem.PlayoutItemWatermarks.Add(
new PlayoutItemWatermark new PlayoutItemWatermark

3
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -55,7 +55,8 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
PlayoutItemWatermarks = [] PlayoutItemWatermarks = []
}; };
foreach (var programScheduleItemWatermark in scheduleItem.ProgramScheduleItemWatermarks ?? []) foreach (ProgramScheduleItemWatermark programScheduleItemWatermark in scheduleItem
.ProgramScheduleItemWatermarks ?? [])
{ {
playoutItem.PlayoutItemWatermarks.Add( playoutItem.PlayoutItemWatermarks.Add(
new PlayoutItemWatermark new PlayoutItemWatermark

6
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs

@ -185,9 +185,9 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
} }
else else
{ {
foreach (var midRollSequence in maybeMidRollSequence) foreach (YamlPlayoutContext.MidRollSequence midRollSequence in maybeMidRollSequence)
{ {
var filteredChapters = FillerExpression.FilterChapters( List<MediaChapter> filteredChapters = FillerExpression.FilterChapters(
midRollSequence.Expression, midRollSequence.Expression,
itemChapters, itemChapters,
playoutItem); playoutItem);
@ -201,7 +201,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
{ {
for (var j = 0; j < filteredChapters.Count; j++) for (var j = 0; j < filteredChapters.Count; j++)
{ {
var nextItem = playoutItem.ForChapter(filteredChapters[j]); PlayoutItem nextItem = playoutItem.ForChapter(filteredChapters[j]);
nextItem.Start = context.CurrentTime.UtcDateTime; nextItem.Start = context.CurrentTime.UtcDateTime;
nextItem.Finish = context.CurrentTime.UtcDateTime + (nextItem.OutPoint - nextItem.InPoint); nextItem.Finish = context.CurrentTime.UtcDateTime + (nextItem.OutPoint - nextItem.InPoint);

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs

@ -29,7 +29,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
return false; return false;
} }
if (duration.StopBeforeEnd == false && duration.OfflineTail) if (!duration.StopBeforeEnd && duration.OfflineTail)
{ {
logger.LogError("offline_tail must be false when stop_before_end is false"); logger.LogError("offline_tail must be false when stop_before_end is false");
return false; return false;

4
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOffHandler.cs

@ -30,7 +30,7 @@ public class YamlPlayoutGraphicsOffHandler(IGraphicsElementRepository graphicsEl
} }
else else
{ {
foreach (var ge in await GetGraphicsElementByPath(graphicsOff.GraphicsOff)) foreach (GraphicsElement ge in await GetGraphicsElementByPath(graphicsOff.GraphicsOff))
{ {
context.RemoveGraphicsElement(ge.Id); context.RemoveGraphicsElement(ge.Id);
} }
@ -41,7 +41,7 @@ public class YamlPlayoutGraphicsOffHandler(IGraphicsElementRepository graphicsEl
private async Task<Option<GraphicsElement>> GetGraphicsElementByPath(string path) private async Task<Option<GraphicsElement>> GetGraphicsElementByPath(string path)
{ {
if (_graphicsElementCache.TryGetValue(path, out var cachedGraphicsElement)) if (_graphicsElementCache.TryGetValue(path, out Option<GraphicsElement> cachedGraphicsElement))
{ {
foreach (GraphicsElement graphicsElement in cachedGraphicsElement) foreach (GraphicsElement graphicsElement in cachedGraphicsElement)
{ {

4
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOnHandler.cs

@ -35,7 +35,7 @@ public class YamlPlayoutGraphicsOnHandler(IGraphicsElementRepository graphicsEle
return false; return false;
} }
foreach (var ge in await GetGraphicsElementByPath(graphicsOn.GraphicsOn)) foreach (GraphicsElement ge in await GetGraphicsElementByPath(graphicsOn.GraphicsOn))
{ {
string variables = null; string variables = null;
if (graphicsOn.Variables.Count > 0) if (graphicsOn.Variables.Count > 0)
@ -51,7 +51,7 @@ public class YamlPlayoutGraphicsOnHandler(IGraphicsElementRepository graphicsEle
private async Task<Option<GraphicsElement>> GetGraphicsElementByPath(string path) private async Task<Option<GraphicsElement>> GetGraphicsElementByPath(string path)
{ {
if (_graphicsElementCache.TryGetValue(path, out var cachedGraphicsElement)) if (_graphicsElementCache.TryGetValue(path, out Option<GraphicsElement> cachedGraphicsElement))
{ {
foreach (GraphicsElement graphicsElement in cachedGraphicsElement) foreach (GraphicsElement graphicsElement in cachedGraphicsElement)
{ {

3
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutMidRollHandler.cs

@ -20,7 +20,8 @@ public class YamlPlayoutMidRollHandler : IYamlPlayoutHandler
return Task.FromResult(false); return Task.FromResult(false);
} }
if (midRoll.MidRoll && !string.IsNullOrWhiteSpace(midRoll.Sequence) && !string.IsNullOrWhiteSpace(midRoll.Expression)) if (midRoll.MidRoll && !string.IsNullOrWhiteSpace(midRoll.Sequence) &&
!string.IsNullOrWhiteSpace(midRoll.Expression))
{ {
context.SetMidRollSequence(new YamlPlayoutContext.MidRollSequence(midRoll.Sequence, midRoll.Expression)); context.SetMidRollSequence(new YamlPlayoutContext.MidRollSequence(midRoll.Sequence, midRoll.Expression));
} }

6
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWatermarkHandler.cs

@ -26,7 +26,7 @@ public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) :
if (watermark.Watermark && !string.IsNullOrWhiteSpace(watermark.Name)) if (watermark.Watermark && !string.IsNullOrWhiteSpace(watermark.Name))
{ {
foreach (var wm in await GetChannelWatermarkByName(watermark.Name)) foreach (ChannelWatermark wm in await GetChannelWatermarkByName(watermark.Name))
{ {
context.SetChannelWatermarkId(wm.Id); context.SetChannelWatermarkId(wm.Id);
} }
@ -35,7 +35,7 @@ public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) :
{ {
if (!string.IsNullOrWhiteSpace(watermark.Name)) if (!string.IsNullOrWhiteSpace(watermark.Name))
{ {
foreach (var wm in await GetChannelWatermarkByName(watermark.Name)) foreach (ChannelWatermark wm in await GetChannelWatermarkByName(watermark.Name))
{ {
context.RemoveChannelWatermarkId(wm.Id); context.RemoveChannelWatermarkId(wm.Id);
} }
@ -51,7 +51,7 @@ public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) :
private async Task<Option<ChannelWatermark>> GetChannelWatermarkByName(string name) private async Task<Option<ChannelWatermark>> GetChannelWatermarkByName(string name)
{ {
if (_watermarkCache.TryGetValue(name, out var cachedWatermark)) if (_watermarkCache.TryGetValue(name, out Option<ChannelWatermark> cachedWatermark))
{ {
foreach (ChannelWatermark channelWatermark in cachedWatermark) foreach (ChannelWatermark channelWatermark in cachedWatermark)
{ {

54
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs

@ -30,7 +30,7 @@ public class YamlPlayoutBuilder(
PlayoutBuildMode mode, PlayoutBuildMode mode,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var result = PlayoutBuildResult.Empty; PlayoutBuildResult result = PlayoutBuildResult.Empty;
if (!localFileSystem.FileExists(playout.TemplateFile)) if (!localFileSystem.FileExists(playout.TemplateFile))
{ {
@ -39,7 +39,7 @@ public class YamlPlayoutBuilder(
} }
Option<YamlPlayoutDefinition> maybePlayoutDefinition = Option<YamlPlayoutDefinition> maybePlayoutDefinition =
await LoadYamlDefinition(playout.TemplateFile, isImport: false, cancellationToken); await LoadYamlDefinition(playout.TemplateFile, false, cancellationToken);
if (maybePlayoutDefinition.IsNone) if (maybePlayoutDefinition.IsNone)
{ {
logger.LogWarning("YAML playout file {File} is invalid; aborting.", playout.TemplateFile); logger.LogWarning("YAML playout file {File} is invalid; aborting.", playout.TemplateFile);
@ -49,11 +49,11 @@ public class YamlPlayoutBuilder(
// using ValueUnsafe to avoid nesting // using ValueUnsafe to avoid nesting
YamlPlayoutDefinition playoutDefinition = maybePlayoutDefinition.ValueUnsafe(); YamlPlayoutDefinition playoutDefinition = maybePlayoutDefinition.ValueUnsafe();
foreach (var import in playoutDefinition.Import) foreach (string import in playoutDefinition.Import)
{ {
try try
{ {
var path = import; string path = import;
if (!File.Exists(import)) if (!File.Exists(import))
{ {
path = Path.Combine( path = Path.Combine(
@ -66,16 +66,23 @@ public class YamlPlayoutBuilder(
} }
} }
var maybeImportedDefinition = await LoadYamlDefinition(path, isImport: true, cancellationToken); Option<YamlPlayoutDefinition> maybeImportedDefinition =
foreach (var importedDefinition in maybeImportedDefinition) await LoadYamlDefinition(path, true, cancellationToken);
foreach (YamlPlayoutDefinition importedDefinition in maybeImportedDefinition)
{ {
var contentToAdd = importedDefinition.Content IEnumerable<YamlPlayoutContentItem> contentToAdd = importedDefinition.Content
.Where(c => playoutDefinition.Content.All(c2 => !string.Equals(c2.Key, c.Key, StringComparison.OrdinalIgnoreCase))); .Where(c => playoutDefinition.Content.All(c2 => !string.Equals(
c2.Key,
c.Key,
StringComparison.OrdinalIgnoreCase)));
playoutDefinition.Content.AddRange(contentToAdd); playoutDefinition.Content.AddRange(contentToAdd);
var sequencesToAdd = importedDefinition.Sequence IEnumerable<YamlPlayoutSequenceItem> sequencesToAdd = importedDefinition.Sequence
.Where(s => playoutDefinition.Sequence.All(s2 => !string.Equals(s2.Key, s.Key, StringComparison.OrdinalIgnoreCase))); .Where(s => playoutDefinition.Sequence.All(s2 => !string.Equals(
s2.Key,
s.Key,
StringComparison.OrdinalIgnoreCase)));
playoutDefinition.Sequence.AddRange(sequencesToAdd); playoutDefinition.Sequence.AddRange(sequencesToAdd);
} }
@ -236,13 +243,16 @@ public class YamlPlayoutBuilder(
continue; continue;
async Task ExecuteSequenceLocal(string sequence) => await ExecuteSequence( async Task ExecuteSequenceLocal(string sequence)
handlers, {
enumeratorCache, await ExecuteSequence(
mode, handlers,
context, enumeratorCache,
sequence, mode,
cancellationToken); context,
sequence,
cancellationToken);
}
} }
if (!instruction.ChangesIndex) if (!instruction.ChangesIndex)
@ -303,7 +313,13 @@ public class YamlPlayoutBuilder(
foreach (IYamlPlayoutHandler handler in maybeHandler) foreach (IYamlPlayoutHandler handler in maybeHandler)
{ {
if (!await handler.Handle(context, instruction, mode, _ => Task.CompletedTask, logger, cancellationToken)) if (!await handler.Handle(
context,
instruction,
mode,
_ => Task.CompletedTask,
logger,
cancellationToken))
{ {
logger.LogInformation("YAML playout instruction handler failed"); logger.LogInformation("YAML playout instruction handler failed");
} }
@ -432,7 +448,7 @@ public class YamlPlayoutBuilder(
try try
{ {
string yaml = await File.ReadAllTextAsync(fileName, cancellationToken); string yaml = await File.ReadAllTextAsync(fileName, cancellationToken);
if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false) if (!await yamlScheduleValidator.ValidateSchedule(yaml, isImport))
{ {
return Option<YamlPlayoutDefinition>.None; return Option<YamlPlayoutDefinition>.None;
} }

10
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs

@ -13,16 +13,17 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
NullValueHandling = NullValueHandling.Ignore NullValueHandling = NullValueHandling.Ignore
}; };
private readonly System.Collections.Generic.HashSet<int> _visitedInstructions = [];
private readonly Stack<FillerKind> _fillerKind = new();
private readonly System.Collections.Generic.HashSet<int> _channelWatermarkIds = []; private readonly System.Collections.Generic.HashSet<int> _channelWatermarkIds = [];
private readonly Stack<FillerKind> _fillerKind = new();
private readonly Dictionary<int, string> _graphicsElements = []; private readonly Dictionary<int, string> _graphicsElements = [];
private readonly System.Collections.Generic.HashSet<int> _visitedInstructions = [];
private int _guideGroup = guideGroup; private int _guideGroup = guideGroup;
private bool _guideGroupLocked; private bool _guideGroupLocked;
private int _instructionIndex; private int _instructionIndex;
private Option<string> _preRollSequence;
private Option<string> _postRollSequence;
private Option<MidRollSequence> _midRollSequence; private Option<MidRollSequence> _midRollSequence;
private Option<string> _postRollSequence;
private Option<string> _preRollSequence;
public Playout Playout { get; } = playout; public Playout Playout { get; } = playout;
@ -112,6 +113,7 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
public void PushFillerKind(FillerKind fillerKind) => _fillerKind.Push(fillerKind); public void PushFillerKind(FillerKind fillerKind) => _fillerKind.Push(fillerKind);
public void PopFillerKind() => _fillerKind.Pop(); public void PopFillerKind() => _fillerKind.Pop();
public Option<FillerKind> GetFillerKind() => public Option<FillerKind> GetFillerKind() =>
_fillerKind.TryPeek(out FillerKind fillerKind) ? fillerKind : Option<FillerKind>.None; _fillerKind.TryPeek(out FillerKind fillerKind) ? fillerKind : Option<FillerKind>.None;

15
ErsatzTV.Core/Troubleshooting/TroubleshootingNotifier.cs

@ -7,18 +7,9 @@ public class TroubleshootingNotifier : ITroubleshootingNotifier
{ {
private readonly ConcurrentDictionary<Guid, bool> _failedSessions = new(); private readonly ConcurrentDictionary<Guid, bool> _failedSessions = new();
public bool IsFailed(Guid sessionId) public bool IsFailed(Guid sessionId) => _failedSessions.TryGetValue(sessionId, out _);
{
return _failedSessions.TryGetValue(sessionId, out _);
}
public void NotifyFailed(Guid sessionId) public void NotifyFailed(Guid sessionId) => _failedSessions[sessionId] = true;
{
_failedSessions[sessionId] = true;
}
public void RemoveSession(Guid sessionId) public void RemoveSession(Guid sessionId) => _failedSessions.TryRemove(sessionId, out _);
{
_failedSessions.TryRemove(sessionId, out _);
}
} }

5
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs

@ -2,13 +2,12 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public record FFmpegKnownEncoder public record FFmpegKnownEncoder
{ {
public static readonly FFmpegKnownEncoder H264VideoToolbox = new("h264_videotoolbox");
public static readonly FFmpegKnownEncoder HevcVideoToolbox = new("hevc_videotoolbox");
private FFmpegKnownEncoder(string Name) => this.Name = Name; private FFmpegKnownEncoder(string Name) => this.Name = Name;
public string Name { get; } public string Name { get; }
public static readonly FFmpegKnownEncoder H264VideoToolbox = new("h264_videotoolbox");
public static readonly FFmpegKnownEncoder HevcVideoToolbox = new("hevc_videotoolbox");
// only list the encoders that we actually check for // only list the encoders that we actually check for
public static IList<string> AllEncoders => public static IList<string> AllEncoders =>
[ [

8
ErsatzTV.FFmpeg/Capabilities/FourCC.cs

@ -2,14 +2,14 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public static class FourCC public static class FourCC
{ {
public const string H264 = "avc1";
public const string Hevc = "hvc1";
public const string Vp9 = "vp90";
public static readonly List<string> AllVideoToolbox = public static readonly List<string> AllVideoToolbox =
[ [
H264, H264,
Hevc, Hevc,
Vp9 Vp9
]; ];
public const string H264 = "avc1";
public const string Hevc = "hvc1";
public const string Vp9 = "vp90";
} }

7
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -265,7 +265,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{ {
var result = new List<string>(); var result = new List<string>();
foreach (var fourCC in FourCC.AllVideoToolbox) foreach (string fourCC in FourCC.AllVideoToolbox)
{ {
if (VideoToolboxUtil.IsHardwareDecoderSupported(fourCC, _logger)) if (VideoToolboxUtil.IsHardwareDecoderSupported(fourCC, _logger))
{ {
@ -276,10 +276,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return result; return result;
} }
public List<string> GetVideoToolboxEncoders() public List<string> GetVideoToolboxEncoders() => VideoToolboxUtil.GetAvailableEncoders(_logger);
{
return VideoToolboxUtil.GetAvailableEncoders(_logger);
}
private async Task<IReadOnlySet<string>> GetFFmpegCapabilities( private async Task<IReadOnlySet<string>> GetFFmpegCapabilities(
string ffmpegPath, string ffmpegPath,

6
ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs

@ -4,18 +4,18 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public interface IHardwareCapabilities public interface IHardwareCapabilities
{ {
public FFmpegCapability CanDecode( FFmpegCapability CanDecode(
string videoFormat, string videoFormat,
Option<string> videoProfile, Option<string> videoProfile,
Option<IPixelFormat> maybePixelFormat, Option<IPixelFormat> maybePixelFormat,
bool isHdr); bool isHdr);
public FFmpegCapability CanEncode( FFmpegCapability CanEncode(
string videoFormat, string videoFormat,
Option<string> videoProfile, Option<string> videoProfile,
Option<IPixelFormat> maybePixelFormat); Option<IPixelFormat> maybePixelFormat);
public Option<RateControlMode> GetRateControlMode( Option<RateControlMode> GetRateControlMode(
string videoFormat, string videoFormat,
Option<IPixelFormat> maybePixelFormat); Option<IPixelFormat> maybePixelFormat);
} }

8
ErsatzTV.FFmpeg/Capabilities/VideoToolbox/VideoToolboxUtil.cs

@ -8,7 +8,7 @@ internal static partial class VideoToolboxUtil
{ {
private const string CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; private const string CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
private const string VideoToolbox = "/System/Library/Frameworks/VideoToolbox.framework/VideoToolbox"; private const string VideoToolbox = "/System/Library/Frameworks/VideoToolbox.framework/VideoToolbox";
private const string LibSystem = "/usr/lib/libSystem.dylib"; private const string LibSystem = "/usr/lib/libSystem.dylib";
[LibraryImport(CoreFoundation)] [LibraryImport(CoreFoundation)]
private static partial long CFArrayGetCount(IntPtr array); private static partial long CFArrayGetCount(IntPtr array);
@ -89,7 +89,7 @@ internal static partial class VideoToolboxUtil
} }
long maxSize = length * 4 + 1; long maxSize = length * 4 + 1;
byte[] buffer = new byte[maxSize]; var buffer = new byte[maxSize];
if (CFStringGetCString(cfString, buffer, maxSize, kCFStringEncodingUTF8)) if (CFStringGetCString(cfString, buffer, maxSize, kCFStringEncodingUTF8))
{ {
int terminator = Array.IndexOf(buffer, (byte)0); int terminator = Array.IndexOf(buffer, (byte)0);
@ -107,7 +107,7 @@ internal static partial class VideoToolboxUtil
throw new ArgumentException("FourCC must be 4 characters long.", nameof(fourCC)); throw new ArgumentException("FourCC must be 4 characters long.", nameof(fourCC));
} }
return ((uint)fourCC[0] << 24) | ((uint)fourCC[1] << 16) | ((uint)fourCC[2] << 8) | (uint)fourCC[3]; return ((uint)fourCC[0] << 24) | ((uint)fourCC[1] << 16) | ((uint)fourCC[2] << 8) | fourCC[3];
} }
internal static List<string> GetAvailableEncoders(ILogger logger) internal static List<string> GetAvailableEncoders(ILogger logger)
@ -138,7 +138,7 @@ internal static partial class VideoToolboxUtil
} }
var count = (int)CFArrayGetCount(encoderList); var count = (int)CFArrayGetCount(encoderList);
for (int i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
IntPtr encoderDict = CFArrayGetValueAtIndex(encoderList, i); IntPtr encoderDict = CFArrayGetValueAtIndex(encoderList, i);
if (encoderDict == IntPtr.Zero) if (encoderDict == IntPtr.Zero)

19
ErsatzTV.FFmpeg/Capabilities/VideoToolboxHardwareCapabilities.cs

@ -8,8 +8,8 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public class VideoToolboxHardwareCapabilities : IHardwareCapabilities public class VideoToolboxHardwareCapabilities : IHardwareCapabilities
{ {
private static readonly ConcurrentDictionary<string, bool> Encoders = new (); private static readonly ConcurrentDictionary<string, bool> Encoders = new();
private static readonly ConcurrentDictionary<string, bool> Decoders = new (); private static readonly ConcurrentDictionary<string, bool> Decoders = new();
private readonly IFFmpegCapabilities _ffmpegCapabilities; private readonly IFFmpegCapabilities _ffmpegCapabilities;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -20,7 +20,11 @@ public class VideoToolboxHardwareCapabilities : IHardwareCapabilities
_logger = logger; _logger = logger;
} }
public FFmpegCapability CanDecode(string videoFormat, Option<string> videoProfile, Option<IPixelFormat> maybePixelFormat, bool isHdr) public FFmpegCapability CanDecode(
string videoFormat,
Option<string> videoProfile,
Option<IPixelFormat> maybePixelFormat,
bool isHdr)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Decoders.IsEmpty) if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Decoders.IsEmpty)
{ {
@ -50,15 +54,18 @@ public class VideoToolboxHardwareCapabilities : IHardwareCapabilities
}; };
} }
public FFmpegCapability CanEncode(string videoFormat, Option<string> videoProfile, Option<IPixelFormat> maybePixelFormat) public FFmpegCapability CanEncode(
string videoFormat,
Option<string> videoProfile,
Option<IPixelFormat> maybePixelFormat)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Encoders.IsEmpty) if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Encoders.IsEmpty)
{ {
var encoderList = VideoToolboxUtil.GetAvailableEncoders(_logger); List<string> encoderList = VideoToolboxUtil.GetAvailableEncoders(_logger);
_logger.LogDebug("VideoToolbox reports {Count} encoders", encoderList.Count); _logger.LogDebug("VideoToolbox reports {Count} encoders", encoderList.Count);
// we only really care about h264 and hevc hardware encoders // we only really care about h264 and hevc hardware encoders
foreach (var encoder in encoderList) foreach (string encoder in encoderList)
{ {
if (encoder.Contains("HEVC (HW)", StringComparison.OrdinalIgnoreCase)) if (encoder.Contains("HEVC (HW)", StringComparison.OrdinalIgnoreCase))
{ {

2
ErsatzTV.FFmpeg/Environment/CudaVisibleDevicesVariable.cs

@ -2,7 +2,7 @@
public class CudaVisibleDevicesVariable(string visibleDevices) : IPipelineStep public class CudaVisibleDevicesVariable(string visibleDevices) : IPipelineStep
{ {
public EnvironmentVariable[] EnvironmentVariables => [ new("CUDA_VISIBLE_DEVICES", visibleDevices) ]; public EnvironmentVariable[] EnvironmentVariables => [new("CUDA_VISIBLE_DEVICES", visibleDevices)];
public string[] GlobalOptions => []; public string[] GlobalOptions => [];
public string[] InputOptions(InputFile inputFile) => []; public string[] InputOptions(InputFile inputFile) => [];
public string[] FilterOptions => []; public string[] FilterOptions => [];

5
ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs

@ -7,8 +7,8 @@ public class ColorspaceFilter : BaseFilter
private readonly FrameState _currentState; private readonly FrameState _currentState;
private readonly IPixelFormat _desiredPixelFormat; private readonly IPixelFormat _desiredPixelFormat;
private readonly bool _forceInputOverrides; private readonly bool _forceInputOverrides;
private readonly VideoStream _videoStream;
private readonly bool _isQsv; private readonly bool _isQsv;
private readonly VideoStream _videoStream;
public ColorspaceFilter( public ColorspaceFilter(
FrameState currentState, FrameState currentState,
@ -106,7 +106,8 @@ public class ColorspaceFilter : BaseFilter
string colorspace = _desiredPixelFormat.BitDepth switch string colorspace = _desiredPixelFormat.BitDepth switch
{ {
_ when cp.IsUnknown && _isQsv => $"{hwdownload}setparams=range=tv:colorspace=bt709:color_trc=bt709:color_primaries=bt709", _ when cp.IsUnknown && _isQsv =>
$"{hwdownload}setparams=range=tv:colorspace=bt709:color_trc=bt709:color_primaries=bt709",
_ when cp.IsUnknown => "setparams=range=tv:colorspace=bt709:color_trc=bt709:color_primaries=bt709", _ when cp.IsUnknown => "setparams=range=tv:colorspace=bt709:color_trc=bt709:color_primaries=bt709",
10 when !cp.IsUnknown => 10 when !cp.IsUnknown =>
$"{hwdownload}colorspace={inputOverrides}all=bt709:format=yuv420p10", $"{hwdownload}colorspace={inputOverrides}all=bt709:format=yuv420p10",

2
ErsatzTV.FFmpeg/Filter/ComplexFilter.cs

@ -6,8 +6,8 @@ namespace ErsatzTV.FFmpeg.Filter;
public class ComplexFilter : IPipelineStep public class ComplexFilter : IPipelineStep
{ {
private readonly Option<AudioInputFile> _maybeAudioInputFile; private readonly Option<AudioInputFile> _maybeAudioInputFile;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly Option<GraphicsEngineInput> _maybeGraphicsEngineInput; private readonly Option<GraphicsEngineInput> _maybeGraphicsEngineInput;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly Option<VideoInputFile> _maybeVideoInputFile; private readonly Option<VideoInputFile> _maybeVideoInputFile;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile; private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
private readonly PipelineContext _pipelineContext; private readonly PipelineContext _pipelineContext;

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

Loading…
Cancel
Save