Browse Source

overdue code cleanup (#1271)

pull/1272/head
Jason Dove 3 years ago committed by GitHub
parent
commit
03df2a6c8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs
  2. 17
      ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs
  3. 12
      ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs
  4. 17
      ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs
  5. 2
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  6. 8
      ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs
  7. 4
      ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs
  8. 3
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs
  9. 8
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs
  10. 4
      ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs
  11. 36
      ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs
  12. 11
      ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs
  13. 2
      ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSourcesHandler.cs
  14. 4
      ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs
  15. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs
  16. 6
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  17. 2
      ErsatzTV.Application/Filler/Commands/DeleteFillerPresetHandler.cs
  18. 18
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs
  19. 6
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSourcesHandler.cs
  20. 10
      ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs
  21. 2
      ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryHandler.cs
  22. 2
      ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs
  23. 2
      ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs
  24. 6
      ErsatzTV.Application/Libraries/Queries/GetExternalCollectionsHandler.cs
  25. 2
      ErsatzTV.Application/Logs/Mapper.cs
  26. 5
      ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs
  27. 4
      ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedSubtitlesHandler.cs
  28. 6
      ErsatzTV.Application/Maintenance/Commands/ReleaseMemoryHandler.cs
  29. 2
      ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs
  30. 2
      ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs
  31. 2
      ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs
  32. 2
      ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs
  33. 2
      ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollectionHandler.cs
  34. 2
      ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollectionHandler.cs
  35. 6
      ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs
  36. 4
      ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs
  37. 6
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  38. 2
      ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs
  39. 2
      ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs
  40. 6
      ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs
  41. 2
      ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs
  42. 13
      ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs
  43. 4
      ErsatzTV.Application/ProgramSchedules/Commands/CopyProgramScheduleHandler.cs
  44. 2
      ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramScheduleHandler.cs
  45. 2
      ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs
  46. 2
      ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs
  47. 2
      ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs
  48. 2
      ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs
  49. 2
      ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs
  50. 2
      ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs
  51. 4
      ErsatzTV.Application/Search/Queries/SearchCollectionsHandler.cs
  52. 4
      ErsatzTV.Application/Search/Queries/SearchMultiCollectionsHandler.cs
  53. 4
      ErsatzTV.Application/Search/Queries/SearchSmartCollectionsHandler.cs
  54. 4
      ErsatzTV.Application/Search/Queries/SearchTelevisionSeasonsHandler.cs
  55. 4
      ErsatzTV.Application/Search/Queries/SearchTelevisionShowsHandler.cs
  56. 4
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  57. 2
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  58. 4
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  59. 14
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  60. 12
      ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs
  61. 5
      ErsatzTV.Application/Templates/Queries/GetMusicVideoCreditTemplatesHandler.cs
  62. 12
      ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs
  63. 2
      ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs
  64. 2
      ErsatzTV.Application/Watermarks/Commands/DeleteWatermarkHandler.cs
  65. 2
      ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs
  66. 2
      ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs
  67. 2
      ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs
  68. 11
      ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs
  69. 7
      ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs
  70. 36
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  71. 32
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  72. 11
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs
  73. 23
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs
  74. 9
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  75. 9
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs
  76. 9
      ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs
  77. 52
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  78. 9
      ErsatzTV.Core.Tests/Scheduling/SeasonEpisodeContentTests.cs
  79. 9
      ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs
  80. 11
      ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs
  81. 2
      ErsatzTV.Core/Domain/Filler/FillerKind.cs
  82. 3
      ErsatzTV.Core/Domain/MediaItem/Episode.cs
  83. 20
      ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs
  84. 2
      ErsatzTV.Core/Emby/EmbyUrl.cs
  85. 2
      ErsatzTV.Core/Errors/ScanIsNotRequired.cs
  86. 4
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  87. 11
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  88. 5
      ErsatzTV.Core/FFmpeg/FFmpegProcess.cs
  89. 9
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  90. 15
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  91. 2
      ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs
  92. 4
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  93. 6
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  94. 10
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  95. 24
      ErsatzTV.Core/Scheduling/MultiCollectionGroup.cs
  96. 9
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  97. 4
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  98. 4
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  99. 2
      ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs
  100. 2
      ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs
  101. Some files were not shown because too many files have changed in this diff Show More

9
ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs

@ -10,10 +10,7 @@ public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMed
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllArtistsHandler(IDbContextFactory<TvContext> dbContextFactory) public GetAllArtistsHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
{
_dbContextFactory = dbContextFactory;
}
public async Task<List<NamedMediaItemViewModel>> Handle( public async Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request, GetAllArtists request,
@ -24,8 +21,8 @@ public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMed
List<Artist> allArtists = await dbContext.Artists List<Artist> allArtists = await dbContext.Artists
.AsNoTracking() .AsNoTracking()
.Include(a => a.ArtistMetadata) .Include(a => a.ArtistMetadata)
.ToListAsync(cancellationToken: cancellationToken); .ToListAsync(cancellationToken);
return allArtists.Bind(a => ProjectArtist(a)).ToList(); return allArtists.Bind(a => ProjectArtist(a)).ToList();
} }

17
ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs

@ -1,19 +1,18 @@
using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
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.Channels; namespace ErsatzTV.Application.Channels;
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Unit>> public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeleteChannelHandler( public DeleteChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel, ChannelWriter<IBackgroundServiceRequest> workerChannel,
@ -28,12 +27,12 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken) public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Core.Domain.Channel> validation = await ChannelMustExist(dbContext, request); Validation<BaseError, Channel> validation = await ChannelMustExist(dbContext, request);
return await validation.Apply(c => DoDeletion(dbContext, c, cancellationToken)); return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c, cancellationToken));
} }
private async Task<Unit> DoDeletion(TvContext dbContext, Core.Domain.Channel channel, CancellationToken cancellationToken) private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
{ {
dbContext.Channels.Remove(channel); dbContext.Channels.Remove(channel);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
@ -51,9 +50,11 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
return Unit.Default; return Unit.Default;
} }
private static async Task<Validation<BaseError, Core.Domain.Channel>> ChannelMustExist(TvContext dbContext, DeleteChannel deleteChannel) private static async Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
DeleteChannel deleteChannel)
{ {
Option<Core.Domain.Channel> maybeChannel = await dbContext.Channels Option<Channel> maybeChannel = await dbContext.Channels
.SelectOneAsync(c => c.Id, c => c.Id == deleteChannel.ChannelId); .SelectOneAsync(c => c.Id, c => c.Id == deleteChannel.ChannelId);
return maybeChannel.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist."); return maybeChannel.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.");
} }

12
ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs

@ -14,10 +14,10 @@ namespace ErsatzTV.Application.Channels;
public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
{ {
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RefreshChannelDataHandler> _logger; private readonly ILogger<RefreshChannelDataHandler> _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RefreshChannelDataHandler( public RefreshChannelDataHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager, RecyclableMemoryStreamManager recyclableMemoryStreamManager,
@ -359,7 +359,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{request.ChannelNumber}.xml"); string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{request.ChannelNumber}.xml");
File.Move(tempFile, targetFile, true); File.Move(tempFile, targetFile, true);
} }
private static string GetArtworkUrl(Artwork artwork, ArtworkKind artworkKind) private static string GetArtworkUrl(Artwork artwork, ArtworkKind artworkKind)
{ {
string artworkPath = artwork.Path; string artworkPath = artwork.Path;
@ -456,7 +456,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
_ => string.Empty _ => string.Empty
}; };
} }
private Option<ContentRating> GetContentRating(PlayoutItem playoutItem) private Option<ContentRating> GetContentRating(PlayoutItem playoutItem)
{ {
try try
@ -478,7 +478,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
return None; return None;
} }
} }
private static Option<ContentRating> ParseContentRating(string contentRating, string system) private static Option<ContentRating> ParseContentRating(string contentRating, string system)
{ {
Option<string> maybeFirst = (contentRating ?? string.Empty).Split('/').HeadOrNone(); Option<string> maybeFirst = (contentRating ?? string.Empty).Split('/').HeadOrNone();
@ -499,8 +499,6 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
}).Flatten(); }).Flatten();
} }
private record ContentRating(Option<string> System, string Value);
private string GetPrioritizedArtworkPath(Metadata metadata) private string GetPrioritizedArtworkPath(Metadata metadata)
{ {
Option<string> maybeArtwork = Optional(metadata.Artwork).Flatten() Option<string> maybeArtwork = Optional(metadata.Artwork).Flatten()
@ -518,4 +516,6 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
return maybeArtwork.IfNone(string.Empty); return maybeArtwork.IfNone(string.Empty);
} }
private record ContentRating(Option<string> System, string Value);
} }

17
ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs

@ -12,9 +12,9 @@ namespace ErsatzTV.Application.Channels;
public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList> public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
{ {
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RefreshChannelListHandler( public RefreshChannelListHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager, RecyclableMemoryStreamManager recyclableMemoryStreamManager,
@ -29,7 +29,7 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
public async Task Handle(RefreshChannelList request, CancellationToken cancellationToken) public async Task Handle(RefreshChannelList request, CancellationToken cancellationToken)
{ {
_localFileSystem.EnsureFolderExists(FileSystemLayout.ChannelGuideCacheFolder); _localFileSystem.EnsureFolderExists(FileSystemLayout.ChannelGuideCacheFolder);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream(); using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
@ -73,7 +73,7 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
string tempFile = Path.GetTempFileName(); string tempFile = Path.GetTempFileName();
await File.WriteAllBytesAsync(tempFile, ms.ToArray(), cancellationToken); await File.WriteAllBytesAsync(tempFile, ms.ToArray(), cancellationToken);
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml"); string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
File.Move(tempFile, targetFile, true); File.Move(tempFile, targetFile, true);
} }
@ -89,13 +89,16 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
await using var reader = (DbDataReader)await dbContext.Connection.ExecuteReaderAsync(QUERY); await using var reader = (DbDataReader)await dbContext.Connection.ExecuteReaderAsync(QUERY);
Func<IDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>(); Func<IDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>();
while (await reader.ReadAsync()) { while (await reader.ReadAsync())
{
yield return rowParser(reader); yield return rowParser(reader);
} }
while (await reader.NextResultAsync()) {} while (await reader.NextResultAsync())
{
}
} }
private static List<string> GetCategories(string categories) => private static List<string> GetCategories(string categories) =>
(categories ?? string.Empty).Split(',') (categories ?? string.Empty).Split(',')
.Map(s => s.Trim()) .Map(s => s.Trim())

2
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -31,7 +31,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request); Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
} }
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update) private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)

8
ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs

@ -11,8 +11,8 @@ namespace ErsatzTV.Application.Channels;
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<BaseError, ChannelGuide>> public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<BaseError, ChannelGuide>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public GetChannelGuideHandler( public GetChannelGuideHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@ -35,7 +35,7 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
{ {
return BaseError.New($"Required file {channelsFile} is missing"); return BaseError.New($"Required file {channelsFile} is missing");
} }
string accessTokenUri = string.Empty; string accessTokenUri = string.Empty;
if (!string.IsNullOrWhiteSpace(request.AccessToken)) if (!string.IsNullOrWhiteSpace(request.AccessToken))
{ {
@ -43,7 +43,7 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
} }
string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken); string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken);
// TODO: is regex faster? // TODO: is regex faster?
channelsFragment = channelsFragment channelsFragment = channelsFragment
.Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}") .Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}")
@ -59,7 +59,7 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
} }
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken); string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken);
channelDataFragment = channelDataFragment channelDataFragment = channelDataFragment
.Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}") .Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}")
.Replace("{AccessTokenUri}", accessTokenUri); .Replace("{AccessTokenUri}", accessTokenUri);

4
ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs

@ -8,10 +8,8 @@ public class GetChannelNameByPlayoutIdHandler : IRequestHandler<GetChannelNameBy
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory) public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
{
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
}
public async Task<Option<string>> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken) public async Task<Option<string>> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken)
{ {

3
ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs

@ -2,4 +2,5 @@ using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels; namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist(string Scheme, string Host, string BaseUrl, string Mode, string AccessToken) : IRequest<ChannelPlaylist>; public record GetChannelPlaylist
(string Scheme, string Host, string BaseUrl, string Mode, string AccessToken) : IRequest<ChannelPlaylist>;

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

@ -14,7 +14,13 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) => public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll() _channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode)) .Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, request.BaseUrl, channels, request.AccessToken)); .Map(
channels => new ChannelPlaylist(
request.Scheme,
request.Host,
request.BaseUrl,
channels,
request.AccessToken));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode) private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{ {

4
ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs

@ -9,8 +9,6 @@ public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementBy
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) => public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) =>
{
await _configElementRepository.Upsert(request.Key, request.Value); await _configElementRepository.Upsert(request.Key, request.Value);
}
} }

36
ErsatzTV.Application/Emby/Commands/CallEmbyCollectionScannerHandler.cs

@ -22,6 +22,23 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
{ {
} }
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
{
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
{
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
}
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
});
}
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizeEmbyCollections request) protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizeEmbyCollections request)
{ {
DateTime minDateTime = await dbContext.EmbyMediaSources DateTime minDateTime = await dbContext.EmbyMediaSources
@ -42,26 +59,9 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
} }
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now); return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
} }
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
{
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
{
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
}
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
});
}
private async Task<Either<BaseError, Unit>> PerformScan( private async Task<Either<BaseError, Unit>> PerformScan(
string scanner, string scanner,
SynchronizeEmbyCollections request, SynchronizeEmbyCollections request,

11
ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs

@ -28,9 +28,10 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
ForceSynchronizeEmbyLibraryById request, ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken); CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>.Handle( Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>.
SynchronizeEmbyLibraryByIdIfNeeded request, Handle(
CancellationToken cancellationToken) => Handle(request, cancellationToken); SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private async Task<Either<BaseError, string>> Handle( private async Task<Either<BaseError, string>> Handle(
ISynchronizeEmbyLibraryById request, ISynchronizeEmbyLibraryById request,
@ -80,7 +81,7 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
DateTime minDateTime = await dbContext.EmbyLibraries DateTime minDateTime = await dbContext.EmbyLibraries
.SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId) .SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId)
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); .Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
return new DateTimeOffset(minDateTime, TimeSpan.Zero); return new DateTimeOffset(minDateTime, TimeSpan.Zero);
} }
@ -95,6 +96,6 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
} }
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now); return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
} }
} }

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

@ -8,8 +8,8 @@ namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources, public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>> Either<BaseError, List<EmbyMediaSource>>>
{ {
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
public SynchronizeEmbyMediaSourcesHandler( public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,

4
ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs

@ -10,8 +10,8 @@ namespace ErsatzTV.Application.Emby;
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters, public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
Either<BaseError, EmbyConnectionParametersViewModel>> Either<BaseError, EmbyConnectionParametersViewModel>>
{ {
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IEmbySecretStore _embySecretStore; private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
public GetEmbyConnectionParametersHandler( public GetEmbyConnectionParametersHandler(
@ -65,7 +65,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection"); .ToValidation<BaseError>("Emby media source requires an active connection");
} }
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey( private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters) ConnectionParameters connectionParameters)
{ {

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

@ -19,7 +19,7 @@ public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, E
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request); Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p)); return await validation.Apply(p => DoDeletion(dbContext, p));
} }
private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile) private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)

6
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs

@ -20,7 +20,7 @@ public class
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request); Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request)); return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
} }
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest( private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
@ -36,12 +36,12 @@ public class
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames; p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId; p.ResolutionId = update.ResolutionId;
p.VideoFormat = update.VideoFormat; p.VideoFormat = update.VideoFormat;
// mpeg2video only supports 8-bit content // mpeg2video only supports 8-bit content
p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video
? FFmpegProfileBitDepth.EightBit ? FFmpegProfileBitDepth.EightBit
: update.BitDepth; : update.BitDepth;
p.VideoBitrate = update.VideoBitrate; p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize; p.VideoBufferSize = update.VideoBufferSize;
p.AudioFormat = update.AudioFormat; p.AudioFormat = update.AudioFormat;

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

@ -19,7 +19,7 @@ public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Eit
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request); Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => DoDeletion(dbContext, ps)); return await validation.Apply(ps => DoDeletion(dbContext, ps));
} }
private static Task<Unit> DoDeletion(TvContext dbContext, FillerPreset fillerPreset) private static Task<Unit> DoDeletion(TvContext dbContext, FillerPreset fillerPreset)

18
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs

@ -24,13 +24,15 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
{ {
} }
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>.Handle( Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>.
ForceSynchronizeJellyfinLibraryById request, Handle(
CancellationToken cancellationToken) => Handle(request, cancellationToken); ForceSynchronizeJellyfinLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizeJellyfinLibraryByIdIfNeeded, Either<BaseError, string>>.Handle( Task<Either<BaseError, string>> IRequestHandler<SynchronizeJellyfinLibraryByIdIfNeeded, Either<BaseError, string>>.
SynchronizeJellyfinLibraryByIdIfNeeded request, Handle(
CancellationToken cancellationToken) => Handle(request, cancellationToken); SynchronizeJellyfinLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private async Task<Either<BaseError, string>> Handle( private async Task<Either<BaseError, string>> Handle(
ISynchronizeJellyfinLibraryById request, ISynchronizeJellyfinLibraryById request,
@ -64,7 +66,7 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
{ {
arguments.Add("--force"); arguments.Add("--force");
} }
if (request.DeepScan) if (request.DeepScan)
{ {
arguments.Add("--deep"); arguments.Add("--deep");
@ -95,6 +97,6 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
} }
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now); return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
} }
} }

6
ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSourcesHandler.cs

@ -8,8 +8,8 @@ namespace ErsatzTV.Application.Jellyfin;
public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler<SynchronizeJellyfinMediaSources, public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler<SynchronizeJellyfinMediaSources,
Either<BaseError, List<JellyfinMediaSource>>> Either<BaseError, List<JellyfinMediaSource>>>
{ {
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
public SynchronizeJellyfinMediaSourcesHandler( public SynchronizeJellyfinMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
@ -26,7 +26,9 @@ public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler<Synchroniz
List<JellyfinMediaSource> mediaSources = await _mediaSourceRepository.GetAllJellyfin(); List<JellyfinMediaSource> mediaSources = await _mediaSourceRepository.GetAllJellyfin();
foreach (JellyfinMediaSource mediaSource in mediaSources) foreach (JellyfinMediaSource mediaSource in mediaSources)
{ {
await _scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinAdminUserId(mediaSource.Id), cancellationToken); await _scannerWorkerChannel.WriteAsync(
new SynchronizeJellyfinAdminUserId(mediaSource.Id),
cancellationToken);
await _scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSource.Id), cancellationToken); await _scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSource.Id), cancellationToken);
} }

10
ErsatzTV.Application/Libraries/Commands/CallLibraryScannerHandler.cs

@ -20,9 +20,9 @@ namespace ErsatzTV.Application.Libraries;
public abstract class CallLibraryScannerHandler<TRequest> public abstract class CallLibraryScannerHandler<TRequest>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IConfigElementRepository _configElementRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _channel; private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _channel;
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private string _libraryName; private string _libraryName;
@ -152,9 +152,9 @@ public abstract class CallLibraryScannerHandler<TRequest>
.IfNoneAsync(0); .IfNoneAsync(0);
libraryRefreshInterval = Math.Clamp(libraryRefreshInterval, 0, 999_999); libraryRefreshInterval = Math.Clamp(libraryRefreshInterval, 0, 999_999);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
DateTimeOffset lastScan = await GetLastScan(dbContext, request); DateTimeOffset lastScan = await GetLastScan(dbContext, request);
if (!ScanIsRequired(lastScan, libraryRefreshInterval, request)) if (!ScanIsRequired(lastScan, libraryRefreshInterval, request))
{ {
@ -164,7 +164,7 @@ public abstract class CallLibraryScannerHandler<TRequest>
string executable = _runtimeInfo.IsOSPlatform(OSPlatform.Windows) string executable = _runtimeInfo.IsOSPlatform(OSPlatform.Windows)
? "ErsatzTV.Scanner.exe" ? "ErsatzTV.Scanner.exe"
: "ErsatzTV.Scanner"; : "ErsatzTV.Scanner";
string processFileName = Environment.ProcessPath ?? string.Empty; string processFileName = Environment.ProcessPath ?? string.Empty;
if (!string.IsNullOrWhiteSpace(processFileName)) if (!string.IsNullOrWhiteSpace(processFileName))
{ {

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

@ -32,7 +32,7 @@ public class CreateLocalLibraryHandler : LocalLibraryHandlerBase,
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request); Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request);
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary)); return await LanguageExtensions.Apply(validation, localLibrary => PersistLocalLibrary(dbContext, localLibrary));
} }
private async Task<LocalLibraryViewModel> PersistLocalLibrary( private async Task<LocalLibraryViewModel> PersistLocalLibrary(

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

@ -39,7 +39,7 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request); Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => MovePath(dbContext, parameters)); return await LanguageExtensions.Apply(validation, parameters => MovePath(dbContext, parameters));
} }
private async Task<Unit> MovePath(TvContext dbContext, Parameters parameters) private async Task<Unit> MovePath(TvContext dbContext, Parameters parameters)

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

@ -16,8 +16,8 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly ISearchIndex _searchIndex;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel; private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly ISearchIndex _searchIndex;
public UpdateLocalLibraryHandler( public UpdateLocalLibraryHandler(
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel, ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel,

6
ErsatzTV.Application/Libraries/Queries/GetExternalCollectionsHandler.cs

@ -8,10 +8,8 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetExternalCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) public GetExternalCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
{
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
}
public async Task<List<LibraryViewModel>> Handle( public async Task<List<LibraryViewModel>> Handle(
GetExternalCollections request, GetExternalCollections request,
@ -21,7 +19,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
List<int> mediaSourceIds = await dbContext.EmbyMediaSources List<int> mediaSourceIds = await dbContext.EmbyMediaSources
.Filter(ems => ems.Libraries.Any(l => ((EmbyLibrary)l).ShouldSyncItems)) .Filter(ems => ems.Libraries.Any(l => ((EmbyLibrary)l).ShouldSyncItems))
.Map(ems => ems.Id) .Map(ems => ems.Id)
.ToListAsync(cancellationToken: cancellationToken); .ToListAsync(cancellationToken);
return mediaSourceIds.Map( return mediaSourceIds.Map(
id => new LibraryViewModel( id => new LibraryViewModel(

2
ErsatzTV.Application/Logs/Mapper.cs

@ -7,7 +7,7 @@ internal partial class Mapper
{ {
[GeneratedRegex(@"(.*)\[(DBG|INF|WRN|ERR|FTL)\](.*)")] [GeneratedRegex(@"(.*)\[(DBG|INF|WRN|ERR|FTL)\](.*)")]
private static partial Regex LogEntryRegex(); private static partial Regex LogEntryRegex();
internal static Option<LogEntryViewModel> ProjectToViewModel(string line) internal static Option<LogEntryViewModel> ProjectToViewModel(string line)
{ {
Match match = LogEntryRegex().Match(line); Match match = LogEntryRegex().Match(line);

5
ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs

@ -8,10 +8,7 @@ public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, P
{ {
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
public GetRecentLogEntriesHandler(ILocalFileSystem localFileSystem) public GetRecentLogEntriesHandler(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
{
_localFileSystem = localFileSystem;
}
public Task<PagedLogEntriesViewModel> Handle( public Task<PagedLogEntriesViewModel> Handle(
GetRecentLogEntries request, GetRecentLogEntries request,

4
ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedSubtitlesHandler.cs

@ -9,10 +9,8 @@ public class DeleteOrphanedSubtitlesHandler : IRequestHandler<DeleteOrphanedSubt
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteOrphanedSubtitlesHandler(IDbContextFactory<TvContext> dbContextFactory) public DeleteOrphanedSubtitlesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
{
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
DeleteOrphanedSubtitles request, DeleteOrphanedSubtitles request,

6
ErsatzTV.Application/Maintenance/Commands/ReleaseMemoryHandler.cs

@ -7,7 +7,7 @@ namespace ErsatzTV.Application.Maintenance;
public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory> public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory>
{ {
private static long _lastRelease; private static long _lastRelease;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly ILogger<ReleaseMemoryHandler> _logger; private readonly ILogger<ReleaseMemoryHandler> _logger;
@ -31,12 +31,12 @@ public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory>
if (request.ForceAggressive || !hasActiveWorkers) if (request.ForceAggressive || !hasActiveWorkers)
{ {
_logger.LogDebug("Starting aggressive garbage collection"); _logger.LogDebug("Starting aggressive garbage collection");
GC.Collect(2, GCCollectionMode.Aggressive, blocking: true, compacting: true); GC.Collect(2, GCCollectionMode.Aggressive, true, true);
} }
else else
{ {
_logger.LogDebug("Starting garbage collection"); _logger.LogDebug("Starting garbage collection");
GC.Collect(2, GCCollectionMode.Forced, blocking: false); GC.Collect(2, GCCollectionMode.Forced, false);
} }
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();

2
ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs

@ -20,7 +20,7 @@ public class CreateCollectionHandler :
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Collection> validation = await Validate(dbContext, request); Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c)); return await validation.Apply(c => PersistCollection(dbContext, c));
} }
private static async Task<MediaCollectionViewModel> PersistCollection( private static async Task<MediaCollectionViewModel> PersistCollection(

2
ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs

@ -20,7 +20,7 @@ public class CreateMultiCollectionHandler :
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request); Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c)); return await validation.Apply(c => PersistCollection(dbContext, c));
} }
private static async Task<MultiCollectionViewModel> PersistCollection( private static async Task<MultiCollectionViewModel> PersistCollection(

2
ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs

@ -20,7 +20,7 @@ public class CreateSmartCollectionHandler :
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, SmartCollection> validation = await Validate(dbContext, request); Validation<BaseError, SmartCollection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c)); return await validation.Apply(c => PersistCollection(dbContext, c));
} }
private static async Task<SmartCollectionViewModel> PersistCollection( private static async Task<SmartCollectionViewModel> PersistCollection(

2
ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs

@ -20,7 +20,7 @@ public class DeleteCollectionHandler : IRequestHandler<DeleteCollection, Either<
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Collection> validation = await CollectionMustExist(dbContext, request); Validation<BaseError, Collection> validation = await CollectionMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); return await validation.Apply(c => DoDeletion(dbContext, c));
} }
private static Task<Unit> DoDeletion(TvContext dbContext, Collection collection) private static Task<Unit> DoDeletion(TvContext dbContext, Collection collection)

2
ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollectionHandler.cs

@ -20,7 +20,7 @@ public class DeleteMultiCollectionHandler : IRequestHandler<DeleteMultiCollectio
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, MultiCollection> validation = await MultiCollectionMustExist(dbContext, request); Validation<BaseError, MultiCollection> validation = await MultiCollectionMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); return await validation.Apply(c => DoDeletion(dbContext, c));
} }
private static Task<Unit> DoDeletion(TvContext dbContext, MultiCollection multiCollection) private static Task<Unit> DoDeletion(TvContext dbContext, MultiCollection multiCollection)

2
ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollectionHandler.cs

@ -20,7 +20,7 @@ public class DeleteSmartCollectionHandler : IRequestHandler<DeleteSmartCollectio
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, SmartCollection> validation = await SmartCollectionMustExist(dbContext, request); Validation<BaseError, SmartCollection> validation = await SmartCollectionMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); return await validation.Apply(c => DoDeletion(dbContext, c));
} }
private static Task<Unit> DoDeletion(TvContext dbContext, SmartCollection smartCollection) private static Task<Unit> DoDeletion(TvContext dbContext, SmartCollection smartCollection)

6
ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs

@ -11,10 +11,8 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetMediaItemInfoHandler(IDbContextFactory<TvContext> dbContextFactory) public GetMediaItemInfoHandler(IDbContextFactory<TvContext> dbContextFactory) =>
{
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, MediaItemInfo>> Handle( public async Task<Either<BaseError, MediaItemInfo>> Handle(
GetMediaItemInfo request, GetMediaItemInfo request,
@ -56,7 +54,7 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
JellyfinMediaSource jellyfinMediaSource => jellyfinMediaSource.ServerName, JellyfinMediaSource jellyfinMediaSource => jellyfinMediaSource.ServerName,
_ => null _ => null
}; };
return new MediaItemInfo( return new MediaItemInfo(
mediaItem.Id, mediaItem.Id,
mediaItem.GetType().Name, mediaItem.GetType().Name,

4
ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs

@ -71,7 +71,7 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
List<LibraryPath> libraryPaths = await dbContext.LibraryPaths List<LibraryPath> libraryPaths = await dbContext.LibraryPaths
.Filter(lp => lp.LibraryId == request.LibraryId) .Filter(lp => lp.LibraryId == request.LibraryId)
.ToListAsync(); .ToListAsync();
DateTime minDateTime = libraryPaths.Any() DateTime minDateTime = libraryPaths.Any()
? libraryPaths.Min(lp => lp.LastScan ?? SystemTime.MinValueUtc) ? libraryPaths.Min(lp => lp.LastScan ?? SystemTime.MinValueUtc)
: SystemTime.MaxValueUtc; : SystemTime.MaxValueUtc;
@ -90,6 +90,6 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
} }
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now); return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
} }
} }

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

@ -19,8 +19,8 @@ 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 IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IPlayoutBuilder _playoutBuilder; private readonly IPlayoutBuilder _playoutBuilder;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public BuildPlayoutHandler( public BuildPlayoutHandler(
IClient client, IClient client,
@ -108,7 +108,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
dbContext.Playouts dbContext.Playouts
.Include(p => p.Channel) .Include(p => p.Channel)
.Include(p => p.Items) .Include(p => p.Items)
.Include(p => p.ProgramScheduleAlternates) .Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule) .ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
@ -137,12 +136,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ThenInclude(a => a.ProgramSchedule) .ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors) .Include(p => p.ProgramScheduleAnchors)
.ThenInclude(psa => psa.EnumeratorState) .ThenInclude(psa => psa.EnumeratorState)
.Include(p => p.ProgramScheduleAnchors) .Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem) .ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule) .Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection) .ThenInclude(psi => psi.Collection)
@ -164,7 +161,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.Include(p => p.ProgramSchedule) .Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId) .SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
.Map(o => o.ToValidation<BaseError>("Playout does not exist.")); .Map(o => o.ToValidation<BaseError>("Playout does not exist."));
} }

2
ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs

@ -29,7 +29,7 @@ public class CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseEr
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request); Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await validation.Apply(playout => PersistPlayout(dbContext, playout)); return await LanguageExtensions.Apply(validation, playout => PersistPlayout(dbContext, playout));
} }
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout) private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout)

2
ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs

@ -11,9 +11,9 @@ namespace ErsatzTV.Application.Playouts;
public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Unit>> public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeletePlayoutHandler( public DeletePlayoutHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel, ChannelWriter<IBackgroundServiceRequest> workerChannel,

6
ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs

@ -12,8 +12,8 @@ namespace ErsatzTV.Application.Playouts;
public class ReplacePlayoutAlternateScheduleItemsHandler : public class ReplacePlayoutAlternateScheduleItemsHandler :
IRequestHandler<ReplacePlayoutAlternateScheduleItems, Either<BaseError, Unit>> IRequestHandler<ReplacePlayoutAlternateScheduleItems, Either<BaseError, Unit>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<ReplacePlayoutAlternateScheduleItemsHandler> _logger; private readonly ILogger<ReplacePlayoutAlternateScheduleItemsHandler> _logger;
public ReplacePlayoutAlternateScheduleItemsHandler( public ReplacePlayoutAlternateScheduleItemsHandler(
@ -46,7 +46,7 @@ public class ReplacePlayoutAlternateScheduleItemsHandler :
{ {
var existingScheduleMap = new Dictionary<DateTimeOffset, ProgramSchedule>(); var existingScheduleMap = new Dictionary<DateTimeOffset, ProgramSchedule>();
var daysToCheck = new List<DateTimeOffset>(); var daysToCheck = new List<DateTimeOffset>();
Option<PlayoutItem> maybeLastPlayoutItem = await dbContext.PlayoutItems Option<PlayoutItem> maybeLastPlayoutItem = await dbContext.PlayoutItems
.Filter(pi => pi.PlayoutId == request.PlayoutId) .Filter(pi => pi.PlayoutId == request.PlayoutId)
.OrderByDescending(pi => pi.Start) .OrderByDescending(pi => pi.Start)
@ -146,7 +146,7 @@ public class ReplacePlayoutAlternateScheduleItemsHandler :
} }
} }
} }
return Unit.Default; return Unit.Default;
} }
catch (Exception ex) catch (Exception ex)

2
ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs

@ -19,7 +19,7 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Playout> validation = await Validate(dbContext, request); Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, playout => ApplyUpdateRequest(dbContext, request, playout)); return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout));
} }
private static async Task<PlayoutNameViewModel> ApplyUpdateRequest( private static async Task<PlayoutNameViewModel> ApplyUpdateRequest(

13
ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs

@ -28,9 +28,10 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
ForceSynchronizePlexLibraryById request, ForceSynchronizePlexLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken); CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>.Handle( Task<Either<BaseError, string>> IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>.
SynchronizePlexLibraryByIdIfNeeded request, Handle(
CancellationToken cancellationToken) => Handle(request, cancellationToken); SynchronizePlexLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private async Task<Either<BaseError, string>> Handle( private async Task<Either<BaseError, string>> Handle(
ISynchronizePlexLibraryById request, ISynchronizePlexLibraryById request,
@ -77,10 +78,10 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
TvContext dbContext, TvContext dbContext,
ISynchronizePlexLibraryById request) ISynchronizePlexLibraryById request)
{ {
DateTime minDateTime = await dbContext.PlexLibraries DateTime minDateTime = await dbContext.PlexLibraries
.SelectOneAsync(l => l.Id, l => l.Id == request.PlexLibraryId) .SelectOneAsync(l => l.Id, l => l.Id == request.PlexLibraryId)
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc); .Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
return new DateTimeOffset(minDateTime, TimeSpan.Zero); return new DateTimeOffset(minDateTime, TimeSpan.Zero);
} }
@ -95,6 +96,6 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
} }
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now); return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
} }
} }

4
ErsatzTV.Application/ProgramSchedules/Commands/CopyProgramScheduleHandler.cs

@ -23,7 +23,9 @@ public class
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request); Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await validation.Apply(p => PerformCopy(dbContext, p, request, cancellationToken)); return await LanguageExtensions.Apply(
validation,
p => PerformCopy(dbContext, p, request, cancellationToken));
} }
catch (Exception ex) catch (Exception ex)
{ {

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

@ -19,7 +19,7 @@ public class DeleteProgramScheduleHandler : IRequestHandler<DeleteProgramSchedul
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, ProgramSchedule> validation = await ProgramScheduleMustExist(dbContext, request); Validation<BaseError, ProgramSchedule> validation = await ProgramScheduleMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => DoDeletion(dbContext, ps)); return await validation.Apply(ps => DoDeletion(dbContext, ps));
} }
private static Task<Unit> DoDeletion(TvContext dbContext, ProgramSchedule programSchedule) private static Task<Unit> DoDeletion(TvContext dbContext, ProgramSchedule programSchedule)

2
ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs

@ -19,13 +19,13 @@ namespace ErsatzTV.Application.Search;
public class public class
QuerySearchIndexEpisodesHandler : IRequestHandler<QuerySearchIndexEpisodes, TelevisionEpisodeCardResultsViewModel> QuerySearchIndexEpisodesHandler : IRequestHandler<QuerySearchIndexEpisodes, TelevisionEpisodeCardResultsViewModel>
{ {
private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEmbyPathReplacementService _embyPathReplacementService; private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IClient _client;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;

2
ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs

@ -10,9 +10,9 @@ namespace ErsatzTV.Application.Search;
public class QuerySearchIndexMoviesHandler : IRequestHandler<QuerySearchIndexMovies, MovieCardResultsViewModel> public class QuerySearchIndexMoviesHandler : IRequestHandler<QuerySearchIndexMovies, MovieCardResultsViewModel>
{ {
private readonly IClient _client;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
private readonly IClient _client;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
public QuerySearchIndexMoviesHandler( public QuerySearchIndexMoviesHandler(

2
ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs

@ -15,11 +15,11 @@ namespace ErsatzTV.Application.Search;
public class public class
QuerySearchIndexMusicVideosHandler : IRequestHandler<QuerySearchIndexMusicVideos, MusicVideoCardResultsViewModel> QuerySearchIndexMusicVideosHandler : IRequestHandler<QuerySearchIndexMusicVideos, MusicVideoCardResultsViewModel>
{ {
private readonly IClient _client;
private readonly IEmbyPathReplacementService _embyPathReplacementService; private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IMusicVideoRepository _musicVideoRepository; private readonly IMusicVideoRepository _musicVideoRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IClient _client;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
public QuerySearchIndexMusicVideosHandler( public QuerySearchIndexMusicVideosHandler(

2
ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs

@ -11,8 +11,8 @@ public class
QuerySearchIndexOtherVideosHandler : IRequestHandler<QuerySearchIndexOtherVideos, QuerySearchIndexOtherVideosHandler : IRequestHandler<QuerySearchIndexOtherVideos,
OtherVideoCardResultsViewModel> OtherVideoCardResultsViewModel>
{ {
private readonly IOtherVideoRepository _otherVideoRepository;
private readonly IClient _client; private readonly IClient _client;
private readonly IOtherVideoRepository _otherVideoRepository;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
public QuerySearchIndexOtherVideosHandler( public QuerySearchIndexOtherVideosHandler(

2
ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs

@ -11,8 +11,8 @@ namespace ErsatzTV.Application.Search;
public class public class
QuerySearchIndexSeasonsHandler : IRequestHandler<QuerySearchIndexSeasons, TelevisionSeasonCardResultsViewModel> QuerySearchIndexSeasonsHandler : IRequestHandler<QuerySearchIndexSeasons, TelevisionSeasonCardResultsViewModel>
{ {
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IClient _client; private readonly IClient _client;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;

2
ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs

@ -11,8 +11,8 @@ namespace ErsatzTV.Application.Search;
public class public class
QuerySearchIndexShowsHandler : IRequestHandler<QuerySearchIndexShows, TelevisionShowCardResultsViewModel> QuerySearchIndexShowsHandler : IRequestHandler<QuerySearchIndexShows, TelevisionShowCardResultsViewModel>
{ {
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IClient _client; private readonly IClient _client;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex; private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;

4
ErsatzTV.Application/Search/Queries/SearchCollectionsHandler.cs

@ -12,7 +12,9 @@ public class SearchCollectionsHandler : IRequestHandler<SearchCollections, List<
public SearchCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) => public SearchCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
public async Task<List<MediaCollectionViewModel>> Handle(SearchCollections request, CancellationToken cancellationToken) public async Task<List<MediaCollectionViewModel>> Handle(
SearchCollections request,
CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Collections.FromSqlRaw( return await dbContext.Collections.FromSqlRaw(

4
ErsatzTV.Application/Search/Queries/SearchMultiCollectionsHandler.cs

@ -12,7 +12,9 @@ public class SearchMultiCollectionsHandler : IRequestHandler<SearchMultiCollecti
public SearchMultiCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) => public SearchMultiCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
public async Task<List<MultiCollectionViewModel>> Handle(SearchMultiCollections request, CancellationToken cancellationToken) public async Task<List<MultiCollectionViewModel>> Handle(
SearchMultiCollections request,
CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.MultiCollections.FromSqlRaw( return await dbContext.MultiCollections.FromSqlRaw(

4
ErsatzTV.Application/Search/Queries/SearchSmartCollectionsHandler.cs

@ -12,7 +12,9 @@ public class SearchSmartCollectionsHandler : IRequestHandler<SearchSmartCollecti
public SearchSmartCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) => public SearchSmartCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
public async Task<List<SmartCollectionViewModel>> Handle(SearchSmartCollections request, CancellationToken cancellationToken) public async Task<List<SmartCollectionViewModel>> Handle(
SearchSmartCollections request,
CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.SmartCollections.FromSqlRaw( return await dbContext.SmartCollections.FromSqlRaw(

4
ErsatzTV.Application/Search/Queries/SearchTelevisionSeasonsHandler.cs

@ -12,7 +12,9 @@ public class SearchTelevisionSeasonsHandler : IRequestHandler<SearchTelevisionSe
public SearchTelevisionSeasonsHandler(IDbContextFactory<TvContext> dbContextFactory) => public SearchTelevisionSeasonsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
public async Task<List<NamedMediaItemViewModel>> Handle(SearchTelevisionSeasons request, CancellationToken cancellationToken) public async Task<List<NamedMediaItemViewModel>> Handle(
SearchTelevisionSeasons request,
CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<TelevisionSeason>( return await dbContext.Connection.QueryAsync<TelevisionSeason>(

4
ErsatzTV.Application/Search/Queries/SearchTelevisionShowsHandler.cs

@ -12,7 +12,9 @@ public class SearchTelevisionShowsHandler : IRequestHandler<SearchTelevisionShow
public SearchTelevisionShowsHandler(IDbContextFactory<TvContext> dbContextFactory) => public SearchTelevisionShowsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
public async Task<List<NamedMediaItemViewModel>> Handle(SearchTelevisionShows request, CancellationToken cancellationToken) public async Task<List<NamedMediaItemViewModel>> Handle(
SearchTelevisionShows request,
CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<TelevisionShow>( return await dbContext.Connection.QueryAsync<TelevisionShow>(

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

@ -16,11 +16,11 @@ namespace ErsatzTV.Application.Streaming;
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>> public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<StartFFmpegSessionHandler> _logger; private readonly ILogger<StartFFmpegSessionHandler> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public StartFFmpegSessionHandler( public StartFFmpegSessionHandler(
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
@ -104,7 +104,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_logger.LogDebug("Playlist exists"); _logger.LogDebug("Playlist exists");
var segmentCount = 0; var segmentCount = 0;
var lastSegmentCount = -1; int lastSegmentCount = -1;
while (DateTimeOffset.Now < finish && segmentCount < initialSegmentCount) while (DateTimeOffset.Now < finish && segmentCount < initialSegmentCount)
{ {
if (segmentCount != lastSegmentCount) if (segmentCount != lastSegmentCount)

2
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -55,7 +55,7 @@ public class HlsSessionWorker : IHlsSessionWorker
lock (_sync) lock (_sync)
{ {
// _logger.LogDebug("Keep alive - session worker for channel {ChannelNumber}", _channelNumber); // _logger.LogDebug("Keep alive - session worker for channel {ChannelNumber}", _channelNumber);
_lastAccess = DateTimeOffset.Now; _lastAccess = DateTimeOffset.Now;
_timer?.Stop(); _timer?.Stop();

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

@ -465,7 +465,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
break; break;
} }
// attempt to remotely stream jellyfin // attempt to remotely stream jellyfin
Option<string> jellyfinItemId = playoutItem.MediaItem switch Option<string> jellyfinItemId = playoutItem.MediaItem switch
{ {
@ -480,7 +480,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
playoutItem, playoutItem,
$"http://localhost:{Settings.ListenPort}/media/jellyfin/{itemId}"); $"http://localhost:{Settings.ListenPort}/media/jellyfin/{itemId}");
} }
// attempt to remotely stream emby // attempt to remotely stream emby
Option<string> embyItemId = playoutItem.MediaItem switch Option<string> embyItemId = playoutItem.MediaItem switch
{ {

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

@ -20,9 +20,9 @@ namespace ErsatzTV.Application.Subtitles;
public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSubtitles, Either<BaseError, Unit>> public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSubtitles, Either<BaseError, Unit>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<ExtractEmbeddedSubtitlesHandler> _logger; private readonly ILogger<ExtractEmbeddedSubtitlesHandler> _logger;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public ExtractEmbeddedSubtitlesHandler( public ExtractEmbeddedSubtitlesHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@ -177,7 +177,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter( .Filter(
em => em.Subtitles.Any( em => em.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(em => em.EpisodeId) .Map(em => em.EpisodeId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
result.AddRange(episodeIds); result.AddRange(episodeIds);
@ -188,7 +189,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter( .Filter(
mm => mm.Subtitles.Any( mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(mm => mm.MovieId) .Map(mm => mm.MovieId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
result.AddRange(movieIds); result.AddRange(movieIds);
@ -199,7 +201,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter( .Filter(
mm => mm.Subtitles.Any( mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(mm => mm.MusicVideoId) .Map(mm => mm.MusicVideoId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
result.AddRange(musicVideoIds); result.AddRange(musicVideoIds);
@ -210,7 +213,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter( .Filter(
ovm => ovm.Subtitles.Any( ovm => ovm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(ovm => ovm.OtherVideoId) .Map(ovm => ovm.OtherVideoId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
result.AddRange(otherVideoIds); result.AddRange(otherVideoIds);

12
ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs

@ -12,10 +12,8 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetSubtitlePathByIdHandler(IDbContextFactory<TvContext> dbContextFactory) public GetSubtitlePathByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
{
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, string>> Handle( public async Task<Either<BaseError, string>> Handle(
GetSubtitlePathById request, GetSubtitlePathById request,
@ -34,7 +32,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
{ {
return jellyfinUrl; return jellyfinUrl;
} }
foreach (string embyUrl in await GetEmbyUrl(request, dbContext, maybeSubtitle)) foreach (string embyUrl in await GetEmbyUrl(request, dbContext, maybeSubtitle))
{ {
return embyUrl; return embyUrl;
@ -125,10 +123,10 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
return $"http://localhost:{Settings.ListenPort}/media/jellyfin/{subtitlePath}"; return $"http://localhost:{Settings.ListenPort}/media/jellyfin/{subtitlePath}";
} }
} }
return Option<string>.None; return Option<string>.None;
} }
private static async Task<Option<string>> GetEmbyUrl( private static async Task<Option<string>> GetEmbyUrl(
GetSubtitlePathById request, GetSubtitlePathById request,
TvContext dbContext, TvContext dbContext,
@ -165,7 +163,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
return $"http://localhost:{Settings.ListenPort}/media/emby/{subtitlePath}"; return $"http://localhost:{Settings.ListenPort}/media/emby/{subtitlePath}";
} }
} }
return Option<string>.None; return Option<string>.None;
} }
} }

5
ErsatzTV.Application/Templates/Queries/GetMusicVideoCreditTemplatesHandler.cs

@ -7,10 +7,7 @@ public class GetMusicVideoCreditTemplatesHandler : IRequestHandler<GetMusicVideo
{ {
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
public GetMusicVideoCreditTemplatesHandler(ILocalFileSystem localFileSystem) public GetMusicVideoCreditTemplatesHandler(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
{
_localFileSystem = localFileSystem;
}
public Task<List<string>> Handle(GetMusicVideoCreditTemplates request, CancellationToken cancellationToken) => public Task<List<string>> Handle(GetMusicVideoCreditTemplates request, CancellationToken cancellationToken) =>
_localFileSystem.ListFiles(FileSystemLayout.MusicVideoCreditsTemplatesFolder) _localFileSystem.ListFiles(FileSystemLayout.MusicVideoCreditsTemplatesFolder)

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

@ -9,7 +9,6 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Runtime;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -17,12 +16,12 @@ namespace ErsatzTV.Application.Troubleshooting.Queries;
public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingInfo, TroubleshootingInfo> public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingInfo, TroubleshootingInfo>
{ {
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IHealthCheckService _healthCheckService;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory; private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
private readonly IConfigElementRepository _configElementRepository; private readonly IHealthCheckService _healthCheckService;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly IRuntimeInfo _runtimeInfo;
public GetTroubleshootingInfoHandler( public GetTroubleshootingInfoHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@ -105,7 +104,8 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
Optional(GetDriverName(activeDriver)), Optional(GetDriverName(activeDriver)),
vaapiDevice)) vaapiDevice))
{ {
vaapiCapabilities += $"Checking driver {activeDriver} device {vaapiDevice}{Environment.NewLine}{Environment.NewLine}"; vaapiCapabilities +=
$"Checking driver {activeDriver} device {vaapiDevice}{Environment.NewLine}{Environment.NewLine}";
vaapiCapabilities += output; vaapiCapabilities += output;
vaapiCapabilities += Environment.NewLine + Environment.NewLine; vaapiCapabilities += Environment.NewLine + Environment.NewLine;
} }
@ -124,7 +124,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
nvidiaCapabilities, nvidiaCapabilities,
vaapiCapabilities); vaapiCapabilities);
} }
// lifted from GetFFmpegSettingsHandler // lifted from GetFFmpegSettingsHandler
private async Task<FFmpegSettingsViewModel> GetFFmpegSettings() private async Task<FFmpegSettingsViewModel> GetFFmpegSettings()
{ {

2
ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs

@ -18,7 +18,7 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, ChannelWatermark> validation = Validate(request); Validation<BaseError, ChannelWatermark> validation = Validate(request);
return await LanguageExtensions.Apply(validation, profile => PersistChannelWatermark(dbContext, profile)); return await validation.Apply(profile => PersistChannelWatermark(dbContext, profile));
} }
private static async Task<CreateWatermarkResult> PersistChannelWatermark( private static async Task<CreateWatermarkResult> PersistChannelWatermark(

2
ErsatzTV.Application/Watermarks/Commands/DeleteWatermarkHandler.cs

@ -19,7 +19,7 @@ public class DeleteWatermarkHandler : IRequestHandler<DeleteWatermark, Either<Ba
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, ChannelWatermark> validation = await WatermarkMustExist(dbContext, request); Validation<BaseError, ChannelWatermark> validation = await WatermarkMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p)); return await validation.Apply(p => DoDeletion(dbContext, p));
} }
private static async Task<Unit> DoDeletion(TvContext dbContext, ChannelWatermark watermark) private static async Task<Unit> DoDeletion(TvContext dbContext, ChannelWatermark watermark)

2
ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs

@ -1,8 +1,8 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Jellyfin;
using ErsatzTV.FFmpeg.Runtime;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;

2
ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs

@ -70,7 +70,7 @@ public class FallbackMetadataProviderTests
// metadata.Season.Should().Be(season); // metadata.Season.Should().Be(season);
metadata.Head().EpisodeNumber.Should().Be(episode); metadata.Head().EpisodeNumber.Should().Be(episode);
} }
[TestCase("Awesome Show - S01_BLAH.mkv", 0)] [TestCase("Awesome Show - S01_BLAH.mkv", 0)]
[TestCase("Awesome Show - NO_EPISODE_NUMBER_HERE.mkv", 0)] [TestCase("Awesome Show - NO_EPISODE_NUMBER_HERE.mkv", 0)]
public void GetFallbackMetadata_ShouldHandleNonEpisodes(string path, int episode) public void GetFallbackMetadata_ShouldHandleNonEpisodes(string path, int episode)

2
ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs

@ -1,8 +1,8 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Core.Plex; using ErsatzTV.Core.Plex;
using ErsatzTV.FFmpeg.Runtime;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;

11
ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs

@ -8,14 +8,11 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class ChronologicalContentTests public class ChronologicalContentTests
{ {
private CancellationToken _cancellationToken;
[SetUp] [SetUp]
public void SetUp() public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private CancellationToken _cancellationToken;
}
[Test] [Test]
public void Episodes_Should_Sort_By_Aired() public void Episodes_Should_Sort_By_Aired()
{ {

7
ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs

@ -8,12 +8,9 @@ namespace ErsatzTV.Core.Tests.Scheduling;
public class CustomOrderContentTests public class CustomOrderContentTests
{ {
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
[SetUp] [SetUp]
public void SetUp() public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
}
[Test] [Test]
public void MediaItems_Should_Sort_By_CustomOrder() public void MediaItems_Should_Sort_By_CustomOrder()

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

@ -17,6 +17,9 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class PlayoutBuilderTests public class PlayoutBuilderTests
{ {
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private readonly ILogger<PlayoutBuilder> _logger; private readonly ILogger<PlayoutBuilder> _logger;
public PlayoutBuilderTests() public PlayoutBuilderTests()
@ -33,12 +36,6 @@ public class PlayoutBuilderTests
} }
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
[SetUp]
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
}
[TestFixture] [TestFixture]
public class NewPlayout : PlayoutBuilderTests public class NewPlayout : PlayoutBuilderTests
@ -2369,7 +2366,7 @@ public class PlayoutBuilderTests
public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays() public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays()
{ {
var mediaItems = new List<MediaItem>(); var mediaItems = new List<MediaItem>();
for (int i = 1; i <= 25; i++) for (var i = 1; i <= 25; i++)
{ {
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
} }
@ -2389,11 +2386,13 @@ public class PlayoutBuilderTests
.First(); .First();
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0); lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
lastCheckpoint.EnumeratorState.Index.Should().Be(3); lastCheckpoint.EnumeratorState.Index.Should().Be(3);
// we need to mess up the ordering to trigger the problematic behavior // we need to mess up the ordering to trigger the problematic behavior
// this simulates the way the rows are loaded with EF // this simulates the way the rows are loaded with EF
PlayoutProgramScheduleAnchor oldest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).Last(); PlayoutProgramScheduleAnchor oldest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate)
PlayoutProgramScheduleAnchor newest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).First(); .Last();
PlayoutProgramScheduleAnchor newest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate)
.First();
result.ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor> result.ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>
{ {
@ -2416,11 +2415,11 @@ public class PlayoutBuilderTests
PlayoutProgramScheduleAnchor continueAnchor = PlayoutProgramScheduleAnchor continueAnchor =
result2.ProgramScheduleAnchors.First(x => x.AnchorDate is null); result2.ProgramScheduleAnchors.First(x => x.AnchorDate is null);
int secondSeedValue = continueAnchor.EnumeratorState.Seed; int secondSeedValue = continueAnchor.EnumeratorState.Seed;
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run // the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
firstSeedValue.Should().Be(secondSeedValue); firstSeedValue.Should().Be(secondSeedValue);
} }
[Test] [Test]
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed() public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed()
{ {
@ -2440,7 +2439,8 @@ public class PlayoutBuilderTests
result.Items.Count.Should().Be(6); result.Items.Count.Should().Be(6);
result.ProgramScheduleAnchors.Count.Should().Be(2); result.ProgramScheduleAnchors.Count.Should().Be(2);
PlayoutProgramScheduleAnchor primaryAnchor = result.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); PlayoutProgramScheduleAnchor primaryAnchor =
result.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
primaryAnchor.EnumeratorState.Seed.Should().BeGreaterThan(0); primaryAnchor.EnumeratorState.Seed.Should().BeGreaterThan(0);
primaryAnchor.EnumeratorState.Index.Should().Be(0); primaryAnchor.EnumeratorState.Index.Should().Be(0);
@ -2463,12 +2463,12 @@ public class PlayoutBuilderTests
primaryAnchor.EnumeratorState.Index.Should().Be(0); primaryAnchor.EnumeratorState.Index.Should().Be(0);
} }
[Test] [Test]
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays() public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays()
{ {
var mediaItems = new List<MediaItem>(); var mediaItems = new List<MediaItem>();
for (int i = 1; i <= 100; i++) for (var i = 1; i <= 100; i++)
{ {
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
} }
@ -2490,7 +2490,7 @@ public class PlayoutBuilderTests
.First(); .First();
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0); lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
lastCheckpoint.EnumeratorState.Index.Should().Be(53); lastCheckpoint.EnumeratorState.Index.Should().Be(53);
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed; int firstSeedValue = lastCheckpoint.EnumeratorState.Seed;
for (var i = 1; i < 20; i++) for (var i = 1; i < 20; i++)
@ -2866,7 +2866,7 @@ public class PlayoutBuilderTests
Collection = mediaCollection, Collection = mediaCollection,
CollectionId = mediaCollection.Id, CollectionId = mediaCollection.Id,
StartTime = null, StartTime = null,
PlaybackOrder = playbackOrder, PlaybackOrder = playbackOrder
}; };
private static ProgramScheduleItem Flood( private static ProgramScheduleItem Flood(
@ -2950,7 +2950,7 @@ public class PlayoutBuilderTests
return new TestData(builder, playout); return new TestData(builder, playout);
} }
private TestData TestDataFloodForSmartCollectionItems( private TestData TestDataFloodForSmartCollectionItems(
List<MediaItem> mediaItems, List<MediaItem> mediaItems,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,

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

@ -13,9 +13,6 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class PlayoutModeSchedulerBaseTests : SchedulerTestBase public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
{ {
private CancellationToken _cancellationToken;
private PlayoutModeSchedulerBase<ProgramScheduleItem> _scheduler;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
@ -23,6 +20,9 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
_scheduler = new TestScheduler(); _scheduler = new TestScheduler();
} }
private CancellationToken _cancellationToken;
private PlayoutModeSchedulerBase<ProgramScheduleItem> _scheduler;
[TestFixture] [TestFixture]
public class CalculateEndTimeWithFiller : PlayoutModeSchedulerBaseTests public class CalculateEndTimeWithFiller : PlayoutModeSchedulerBaseTests
{ {
@ -369,14 +369,14 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
} }
[Test] [Test]
public void Should_Schedule_Post_Roll_After_Padded_Mid_Roll() public void Should_Schedule_Post_Roll_After_Padded_Mid_Roll()
{ {
// content 45 min, mid roll pad to 60, post roll 5 min // content 45 min, mid roll pad to 60, post roll 5 min
// content + post = 50 min, mid roll will add two 5 min items // content + post = 50 min, mid roll will add two 5 min items
// content + mid + post = 60 min // content + mid + post = 60 min
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45)); Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5)); Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5));
@ -453,35 +453,35 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
_cancellationToken); _cancellationToken);
playoutItems.Count.Should().Be(5); playoutItems.Count.Should().Be(5);
// content chapter 1 // content chapter 1
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
// mid-roll 1 // mid-roll 1
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6));
// mid-roll 2 // mid-roll 2
playoutItems[2].MediaItemId.Should().Be(4); playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
// content chapter 2 // content chapter 2
playoutItems[3].MediaItemId.Should().Be(1); playoutItems[3].MediaItemId.Should().Be(1);
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(16)); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(16));
// post-roll // post-roll
playoutItems[4].MediaItemId.Should().Be(5); playoutItems[4].MediaItemId.Should().Be(5);
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(55)); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(55));
} }
[Test] [Test]
public void Should_Schedule_Padded_Post_Roll_After_Mid_Roll_Count() public void Should_Schedule_Padded_Post_Roll_After_Mid_Roll_Count()
{ {
// content 45 min, mid roll 5 min, post roll pad to 60 // content 45 min, mid roll 5 min, post roll pad to 60
// content + mid = 50 min, post roll will add two 5 min items // content + mid = 50 min, post roll will add two 5 min items
// content + mid + post = 60 min // content + mid + post = 60 min
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45)); Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5)); Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5));
@ -558,15 +558,15 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
_cancellationToken); _cancellationToken);
playoutItems.Count.Should().Be(5); playoutItems.Count.Should().Be(5);
// content chapter 1 // content chapter 1
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
// mid-roll 1 // mid-roll 1
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6));
// content chapter 2 // content chapter 2
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
@ -574,7 +574,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
// post-roll 1 // post-roll 1
playoutItems[3].MediaItemId.Should().Be(5); playoutItems[3].MediaItemId.Should().Be(5);
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(50)); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(50));
// post-roll 2 // post-roll 2
playoutItems[4].MediaItemId.Should().Be(6); playoutItems[4].MediaItemId.Should().Be(6);
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(55)); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(55));

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

@ -11,13 +11,10 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class PlayoutModeSchedulerDurationTests : SchedulerTestBase public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
{ {
private CancellationToken _cancellationToken;
[SetUp] [SetUp]
public void SetUp() public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private CancellationToken _cancellationToken;
}
[Test] [Test]
public void Should_Fill_Exact_Duration() public void Should_Fill_Exact_Duration()
@ -301,7 +298,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
// offline should not set guide finish // offline should not set guide finish
playoutItems[2].GuideFinish.HasValue.Should().BeFalse(); playoutItems[2].GuideFinish.HasValue.Should().BeFalse();
} }

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

@ -11,13 +11,10 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class PlayoutModeSchedulerFloodTests : SchedulerTestBase public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
{ {
private CancellationToken _cancellationToken;
[SetUp] [SetUp]
public void SetUp() public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private CancellationToken _cancellationToken;
}
[Test] [Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item() public void Should_Fill_Exactly_To_Next_Schedule_Item()
@ -94,7 +91,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].CustomTitle.Should().Be("CustomTitle"); playoutItems[2].CustomTitle.Should().Be("CustomTitle");
} }
[Test] [Test]
public void Should_Schedule_Single_Item_Fixed_Start_Flood() public void Should_Schedule_Single_Item_Fixed_Start_Flood()
{ {
@ -174,13 +171,13 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[3].GuideGroup.Should().Be(1); playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.None); playoutItems[3].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].CustomTitle.Should().Be("CustomTitle"); playoutItems[3].CustomTitle.Should().Be("CustomTitle");
playoutItems[4].MediaItemId.Should().Be(1); playoutItems[4].MediaItemId.Should().Be(1);
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(4)); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(4));
playoutItems[4].GuideGroup.Should().Be(1); playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.None); playoutItems[4].FillerKind.Should().Be(FillerKind.None);
playoutItems[4].CustomTitle.Should().Be("CustomTitle"); playoutItems[4].CustomTitle.Should().Be("CustomTitle");
playoutItems[5].MediaItemId.Should().Be(2); playoutItems[5].MediaItemId.Should().Be(2);
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.AddHours(5)); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.AddHours(5));
playoutItems[5].GuideGroup.Should().Be(1); playoutItems[5].GuideGroup.Should().Be(1);
@ -840,7 +837,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[6].GuideGroup.Should().Be(3); playoutItems[6].GuideGroup.Should().Be(3);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
} }
[Test] [Test]
public void Should_Not_Schedule_Fallback_Filler_Incomplete_Flood() public void Should_Not_Schedule_Fallback_Filler_Incomplete_Flood()
{ {
@ -885,7 +882,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
// hard stop at 2, an hour before the "next schedule item" at 3 // hard stop at 2, an hour before the "next schedule item" at 3
DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2); DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2);
@ -946,7 +943,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[5].GuideGroup.Should().Be(6); playoutItems[5].GuideGroup.Should().Be(6);
playoutItems[5].FillerKind.Should().Be(FillerKind.None); playoutItems[5].FillerKind.Should().Be(FillerKind.None);
} }
[Test] [Test]
public void Should_Not_Schedule_Tail_Filler_Incomplete_Flood() public void Should_Not_Schedule_Tail_Filler_Incomplete_Flood()
{ {
@ -991,7 +988,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
// hard stop at 2, an hour before the "next schedule item" at 3 // hard stop at 2, an hour before the "next schedule item" at 3
DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2); DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2);

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

@ -11,13 +11,10 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
{ {
private CancellationToken _cancellationToken;
[SetUp] [SetUp]
public void SetUp() public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private CancellationToken _cancellationToken;
}
[Test] [Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item() public void Should_Fill_Exactly_To_Next_Schedule_Item()

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

@ -11,13 +11,10 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class PlayoutModeSchedulerOneTests : SchedulerTestBase public class PlayoutModeSchedulerOneTests : SchedulerTestBase
{ {
private CancellationToken _cancellationToken;
[SetUp] [SetUp]
public void SetUp() public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private CancellationToken _cancellationToken;
}
[Test] [Test]
public void Should_Have_Gap_With_No_Tail_No_Fallback() public void Should_Have_Gap_With_No_Tail_No_Fallback()

9
ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs

@ -8,6 +8,9 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class RandomizedContentTests public class RandomizedContentTests
{ {
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private const int KnownSeed = 22295; private const int KnownSeed = 22295;
private readonly List<int> _expected = new() private readonly List<int> _expected = new()
@ -18,12 +21,6 @@ public class RandomizedContentTests
}; };
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
[SetUp]
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
}
[Test] [Test]
public void Episodes_Should_Randomize() public void Episodes_Should_Randomize()

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

@ -30,10 +30,12 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[Explicit] [Explicit]
public class ScheduleIntegrationTests public class ScheduleIntegrationTests
{ {
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token;
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
public ScheduleIntegrationTests() public ScheduleIntegrationTests() =>
{
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() .MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
@ -41,13 +43,6 @@ public class ScheduleIntegrationTests
.WriteTo.Console() .WriteTo.Console()
.Destructure.UsingAttributes() .Destructure.UsingAttributes()
.CreateLogger(); .CreateLogger();
}
[SetUp]
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token;
}
[Test] [Test]
public async Task TestExistingData() public async Task TestExistingData()
@ -82,13 +77,13 @@ public class ScheduleIntegrationTests
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure"); o.MigrationsAssembly("ErsatzTV.Infrastructure");
})); }));
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler()); SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
SqlMapper.AddTypeHandler(new GuidHandler()); SqlMapper.AddTypeHandler(new GuidHandler());
SqlMapper.AddTypeHandler(new TimeSpanHandler()); SqlMapper.AddTypeHandler(new TimeSpanHandler());
services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory())); services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory()));
services.AddScoped<ISearchRepository, SearchRepository>(); services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<ICachingSearchRepository, CachingSearchRepository>(); services.AddScoped<ICachingSearchRepository, CachingSearchRepository>();
services.AddScoped<IConfigElementRepository, ConfigElementRepository>(); services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
@ -119,7 +114,7 @@ public class ScheduleIntegrationTests
await searchIndex.Rebuild( await searchIndex.Rebuild(
provider.GetRequiredService<ICachingSearchRepository>(), provider.GetRequiredService<ICachingSearchRepository>(),
provider.GetRequiredService<IFallbackMetadataProvider>()); provider.GetRequiredService<IFallbackMetadataProvider>());
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
new ConfigElementRepository(factory), new ConfigElementRepository(factory),
new MediaCollectionRepository(new Mock<IClient>().Object, searchIndex, factory), new MediaCollectionRepository(new Mock<IClient>().Object, searchIndex, factory),
@ -140,10 +135,10 @@ public class ScheduleIntegrationTests
await context.SaveChangesAsync(_cancellationToken); await context.SaveChangesAsync(_cancellationToken);
} }
for (var i = 1; i <= (24 * 1); i++) for (var i = 1; i <= 24 * 1; i++)
{ {
await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken); await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken);
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID); Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe(); Playout playout = maybePlayout.ValueUnsafe();
@ -156,11 +151,11 @@ public class ScheduleIntegrationTests
await context.SaveChangesAsync(_cancellationToken); await context.SaveChangesAsync(_cancellationToken);
} }
for (var i = 25; i <= 26; i++) for (var i = 25; i <= 26; i++)
{ {
await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken); await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken);
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID); Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe(); Playout playout = maybePlayout.ValueUnsafe();
@ -204,7 +199,7 @@ public class ScheduleIntegrationTests
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure"); o.MigrationsAssembly("ErsatzTV.Infrastructure");
})); }));
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler()); SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
SqlMapper.AddTypeHandler(new GuidHandler()); SqlMapper.AddTypeHandler(new GuidHandler());
SqlMapper.AddTypeHandler(new TimeSpanHandler()); SqlMapper.AddTypeHandler(new TimeSpanHandler());
@ -226,7 +221,7 @@ public class ScheduleIntegrationTests
{ {
Path = "Test LibraryPath" Path = "Test LibraryPath"
}; };
var library = new LocalLibrary var library = new LocalLibrary
{ {
MediaKind = LibraryMediaKind.Movies, MediaKind = LibraryMediaKind.Movies,
@ -236,7 +231,7 @@ public class ScheduleIntegrationTests
await dbContext.Libraries.AddAsync(library, _cancellationToken); await dbContext.Libraries.AddAsync(library, _cancellationToken);
await dbContext.SaveChangesAsync(_cancellationToken); await dbContext.SaveChangesAsync(_cancellationToken);
var movies = new List<Movie>(); var movies = new List<Movie>();
for (var i = 1; i < 25; i++) for (var i = 1; i < 25; i++)
{ {
@ -287,9 +282,9 @@ public class ScheduleIntegrationTests
int playoutId = await AddTestData(dbContext, scheduleItems); int playoutId = await AddTestData(dbContext, scheduleItems);
DateTimeOffset start = new DateTimeOffset(2022, 7, 26, 8, 0, 5, TimeSpan.FromHours(-5)); var start = new DateTimeOffset(2022, 7, 26, 8, 0, 5, TimeSpan.FromHours(-5));
DateTimeOffset finish = start.AddDays(2); DateTimeOffset finish = start.AddDays(2);
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
new ConfigElementRepository(factory), new ConfigElementRepository(factory),
new MediaCollectionRepository(new Mock<IClient>().Object, new Mock<ISearchIndex>().Object, factory), new MediaCollectionRepository(new Mock<IClient>().Object, new Mock<ISearchIndex>().Object, factory),
@ -299,10 +294,10 @@ public class ScheduleIntegrationTests
new Mock<ILocalFileSystem>().Object, new Mock<ILocalFileSystem>().Object,
provider.GetRequiredService<ILogger<PlayoutBuilder>>()); provider.GetRequiredService<ILogger<PlayoutBuilder>>());
for (var i = 0; i <= (24 * 4); i++) for (var i = 0; i <= 24 * 4; i++)
{ {
await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken); await using TvContext context = await factory.CreateDbContextAsync(_cancellationToken);
Option<Playout> maybePlayout = await GetPlayout(context, playoutId); Option<Playout> maybePlayout = await GetPlayout(context, playoutId);
Playout playout = maybePlayout.ValueUnsafe(); Playout playout = maybePlayout.ValueUnsafe();
@ -326,7 +321,7 @@ public class ScheduleIntegrationTests
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile); await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
var channel = new Channel(Guid.Parse("00000000-0000-0000-0000-000000000001")) var channel = new Channel(Guid.Parse("00000000-0000-0000-0000-000000000001"))
{ {
Name = "Test Channel", Name = "Test Channel",
@ -360,12 +355,10 @@ public class ScheduleIntegrationTests
return playout.Id; return playout.Id;
} }
private static async Task<Option<Playout>> GetPlayout(TvContext dbContext, int playoutId) private static async Task<Option<Playout>> GetPlayout(TvContext dbContext, int playoutId) =>
{ await dbContext.Playouts
return await dbContext.Playouts
.Include(p => p.Channel) .Include(p => p.Channel)
.Include(p => p.Items) .Include(p => p.Items)
.Include(p => p.ProgramScheduleAlternates) .Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule) .ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
@ -394,12 +387,10 @@ public class ScheduleIntegrationTests
.ThenInclude(a => a.ProgramSchedule) .ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors) .Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.EnumeratorState) .ThenInclude(a => a.EnumeratorState)
.Include(p => p.ProgramScheduleAnchors) .Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem) .ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule) .Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection) .ThenInclude(psi => psi.Collection)
@ -422,5 +413,4 @@ public class ScheduleIntegrationTests
.ThenInclude(ps => ps.Items) .ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller) .ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == playoutId); .SelectOneAsync(p => p.Id, p => p.Id == playoutId);
}
} }

9
ErsatzTV.Core.Tests/Scheduling/SeasonEpisodeContentTests.cs

@ -8,13 +8,10 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class SeasonEpisodeContentTests public class SeasonEpisodeContentTests
{ {
private CancellationToken _cancellationToken;
[SetUp] [SetUp]
public void SetUp() public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private CancellationToken _cancellationToken;
}
[Test] [Test]
public void Episodes_Should_Sort_By_EpisodeNumber() public void Episodes_Should_Sort_By_EpisodeNumber()

9
ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs

@ -8,16 +8,13 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class ShuffledContentTests public class ShuffledContentTests
{ {
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
// this seed will produce (shuffle) 1-10 in order // this seed will produce (shuffle) 1-10 in order
private const int MagicSeed = 670596; private const int MagicSeed = 670596;
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
[SetUp]
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
}
[Test] [Test]
public void Episodes_Should_Not_Duplicate_When_Reshuffling() public void Episodes_Should_Not_Duplicate_When_Reshuffling()

11
ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs

@ -9,20 +9,17 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture] [TestFixture]
public class ShuffledMediaCollectionEnumeratorTests public class ShuffledMediaCollectionEnumeratorTests
{ {
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private readonly List<GroupedMediaItem> _mediaItems = new() private readonly List<GroupedMediaItem> _mediaItems = new()
{ {
new GroupedMediaItem(new MediaItem { Id = 1 }, new List<MediaItem>()), new GroupedMediaItem(new MediaItem { Id = 1 }, new List<MediaItem>()),
new GroupedMediaItem(new MediaItem { Id = 2 }, new List<MediaItem>()), new GroupedMediaItem(new MediaItem { Id = 2 }, new List<MediaItem>()),
new GroupedMediaItem(new MediaItem { Id = 3 }, new List<MediaItem>()) new GroupedMediaItem(new MediaItem { Id = 3 }, new List<MediaItem>())
}; };
private CancellationToken _cancellationToken; private CancellationToken _cancellationToken;
[SetUp]
public void SetUp()
{
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
}
[Test] [Test]
public void Peek_Zero_Should_Match_Current() public void Peek_Zero_Should_Match_Current()

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

@ -8,6 +8,6 @@ public enum FillerKind
PostRoll = 3, PostRoll = 3,
Tail = 4, Tail = 4,
Fallback = 5, Fallback = 5,
GuideMode = 99 GuideMode = 99
} }

3
ErsatzTV.Core/Domain/MediaItem/Episode.cs

@ -2,7 +2,8 @@
namespace ErsatzTV.Core.Domain; namespace ErsatzTV.Core.Domain;
[DebuggerDisplay("{EpisodeMetadata != null && EpisodeMetadata.Count > 0 ? EpisodeMetadata[0].Title : \"[unknown episode]\"}")] [DebuggerDisplay(
"{EpisodeMetadata != null && EpisodeMetadata.Count > 0 ? EpisodeMetadata[0].Title : \"[unknown episode]\"}")]
public class Episode : MediaItem public class Episode : MediaItem
{ {
public int SeasonId { get; set; } public int SeasonId { get; set; }

20
ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs

@ -2,6 +2,16 @@ namespace ErsatzTV.Core.Domain;
public class ProgramScheduleAlternate public class ProgramScheduleAlternate
{ {
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
public int Index { get; set; }
public ICollection<DayOfWeek> DaysOfWeek { get; set; }
public ICollection<int> DaysOfMonth { get; set; }
public ICollection<int> MonthsOfYear { get; set; }
public static List<DayOfWeek> AllDaysOfWeek() => new() public static List<DayOfWeek> AllDaysOfWeek() => new()
{ {
DayOfWeek.Monday, DayOfWeek.Monday,
@ -15,14 +25,4 @@ public class ProgramScheduleAlternate
public static List<int> AllDaysOfMonth() => Enumerable.Range(1, 31).ToList(); public static List<int> AllDaysOfMonth() => Enumerable.Range(1, 31).ToList();
public static List<int> AllMonthsOfYear() => Enumerable.Range(1, 12).ToList(); public static List<int> AllMonthsOfYear() => Enumerable.Range(1, 12).ToList();
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
public int Index { get; set; }
public ICollection<DayOfWeek> DaysOfWeek { get; set; }
public ICollection<int> DaysOfMonth { get; set; }
public ICollection<int> MonthsOfYear { get; set; }
} }

2
ErsatzTV.Core/Emby/EmbyUrl.cs

@ -40,7 +40,7 @@ public static class EmbyUrl
.AppendPathSegment(pathSegment) .AppendPathSegment(pathSegment)
.SetQueryParams(query); .SetQueryParams(query);
} }
public static string PlaceholderProxyForArtwork(string artwork, ArtworkKind artworkKind, int height) public static string PlaceholderProxyForArtwork(string artwork, ArtworkKind artworkKind, int height)
{ {
string[] split = artwork.Replace("emby://", string.Empty).Split('?'); string[] split = artwork.Replace("emby://", string.Empty).Split('?');

2
ErsatzTV.Core/Errors/ScanIsNotRequired.cs

@ -5,4 +5,4 @@ public class ScanIsNotRequired : BaseError
public ScanIsNotRequired() : base("Scan is not required") public ScanIsNotRequired() : base("Scan is not required")
{ {
} }
} }

4
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -96,7 +96,7 @@ public class FFmpegComplexFilterBuilder
var complexFilter = new StringBuilder(); var complexFilter = new StringBuilder();
string videoLabel = $"{videoInput}:{(isSong ? "v" : videoStreamIndex.ToString())}"; var videoLabel = $"{videoInput}:{(isSong ? "v" : videoStreamIndex.ToString())}";
string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a"); string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a");
var videoFilterQueue = new List<string>(); var videoFilterQueue = new List<string>();
@ -229,7 +229,7 @@ public class FFmpegComplexFilterBuilder
complexFilter.Append("[vt];"); complexFilter.Append("[vt];");
} }
string watermarkLabel = $"[{audioInput + 1}:v]"; var watermarkLabel = $"[{audioInput + 1}:v]";
foreach (int index in _watermarkIndex) foreach (int index in _watermarkIndex)
{ {
watermarkLabel = $"[{audioInput + 1}:{index}]"; watermarkLabel = $"[{audioInput + 1}:{index}]";

11
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -18,9 +18,9 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
private readonly FFmpegProcessService _ffmpegProcessService; private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegStreamSelector _ffmpegStreamSelector; private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly ILogger<FFmpegLibraryProcessService> _logger; private readonly ILogger<FFmpegLibraryProcessService> _logger;
private readonly IPipelineBuilderFactory _pipelineBuilderFactory;
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator; private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
private readonly ITempFilePool _tempFilePool; private readonly ITempFilePool _tempFilePool;
private readonly IPipelineBuilderFactory _pipelineBuilderFactory;
public FFmpegLibraryProcessService( public FFmpegLibraryProcessService(
FFmpegProcessService ffmpegProcessService, FFmpegProcessService ffmpegProcessService,
@ -154,7 +154,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoPath != audioPath || channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect; videoPath != audioPath || channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect;
ILogger<FFmpegLibraryProcessService> pixelFormatLogger = isUnknownPixelFormatExpected ? null : _logger; ILogger<FFmpegLibraryProcessService> pixelFormatLogger = isUnknownPixelFormatExpected ? null : _logger;
IPixelFormat pixelFormat = await AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, pixelFormatLogger) IPixelFormat pixelFormat = await AvailablePixelFormats
.ForPixelFormat(videoStream.PixelFormat, pixelFormatLogger)
.IfNoneAsync( .IfNoneAsync(
() => () =>
{ {
@ -165,7 +166,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_ => new PixelFormatUnknown(videoStream.BitsPerRawSample) _ => new PixelFormatUnknown(videoStream.BitsPerRawSample)
}; };
}); });
var ffmpegVideoStream = new VideoStream( var ffmpegVideoStream = new VideoStream(
videoStream.Index, videoStream.Index,
videoStream.Codec, videoStream.Codec,
@ -429,7 +430,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
FileSystemLayout.FFmpegReportsFolder, FileSystemLayout.FFmpegReportsFolder,
FileSystemLayout.FontsCacheFolder, FileSystemLayout.FontsCacheFolder,
ffmpegPath); ffmpegPath);
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState); FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, pipeline); return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, pipeline);
@ -459,7 +460,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
FileSystemLayout.FFmpegReportsFolder, FileSystemLayout.FFmpegReportsFolder,
FileSystemLayout.FontsCacheFolder, FileSystemLayout.FontsCacheFolder,
ffmpegPath); ffmpegPath);
FFmpegPipeline pipeline = pipelineBuilder.Concat( FFmpegPipeline pipeline = pipelineBuilder.Concat(
concatInputFile, concatInputFile,
FFmpegState.Concat(saveReports, channel.Name)); FFmpegState.Concat(saveReports, channel.Name));

5
ErsatzTV.Core/FFmpeg/FFmpegProcess.cs

@ -6,10 +6,7 @@ public class FFmpegProcess : Process
{ {
public static int ProcessCount; public static int ProcessCount;
public FFmpegProcess() public FFmpegProcess() => Interlocked.Increment(ref ProcessCount);
{
Interlocked.Increment(ref ProcessCount);
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {

9
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -31,10 +31,7 @@ internal class FFmpegProcessBuilder
private readonly string _ffmpegPath; private readonly string _ffmpegPath;
private FFmpegComplexFilterBuilder _complexFilterBuilder = new(); private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
public FFmpegProcessBuilder(string ffmpegPath) public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath;
{
_ffmpegPath = ffmpegPath;
}
public FFmpegProcessBuilder WithThreads(int threads) public FFmpegProcessBuilder WithThreads(int threads)
{ {
@ -166,8 +163,8 @@ internal class FFmpegProcessBuilder
audioIndex = 1; audioIndex = 1;
} }
string videoLabel = $"{videoIndex}:{videoStreamIndex}"; var videoLabel = $"{videoIndex}:{videoStreamIndex}";
string audioLabel = $"{audioIndex}:{maybeIndex.Match(i => i.ToString(), () => "a")}"; var audioLabel = $"{audioIndex}:{maybeIndex.Match(i => i.ToString(), () => "a")}";
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build( Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(
audioPath.IsNone, audioPath.IsNone,

15
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -12,12 +12,12 @@ namespace ErsatzTV.Core.FFmpeg;
public class FFmpegStreamSelector : IFFmpegStreamSelector public class FFmpegStreamSelector : IFFmpegStreamSelector
{ {
private readonly IScriptEngine _scriptEngine;
private readonly IStreamSelectorRepository _streamSelectorRepository;
private readonly ISearchRepository _searchRepository;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<FFmpegStreamSelector> _logger; private readonly ILogger<FFmpegStreamSelector> _logger;
private readonly IScriptEngine _scriptEngine;
private readonly ISearchRepository _searchRepository;
private readonly IStreamSelectorRepository _streamSelectorRepository;
public FFmpegStreamSelector( public FFmpegStreamSelector(
IScriptEngine scriptEngine, IScriptEngine scriptEngine,
@ -74,7 +74,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
{ {
_logger.LogDebug("Preferred audio language has multiple codes {Codes}", allLanguageCodes); _logger.LogDebug("Preferred audio language has multiple codes {Codes}", allLanguageCodes);
} }
try try
{ {
switch (version.MediaItem) switch (version.MediaItem)
@ -92,6 +92,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
{ {
return result; return result;
} }
break; break;
case Movie: case Movie:
var sw2 = Stopwatch.StartNew(); var sw2 = Stopwatch.StartNew();
@ -106,6 +107,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
{ {
return result2; return result2;
} }
break; break;
// let default fall through // let default fall through
} }
@ -248,7 +250,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
return PrioritizeDefault(streams); return PrioritizeDefault(streams);
} }
_logger.LogDebug("Unable to find audio stream with preferred title {Title}", title); _logger.LogDebug("Unable to find audio stream with preferred title {Title}", title);
return PrioritizeDefault(streams); return PrioritizeDefault(streams);
@ -322,7 +324,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath); _logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
if (!_localFileSystem.FileExists(jsScriptPath)) if (!_localFileSystem.FileExists(jsScriptPath))
{ {
_logger.LogInformation("Unable to locate movie audio stream selector script; falling back to built-in logic"); _logger.LogInformation(
"Unable to locate movie audio stream selector script; falling back to built-in logic");
return Option<MediaStream>.None; return Option<MediaStream>.None;
} }

2
ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs

@ -170,7 +170,7 @@ public class SongVideoGenerator : ISongVideoGenerator
Streams = new List<MediaStream> Streams = new List<MediaStream>
{ {
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 } new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
}, }
}; };
string customPath = _imageCache.GetPathForImage( string customPath = _imageCache.GetPathForImage(

4
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -6,8 +6,8 @@ namespace ErsatzTV.Core.Iptv;
public class ChannelGuide public class ChannelGuide
{ {
private readonly string _channelsFragment;
private readonly Dictionary<string, string> _channelDataFragments; private readonly Dictionary<string, string> _channelDataFragments;
private readonly string _channelsFragment;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public ChannelGuide( public ChannelGuide(
@ -28,7 +28,7 @@ public class ChannelGuide
xml.WriteStartElement("tv"); xml.WriteStartElement("tv");
xml.WriteAttributeString("generator-info-name", "ersatztv"); xml.WriteAttributeString("generator-info-name", "ersatztv");
xml.WriteRaw(_channelsFragment); xml.WriteRaw(_channelsFragment);
foreach ((string channelNumber, string channelDataFragment) in _channelDataFragments.OrderBy( foreach ((string channelNumber, string channelDataFragment) in _channelDataFragments.OrderBy(

6
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -5,11 +5,11 @@ namespace ErsatzTV.Core.Iptv;
public class ChannelPlaylist public class ChannelPlaylist
{ {
private readonly string _accessToken;
private readonly string _baseUrl;
private readonly List<Channel> _channels; private readonly List<Channel> _channels;
private readonly string _host; private readonly string _host;
private readonly string _baseUrl;
private readonly string _scheme; private readonly string _scheme;
private readonly string _accessToken;
public ChannelPlaylist(string scheme, string host, string baseUrl, List<Channel> channels, string accessToken) public ChannelPlaylist(string scheme, string host, string baseUrl, List<Channel> channels, string accessToken)
{ {
@ -23,7 +23,7 @@ public class ChannelPlaylist
public string ToM3U() public string ToM3U()
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
string accessTokenUri = string.Empty; string accessTokenUri = string.Empty;
string accessTokenUriAmp = string.Empty; string accessTokenUriAmp = string.Empty;
if (_accessToken != null) if (_accessToken != null)

10
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -19,7 +19,7 @@ public partial class FallbackMetadataProvider : IFallbackMetadataProvider
{ {
return seasonNumber; return seasonNumber;
} }
Match match = SeasonPattern.Match(folderName); Match match = SeasonPattern.Match(folderName);
if (match.Success && int.TryParse(match.Groups[1].Value, out seasonNumber)) if (match.Success && int.TryParse(match.Groups[1].Value, out seasonNumber))
{ {
@ -30,7 +30,7 @@ public partial class FallbackMetadataProvider : IFallbackMetadataProvider
{ {
return seasonNumber; return seasonNumber;
} }
if (folder.EndsWith("specials", StringComparison.OrdinalIgnoreCase)) if (folder.EndsWith("specials", StringComparison.OrdinalIgnoreCase))
{ {
return 0; return 0;
@ -38,9 +38,6 @@ public partial class FallbackMetadataProvider : IFallbackMetadataProvider
return None; return None;
} }
[GeneratedRegex(@"s(?:eason)?\s?(\d+)(?![e\d])", RegexOptions.IgnoreCase)]
private static partial Regex SeasonNumber();
public ShowMetadata GetFallbackMetadataForShow(string showFolder) public ShowMetadata GetFallbackMetadataForShow(string showFolder)
{ {
@ -156,6 +153,9 @@ public partial class FallbackMetadataProvider : IFallbackMetadataProvider
return GetSongMetadata(path, metadata); return GetSongMetadata(path, metadata);
} }
[GeneratedRegex(@"s(?:eason)?\s?(\d+)(?![e\d])", RegexOptions.IgnoreCase)]
private static partial Regex SeasonNumber();
private List<EpisodeMetadata> GetEpisodeMetadata(string fileName, EpisodeMetadata baseMetadata) private List<EpisodeMetadata> GetEpisodeMetadata(string fileName, EpisodeMetadata baseMetadata)
{ {
var result = new List<EpisodeMetadata> { baseMetadata }; var result = new List<EpisodeMetadata> { baseMetadata };

24
ErsatzTV.Core/Scheduling/MultiCollectionGroup.cs

@ -23,20 +23,20 @@ public class MultiCollectionGroup : GroupedMediaItem
switch (collectionWithItems.PlaybackOrder) switch (collectionWithItems.PlaybackOrder)
{ {
case PlaybackOrder.Chronological: case PlaybackOrder.Chronological:
{ {
var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new ChronologicalMediaComparer()) var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new ChronologicalMediaComparer())
.ToList(); .ToList();
First = sortedItems.Head(); First = sortedItems.Head();
Additional = sortedItems.Tail().ToList(); Additional = sortedItems.Tail().ToList();
} }
break; break;
case PlaybackOrder.SeasonEpisode: case PlaybackOrder.SeasonEpisode:
{ {
var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new SeasonEpisodeMediaComparer()) var sortedItems = collectionWithItems.MediaItems.OrderBy(identity, new SeasonEpisodeMediaComparer())
.ToList(); .ToList();
First = sortedItems.Head(); First = sortedItems.Head();
Additional = sortedItems.Tail().ToList(); Additional = sortedItems.Tail().ToList();
} }
break; break;
default: default:
throw new NotSupportedException( throw new NotSupportedException(

9
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -3,7 +3,6 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt.UnsafeValueAccess;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Map = LanguageExt.Map; using Map = LanguageExt.Map;
@ -15,11 +14,11 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
private static readonly Random Random = new(); private static readonly Random Random = new();
private readonly IArtistRepository _artistRepository; private readonly IArtistRepository _artistRepository;
private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<PlayoutBuilder> _logger; private readonly ILogger<PlayoutBuilder> _logger;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public PlayoutBuilder( public PlayoutBuilder(
@ -395,7 +394,7 @@ public class PlayoutBuilder : IPlayoutBuilder
// random start points are disabled in some scenarios, so ensure it's enabled and active // random start points are disabled in some scenarios, so ensure it's enabled and active
randomStartPoint = randomStartPoint && activeSchedule.RandomStartPoint; randomStartPoint = randomStartPoint && activeSchedule.RandomStartPoint;
var sortedScheduleItems = activeSchedule.Items.OrderBy(i => i.Index).ToList(); var sortedScheduleItems = activeSchedule.Items.OrderBy(i => i.Index).ToList();
CollectionEnumeratorState scheduleItemsEnumeratorState = CollectionEnumeratorState scheduleItemsEnumeratorState =
playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState
@ -469,7 +468,7 @@ public class PlayoutBuilder : IPlayoutBuilder
var schedulerFlood = new PlayoutModeSchedulerFlood(_logger); var schedulerFlood = new PlayoutModeSchedulerFlood(_logger);
var timeCount = new Dictionary<DateTimeOffset, int>(); var timeCount = new Dictionary<DateTimeOffset, int>();
// loop until we're done filling the desired amount of time // loop until we're done filling the desired amount of time
while (playoutBuilderState.CurrentTime < playoutFinish && !cancellationToken.IsCancellationRequested) while (playoutBuilderState.CurrentTime < playoutFinish && !cancellationToken.IsCancellationRequested)
{ {

4
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -27,7 +27,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
DateTimeOffset hardStop) DateTimeOffset hardStop)
{ {
DateTimeOffset startTime = GetStartTimeAfter(state, scheduleItem); DateTimeOffset startTime = GetStartTimeAfter(state, scheduleItem);
// filler should always stop at the hard stop // filler should always stop at the hard stop
if (hardStop < startTime) if (hardStop < startTime)
{ {
@ -553,7 +553,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
// remove post-roll to add after mid-roll/content // remove post-roll to add after mid-roll/content
var postRoll = result.Where(i => i.FillerKind == FillerKind.PostRoll).ToList(); var postRoll = result.Where(i => i.FillerKind == FillerKind.PostRoll).ToList();
result.RemoveAll(i => i.FillerKind == FillerKind.PostRoll); result.RemoveAll(i => i.FillerKind == FillerKind.PostRoll);
for (var i = 0; i < effectiveChapters.Count; i++) for (var i = 0; i < effectiveChapters.Count; i++)
{ {
result.Add(playoutItem.ForChapter(effectiveChapters[i])); result.Add(playoutItem.ForChapter(effectiveChapters[i]));

4
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -205,7 +205,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
} }
bool hasFallback = playoutItems.Any(p => p.FillerKind == FillerKind.Fallback); bool hasFallback = playoutItems.Any(p => p.FillerKind == FillerKind.Fallback);
var playoutItemsToClear = playoutItems var playoutItemsToClear = playoutItems
.Filter(pi => pi.FillerKind == FillerKind.None) .Filter(pi => pi.FillerKind == FillerKind.None)
.ToList(); .ToList();
@ -217,7 +217,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
{ {
playoutItemsToClear.Remove(lastItem); playoutItemsToClear.Remove(lastItem);
} }
foreach (PlayoutItem item in playoutItemsToClear) foreach (PlayoutItem item in playoutItemsToClear)
{ {
item.GuideFinish = null; item.GuideFinish = null;

2
ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs

@ -5,10 +5,10 @@ namespace ErsatzTV.Core.Scheduling;
public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
{ {
private readonly CancellationToken _cancellationToken;
private readonly IList<CollectionWithItems> _collections; private readonly IList<CollectionWithItems> _collections;
private readonly int _mediaItemCount; private readonly int _mediaItemCount;
private readonly bool _randomStartPoint; private readonly bool _randomStartPoint;
private readonly CancellationToken _cancellationToken;
private Random _random; private Random _random;
private IList<MediaItem> _shuffled; private IList<MediaItem> _shuffled;

2
ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs

@ -5,9 +5,9 @@ namespace ErsatzTV.Core.Scheduling;
public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
{ {
private readonly CancellationToken _cancellationToken;
private readonly int _mediaItemCount; private readonly int _mediaItemCount;
private readonly IList<GroupedMediaItem> _mediaItems; private readonly IList<GroupedMediaItem> _mediaItems;
private readonly CancellationToken _cancellationToken;
private CloneableRandom _random; private CloneableRandom _random;
private IList<MediaItem> _shuffled; private IList<MediaItem> _shuffled;

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

Loading…
Cancel
Save