Browse Source

work around sequential schedule validation limit (#2655)

* remove readalltext

* remove unused method

* remove fileexists

* remove folderexists

* remove readalllines

* remove fake local file system

* show playlist name in playout build errors

* add basic sequential schedule validator tests

* work around sequential schedule validation limit
pull/2656/head
Jason Dove 1 month ago committed by GitHub
parent
commit
ec0d8ea6ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs
  3. 16
      ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs
  4. 8
      ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs
  5. 6
      ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs
  6. 6
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  7. 4
      ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs
  8. 10
      ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs
  9. 30
      ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs
  10. 31
      ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs
  11. 36
      ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs
  12. 6
      ErsatzTV.Application/Playouts/Commands/UpdateScriptedPlayoutHandler.cs
  13. 7
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  14. 26
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  15. 8
      ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs
  16. 8
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  17. 6
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedShowSubtitlesHandler.cs
  18. 8
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  19. 12
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs
  20. 4
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  21. 1
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  22. 177
      ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs
  23. 7
      ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs
  24. 8
      ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs
  25. 3
      ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs
  26. 99
      ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs
  27. 11
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs
  28. 35
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs
  29. 8
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs
  30. 5
      ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs
  31. 6
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  32. 1
      ErsatzTV.Core/ErsatzTV.Core.csproj
  33. 8
      ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs
  34. 11
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  35. 9
      ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
  36. 59
      ErsatzTV.Core/Metadata/LocalFileSystem.cs
  37. 16
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  38. 6
      ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs
  39. 6
      ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs
  40. 7
      ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
  41. 2
      ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs
  42. 200
      ErsatzTV.Infrastructure.Tests/Scheduling/SequentialScheduleValidatorTests.cs
  43. 46
      ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs
  44. 5
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  45. 8
      ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs
  46. 1
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  47. 8
      ErsatzTV.Infrastructure/FFmpeg/MpegTsScriptService.cs
  48. 30
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  49. 6
      ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs
  50. 62
      ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs
  51. 13
      ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs
  52. 10
      ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs
  53. 18
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  54. 88
      ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs
  55. 22
      ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs
  56. 38
      ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs
  57. 1
      ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
  58. 8
      ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs
  59. 8
      ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs
  60. 8
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  61. 8
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  62. 8
      ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs
  63. 13
      ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs
  64. 10
      ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs
  65. 2
      ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs
  66. 14
      ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs
  67. 14
      ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs
  68. 18
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  69. 14
      ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs
  70. 16
      ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs
  71. 12
      ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs
  72. 8
      ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs
  73. 10
      ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs
  74. 18
      ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs
  75. 8
      ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs
  76. 6
      ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs
  77. 8
      ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs
  78. 4
      ErsatzTV.Scanner/Program.cs
  79. 6
      ErsatzTV/Controllers/Api/TroubleshootController.cs
  80. 6
      ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor
  81. 46
      ErsatzTV/Services/RunOnce/CacheCleanerService.cs
  82. 4
      ErsatzTV/Startup.cs

3
CHANGELOG.md

@ -84,6 +84,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -84,6 +84,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add disabled text color and `(D)` and `(H)` labels for disabled and hidden channels in channel list
- Graphics engine: fix subtitle path escaping and font loading
- Fix corrupt output (green artifacts) when decoding certain 10-bit content using AMD Polaris GPUs
- Work around sequential schedule validation limit (1000/hr by Newtonsoft.Json.Schema library)
- Playout builds now use JsonSchema.Net library which has no validation limit
- Validation tool in the UI still uses Newtonsoft.Json.Schema (with 1000/hr limit) as the error output is easier to understand
### Changed
- Classic schedules: `Refresh` classic playouts from playout list; do not `Reset` them

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

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -12,19 +12,19 @@ namespace ErsatzTV.Application.Channels; @@ -12,19 +12,19 @@ namespace ErsatzTV.Application.Channels;
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly IFileSystem _fileSystem;
private readonly ISearchTargets _searchTargets;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeleteChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ISearchTargets searchTargets)
{
_workerChannel = workerChannel;
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_searchTargets = searchTargets;
}
@ -44,7 +44,7 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr @@ -44,7 +44,7 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
// delete channel data from channel guide cache
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
if (_localFileSystem.FileExists(cacheFile))
if (_fileSystem.File.Exists(cacheFile))
{
File.Delete(cacheFile);
}

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Globalization;
using System.IO.Abstractions;
using System.Xml;
using ErsatzTV.Application.Configuration;
using ErsatzTV.Core;
@ -26,6 +27,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -26,6 +27,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RefreshChannelDataHandler> _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
@ -33,12 +35,14 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -33,12 +35,14 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
public RefreshChannelDataHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IDbContextFactory<TvContext> dbContextFactory,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
IConfigElementRepository configElementRepository,
ILogger<RefreshChannelDataHandler> logger)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_dbContextFactory = dbContextFactory;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_configElementRepository = configElementRepository;
_logger = logger;
@ -886,7 +890,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -886,7 +890,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"movie.sbntxt");
// fail if file doesn't exist
if (!_localFileSystem.FileExists(templateFileName))
if (!_fileSystem.File.Exists(templateFileName))
{
_logger.LogError(
"Unable to generate movie XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -905,7 +909,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -905,7 +909,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"episode.sbntxt");
// fail if file doesn't exist
if (!_localFileSystem.FileExists(templateFileName))
if (!_fileSystem.File.Exists(templateFileName))
{
_logger.LogError(
"Unable to generate episode XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -924,7 +928,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -924,7 +928,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"musicVideo.sbntxt");
// fail if file doesn't exist
if (!_localFileSystem.FileExists(templateFileName))
if (!_fileSystem.File.Exists(templateFileName))
{
_logger.LogError(
"Unable to generate music video XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -943,7 +947,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -943,7 +947,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"song.sbntxt");
// fail if file doesn't exist
if (!_localFileSystem.FileExists(templateFileName))
if (!_fileSystem.File.Exists(templateFileName))
{
_logger.LogError(
"Unable to generate song XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -962,7 +966,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -962,7 +966,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"otherVideo.sbntxt");
// fail if file doesn't exist
if (!_localFileSystem.FileExists(templateFileName))
if (!_fileSystem.File.Exists(templateFileName))
{
_logger.LogError(
"Unable to generate other video XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -1077,7 +1081,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -1077,7 +1081,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
{
var result = new List<PlayoutItem>();
if (_localFileSystem.FileExists(path))
if (_fileSystem.File.Exists(path))
{
Option<ExternalJsonChannel> maybeChannel = JsonConvert.DeserializeObject<ExternalJsonChannel>(
await File.ReadAllTextAsync(path));

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Data.Common;
using System.IO.Abstractions;
using System.Net;
using System.Xml;
using Dapper;
@ -19,6 +20,7 @@ namespace ErsatzTV.Application.Channels; @@ -19,6 +20,7 @@ namespace ErsatzTV.Application.Channels;
public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RefreshChannelListHandler> _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
@ -26,11 +28,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList> @@ -26,11 +28,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
public RefreshChannelListHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IDbContextFactory<TvContext> dbContextFactory,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILogger<RefreshChannelListHandler> logger)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_dbContextFactory = dbContextFactory;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_logger = logger;
}
@ -44,13 +48,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList> @@ -44,13 +48,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "channel.sbntxt");
// fall back to default template
if (!_localFileSystem.FileExists(templateFileName))
if (!_fileSystem.File.Exists(templateFileName))
{
templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_channel.sbntxt");
}
// fail if file doesn't exist
if (!_localFileSystem.FileExists(templateFileName))
if (!_fileSystem.File.Exists(templateFileName))
{
_logger.LogError(
"Unable to generate channel list without template file {File}; please restart ErsatzTV",

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using System.Text;
using System.Text.RegularExpressions;
using ErsatzTV.Core;
@ -15,14 +16,17 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E @@ -15,14 +16,17 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly IFileSystem _fileSystem;
public GetChannelGuideHandler(
IDbContextFactory<TvContext> dbContextFactory,
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem)
{
_dbContextFactory = dbContextFactory;
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
}
@ -39,7 +43,7 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E @@ -39,7 +43,7 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
.ToImmutableHashSet();
string channelsFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
if (!_localFileSystem.FileExists(channelsFile))
if (!_fileSystem.File.Exists(channelsFile))
{
return BaseError.New($"Required file {channelsFile} is missing");
}

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

@ -1,18 +1,18 @@ @@ -1,18 +1,18 @@
using System.Diagnostics;
using System.Globalization;
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.FFmpeg;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.FFmpegProfiles;
public class UpdateFFmpegSettingsHandler(
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
: IRequestHandler<UpdateFFmpegSettings, Either<BaseError, Unit>>
{
@ -35,7 +35,7 @@ public class UpdateFFmpegSettingsHandler( @@ -35,7 +35,7 @@ public class UpdateFFmpegSettingsHandler(
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
{
if (!localFileSystem.FileExists(path))
if (!fileSystem.File.Exists(path))
{
return BaseError.New($"{name} path does not exist");
}

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

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
@ -10,6 +11,7 @@ namespace ErsatzTV.Application.Graphics; @@ -10,6 +11,7 @@ namespace ErsatzTV.Application.Graphics;
public class RefreshGraphicsElementsHandler(
IDbContextFactory<TvContext> dbContextFactory,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
IGraphicsElementLoader graphicsElementLoader,
ILogger<RefreshGraphicsElementsHandler> logger)
@ -24,7 +26,7 @@ public class RefreshGraphicsElementsHandler( @@ -24,7 +26,7 @@ public class RefreshGraphicsElementsHandler(
.ToListAsync(cancellationToken);
var missing = allExisting
.Where(e => !localFileSystem.FileExists(e.Path) || (Path.GetExtension(e.Path) != ".yml" && Path.GetExtension(e.Path) != ".yaml"))
.Where(e => !fileSystem.File.Exists(e.Path) || (Path.GetExtension(e.Path) != ".yml" && Path.GetExtension(e.Path) != ".yaml"))
.ToList();
foreach (GraphicsElement existing in missing)

10
ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -14,16 +14,16 @@ namespace ErsatzTV.Application.Playouts; @@ -14,16 +14,16 @@ namespace ErsatzTV.Application.Playouts;
public class CreateExternalJsonPlayoutHandler
: IRequestHandler<CreateExternalJsonPlayout, Either<BaseError, CreatePlayoutResponse>>
{
private readonly IFileSystem _fileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
public CreateExternalJsonPlayoutHandler(
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory)
{
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_channel = channel;
_dbContextFactory = dbContextFactory;
}
@ -76,7 +76,7 @@ public class CreateExternalJsonPlayoutHandler @@ -76,7 +76,7 @@ public class CreateExternalJsonPlayoutHandler
private Validation<BaseError, string> ValidateExternalJsonFile(CreateExternalJsonPlayout request)
{
if (!_localFileSystem.FileExists(request.ScheduleFile))
if (!_fileSystem.File.Exists(request.ScheduleFile))
{
return BaseError.New("External Json File does not exist!");
}

30
ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.CommandLine.Parsing;
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
@ -12,28 +13,17 @@ using Channel = ErsatzTV.Core.Domain.Channel; @@ -12,28 +13,17 @@ using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts;
public class CreateScriptedPlayoutHandler
public class CreateScriptedPlayoutHandler(
IFileSystem fileSystem,
ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateScriptedPlayout, Either<BaseError, CreatePlayoutResponse>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
public CreateScriptedPlayoutHandler(
ILocalFileSystem localFileSystem,
ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory)
{
_localFileSystem = localFileSystem;
_channel = channel;
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
CreateScriptedPlayout request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request, cancellationToken);
return await validation.Apply(playout => PersistPlayout(dbContext, playout, cancellationToken));
}
@ -45,15 +35,15 @@ public class CreateScriptedPlayoutHandler @@ -45,15 +35,15 @@ public class CreateScriptedPlayoutHandler
{
await dbContext.Playouts.AddAsync(playout, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset), cancellationToken);
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset), cancellationToken);
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
await _channel.WriteAsync(
await channel.WriteAsync(
new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false),
cancellationToken);
}
await _channel.WriteAsync(new RefreshChannelList(), cancellationToken);
await channel.WriteAsync(new RefreshChannelList(), cancellationToken);
return new CreatePlayoutResponse(playout.Id);
}
@ -91,7 +81,7 @@ public class CreateScriptedPlayoutHandler @@ -91,7 +81,7 @@ public class CreateScriptedPlayoutHandler
{
var args = CommandLineParser.SplitCommandLine(request.ScheduleFile).ToList();
string scriptFile = args[0];
if (!_localFileSystem.FileExists(scriptFile))
if (!fileSystem.File.Exists(scriptFile))
{
return BaseError.New("Scripted schedule does not exist!");
}

31
ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -11,28 +11,17 @@ using Channel = ErsatzTV.Core.Domain.Channel; @@ -11,28 +11,17 @@ using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts;
public class CreateSequentialPlayoutHandler
public class CreateSequentialPlayoutHandler(
IFileSystem fileSystem,
ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateSequentialPlayout, Either<BaseError, CreatePlayoutResponse>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
public CreateSequentialPlayoutHandler(
ILocalFileSystem localFileSystem,
ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory)
{
_localFileSystem = localFileSystem;
_channel = channel;
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
CreateSequentialPlayout request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request, cancellationToken);
return await validation.Apply(playout => PersistPlayout(dbContext, playout, cancellationToken));
}
@ -44,15 +33,15 @@ public class CreateSequentialPlayoutHandler @@ -44,15 +33,15 @@ public class CreateSequentialPlayoutHandler
{
await dbContext.Playouts.AddAsync(playout, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset), cancellationToken);
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset), cancellationToken);
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
await _channel.WriteAsync(
await channel.WriteAsync(
new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false),
cancellationToken);
}
await _channel.WriteAsync(new RefreshChannelList(), cancellationToken);
await channel.WriteAsync(new RefreshChannelList(), cancellationToken);
return new CreatePlayoutResponse(playout.Id);
}
@ -87,7 +76,7 @@ public class CreateSequentialPlayoutHandler @@ -87,7 +76,7 @@ public class CreateSequentialPlayoutHandler
private Validation<BaseError, string> ValidateYamlFile(CreateSequentialPlayout request)
{
if (!_localFileSystem.FileExists(request.ScheduleFile))
if (!fileSystem.File.Exists(request.ScheduleFile))
{
return BaseError.New("Sequential schedule does not exist!");
}

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

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using System.Threading.Channels;
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Notifications;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -10,28 +10,16 @@ using Microsoft.EntityFrameworkCore; @@ -10,28 +10,16 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Unit>>
public class DeletePlayoutHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory,
IFileSystem fileSystem,
IMediator mediator)
: IRequestHandler<DeletePlayout, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly IMediator _mediator;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeletePlayoutHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
IMediator mediator)
{
_workerChannel = workerChannel;
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_mediator = mediator;
}
public async Task<Either<BaseError, Unit>> Handle(DeletePlayout request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Channel)
@ -44,15 +32,15 @@ public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseEr @@ -44,15 +32,15 @@ public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseEr
// delete channel data from channel guide cache
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{playout.Channel.Number}.xml");
if (_localFileSystem.FileExists(cacheFile))
if (fileSystem.File.Exists(cacheFile))
{
File.Delete(cacheFile);
}
// refresh channel list to remove channel that has no playout
await _workerChannel.WriteAsync(new RefreshChannelList(), cancellationToken);
await workerChannel.WriteAsync(new RefreshChannelList(), cancellationToken);
await _mediator.Publish(new PlayoutUpdatedNotification(playout.Id, false), cancellationToken);
await mediator.Publish(new PlayoutUpdatedNotification(playout.Id, false), cancellationToken);
}
return maybePlayout

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

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
using System.CommandLine.Parsing;
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -14,7 +14,7 @@ public class @@ -14,7 +14,7 @@ public class
UpdateScriptedPlayoutHandler(
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel,
ILocalFileSystem localFileSystem)
IFileSystem fileSystem)
: IRequestHandler<UpdateScriptedPlayout,
Either<BaseError, PlayoutNameViewModel>>
{
@ -63,7 +63,7 @@ public class @@ -63,7 +63,7 @@ public class
{
var args = CommandLineParser.SplitCommandLine(request.ScheduleFile).ToList();
string scriptFile = args[0];
if (!localFileSystem.FileExists(scriptFile))
if (!fileSystem.File.Exists(scriptFile))
{
return BaseError.New("Scripted schedule does not exist!");
}

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Threading.Channels;
using System.IO.Abstractions;
using System.Threading.Channels;
using Bugsnag;
using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Graphics;
@ -21,6 +22,7 @@ namespace ErsatzTV.Application.Streaming; @@ -21,6 +22,7 @@ namespace ErsatzTV.Application.Streaming;
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
{
private readonly IClient _client;
private readonly IFileSystem _fileSystem;
private readonly IConfigElementRepository _configElementRepository;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IGraphicsEngine _graphicsEngine;
@ -40,6 +42,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -40,6 +42,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
IServiceScopeFactory serviceScopeFactory,
IMediator mediator,
IClient client,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILogger<StartFFmpegSessionHandler> logger,
ILogger<HlsSessionWorker> sessionWorkerLogger,
@ -54,6 +57,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -54,6 +57,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_serviceScopeFactory = serviceScopeFactory;
_mediator = mediator;
_client = client;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_logger = logger;
_sessionWorkerLogger = sessionWorkerLogger;
@ -129,6 +133,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -129,6 +133,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_hlsPlaylistFilter,
_hlsInitSegmentCache,
_configElementRepository,
_fileSystem,
_localFileSystem,
_sessionWorkerLogger,
targetFramerate)

26
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Abstractions;
using System.IO.Pipelines;
using System.Text;
using System.Timers;
@ -30,6 +31,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -30,6 +31,7 @@ public class HlsSessionWorker : IHlsSessionWorker
private readonly IHlsInitSegmentCache _hlsInitSegmentCache;
private readonly Dictionary<long, int> _discontinuityMap = [];
private readonly IConfigElementRepository _configElementRepository;
private readonly IFileSystem _fileSystem;
private readonly IGraphicsEngine _graphicsEngine;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly ILocalFileSystem _localFileSystem;
@ -60,6 +62,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -60,6 +62,7 @@ public class HlsSessionWorker : IHlsSessionWorker
IHlsPlaylistFilter hlsPlaylistFilter,
IHlsInitSegmentCache hlsInitSegmentCache,
IConfigElementRepository configElementRepository,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILogger<HlsSessionWorker> logger,
Option<int> targetFramerate)
@ -72,6 +75,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -72,6 +75,7 @@ public class HlsSessionWorker : IHlsSessionWorker
_hlsInitSegmentCache = hlsInitSegmentCache;
_hlsPlaylistFilter = hlsPlaylistFilter;
_configElementRepository = configElementRepository;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_logger = logger;
_targetFramerate = targetFramerate;
@ -308,7 +312,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -308,7 +312,7 @@ public class HlsSessionWorker : IHlsSessionWorker
string playlistFileName = Path.Combine(_workingDirectory, "live.m3u8");
_logger.LogDebug("Waiting for playlist to exist");
while (!_localFileSystem.FileExists(playlistFileName))
while (!_fileSystem.File.Exists(playlistFileName))
{
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
}
@ -679,9 +683,9 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -679,9 +683,9 @@ public class HlsSessionWorker : IHlsSessionWorker
var generatedAtHash = new System.Collections.Generic.HashSet<long>();
// delete old segments
var allSegments = Directory.GetFiles(_workingDirectory, "live*.ts")
.Append(Directory.GetFiles(_workingDirectory, "live*.mp4"))
.Append(Directory.GetFiles(_workingDirectory, "live*.m4s"))
var allSegments = _fileSystem.Directory.GetFiles(_workingDirectory, "live*.ts")
.Append(_fileSystem.Directory.GetFiles(_workingDirectory, "live*.mp4"))
.Append(_fileSystem.Directory.GetFiles(_workingDirectory, "live*.m4s"))
.Map(file =>
{
string fileName = Path.GetFileName(file);
@ -699,7 +703,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -699,7 +703,7 @@ public class HlsSessionWorker : IHlsSessionWorker
})
.ToList();
var allInits = Directory.GetFiles(_workingDirectory, "*init.mp4")
var allInits = _fileSystem.Directory.GetFiles(_workingDirectory, "*init.mp4")
.Map(file => long.TryParse(Path.GetFileName(file).Split('_')[0], out long generatedAt) && !generatedAtHash.Contains(generatedAt)
? new Segment(file, 0, generatedAt)
: Option<Segment>.None)
@ -739,7 +743,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -739,7 +743,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{
try
{
File.Delete(segment.File);
_fileSystem.File.Delete(segment.File);
}
catch (IOException)
{
@ -752,12 +756,12 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -752,12 +756,12 @@ public class HlsSessionWorker : IHlsSessionWorker
private async Task RefreshInits()
{
var allSegments = Directory.GetFiles(_workingDirectory, "live*.m4s")
var allSegments = _fileSystem.Directory.GetFiles(_workingDirectory, "live*.m4s")
.Map(Path.GetFileName)
.Map(s => s.Split("_")[1])
.ToHashSet();
foreach (string file in Directory.GetFiles(_workingDirectory, "*init.mp4"))
foreach (string file in _fileSystem.Directory.GetFiles(_workingDirectory, "*init.mp4"))
{
string key = Path.GetFileName(file).Split("_")[0];
if (allSegments.Contains(key))
@ -812,9 +816,9 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -812,9 +816,9 @@ public class HlsSessionWorker : IHlsSessionWorker
private async Task<Option<string[]>> ReadPlaylistLines(CancellationToken cancellationToken)
{
string fileName = PlaylistFileName();
if (File.Exists(fileName))
if (_fileSystem.File.Exists(fileName))
{
return await File.ReadAllLinesAsync(fileName, cancellationToken);
return await _fileSystem.File.ReadAllLinesAsync(fileName, cancellationToken);
}
_logger.LogDebug("Playlist does not exist at expected location {File}", fileName);
@ -824,7 +828,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -824,7 +828,7 @@ public class HlsSessionWorker : IHlsSessionWorker
private async Task WritePlaylist(string playlist, CancellationToken cancellationToken)
{
string fileName = PlaylistFileName();
await File.WriteAllTextAsync(fileName, playlist, cancellationToken);
await _fileSystem.File.WriteAllTextAsync(fileName, playlist, cancellationToken);
}
private string PlaylistFileName() => Path.Combine(_workingDirectory, "live.m3u8");

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

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
using System.Text;
using System.IO.Abstractions;
using System.Text;
using Bugsnag;
using CliWrap;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@ -14,7 +14,7 @@ namespace ErsatzTV.Application.Streaming; @@ -14,7 +14,7 @@ namespace ErsatzTV.Application.Streaming;
public class GetLastPtsTimeHandler(
IClient client,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ITempFilePool tempFilePool,
IConfigElementRepository configElementRepository,
ILogger<GetLastPtsTimeHandler> logger)
@ -168,7 +168,7 @@ public class GetLastPtsTimeHandler( @@ -168,7 +168,7 @@ public class GetLastPtsTimeHandler(
string playlistFileName = Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8");
string playlistContents = string.Empty;
if (localFileSystem.FileExists(playlistFileName))
if (fileSystem.File.Exists(playlistFileName))
{
playlistContents = await File.ReadAllTextAsync(playlistFileName);
}

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using CliWrap;
using System.IO.Abstractions;
using CliWrap;
using Dapper;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
@ -31,6 +32,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -31,6 +32,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IExternalJsonPlayoutItemProvider _externalJsonPlayoutItemProvider;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IFileSystem _fileSystem;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
@ -47,6 +49,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -47,6 +49,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
public GetPlayoutItemProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
IExternalJsonPlayoutItemProvider externalJsonPlayoutItemProvider,
IPlexPathReplacementService plexPathReplacementService,
@ -64,6 +67,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -64,6 +67,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
: base(dbContextFactory)
{
_ffmpegProcessService = ffmpegProcessService;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_externalJsonPlayoutItemProvider = externalJsonPlayoutItemProvider;
_plexPathReplacementService = plexPathReplacementService;
@ -775,7 +779,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -775,7 +779,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
}
// check filesystem first
if (_localFileSystem.FileExists(path))
if (_fileSystem.File.Exists(path))
{
if (playoutItem.MediaItem is RemoteStream remoteStream)
{

6
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedShowSubtitlesHandler.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
@ -7,7 +8,6 @@ using Microsoft.Extensions.Logging; @@ -7,7 +8,6 @@ using Microsoft.Extensions.Logging;
using Dapper;
using ErsatzTV.Application.Maintenance;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Subtitles;
@ -16,9 +16,9 @@ public class ExtractEmbeddedShowSubtitlesHandler( @@ -16,9 +16,9 @@ public class ExtractEmbeddedShowSubtitlesHandler(
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILogger<ExtractEmbeddedSubtitlesHandler> logger)
: ExtractEmbeddedSubtitlesHandlerBase(localFileSystem, logger),
: ExtractEmbeddedSubtitlesHandlerBase(fileSystem, logger),
IRequestHandler<ExtractEmbeddedShowSubtitles, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(

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

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
using System.Threading.Channels;
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Maintenance;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -23,12 +23,12 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa @@ -23,12 +23,12 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
public ExtractEmbeddedSubtitlesHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
IEntityLocker entityLocker,
IConfigElementRepository configElementRepository,
ChannelWriter<IBackgroundServiceRequest> workerChannel,
ILogger<ExtractEmbeddedSubtitlesHandler> logger)
: base(localFileSystem, logger)
: base(fileSystem, logger)
{
_dbContextFactory = dbContextFactory;
_entityLocker = entityLocker;

12
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
using System.Security.Cryptography;
using System.Text;
using CliWrap;
@ -8,7 +9,6 @@ using Dapper; @@ -8,7 +9,6 @@ using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -17,7 +17,7 @@ using Microsoft.Extensions.Logging; @@ -17,7 +17,7 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Subtitles;
[SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")]
public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem localFileSystem, ILogger logger)
public abstract class ExtractEmbeddedSubtitlesHandlerBase(IFileSystem fileSystem, ILogger logger)
{
protected static Task<Validation<BaseError, string>> FFmpegPathMustExist(
TvContext dbContext,
@ -133,7 +133,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local @@ -133,7 +133,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
{
string fullOutputPath = Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.OutputPath);
Directory.CreateDirectory(Path.GetDirectoryName(fullOutputPath));
if (localFileSystem.FileExists(fullOutputPath))
if (fileSystem.File.Exists(fullOutputPath))
{
File.Delete(fullOutputPath);
}
@ -193,7 +193,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local @@ -193,7 +193,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
}
string fullOutputPath = Path.Combine(FileSystemLayout.FontsCacheFolder, fontStream.FileName);
if (localFileSystem.FileExists(fullOutputPath))
if (fileSystem.File.Exists(fullOutputPath))
{
// already extracted
continue;
@ -212,7 +212,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local @@ -212,7 +212,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
// ffmpeg seems to return exit code 1 in all cases when dumping an attachment
// so ignore it and check success a different way
if (localFileSystem.FileExists(fullOutputPath))
if (fileSystem.File.Exists(fullOutputPath))
{
logger.LogDebug("Successfully extracted font {Font}", fontStream.FileName);
}
@ -300,7 +300,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local @@ -300,7 +300,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
{
foreach (string path in GetRelativeOutputPath(mediaItemId, subtitle))
{
return !localFileSystem.FileExists(path);
return !fileSystem.File.Exists(path);
}
return false;

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

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using System.IO.Abstractions;
using Dapper;
using ErsatzTV.Application.Streaming;
using ErsatzTV.Core;
@ -30,6 +31,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -30,6 +31,7 @@ public class PrepareTroubleshootingPlaybackHandler(
IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService,
IFFmpegProcessService ffmpegProcessService,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ISongVideoGenerator songVideoGenerator,
IWatermarkSelector watermarkSelector,
@ -471,7 +473,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -471,7 +473,7 @@ public class PrepareTroubleshootingPlaybackHandler(
string path = await GetLocalPath(mediaItem, cancellationToken);
// check filesystem first
if (localFileSystem.FileExists(path))
if (fileSystem.File.Exists(path))
{
if (mediaItem is RemoteStream remoteStream)
{

1
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -27,6 +27,7 @@ @@ -27,6 +27,7 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

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

@ -2,11 +2,11 @@ using Destructurama; @@ -2,11 +2,11 @@ using Destructurama;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Tests.Fakes;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Serilog;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Core.Tests.FFmpeg;
@ -94,9 +94,10 @@ public class CustomStreamSelectorTests @@ -94,9 +94,10 @@ public class CustomStreamSelectorTests
- "eng"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -123,9 +124,10 @@ public class CustomStreamSelectorTests @@ -123,9 +124,10 @@ public class CustomStreamSelectorTests
- audio_language: ["und"]
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -152,9 +154,10 @@ public class CustomStreamSelectorTests @@ -152,9 +154,10 @@ public class CustomStreamSelectorTests
- audio_language: ["en", "eng"]
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -187,9 +190,10 @@ public class CustomStreamSelectorTests @@ -187,9 +190,10 @@ public class CustomStreamSelectorTests
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -217,9 +221,10 @@ public class CustomStreamSelectorTests @@ -217,9 +221,10 @@ public class CustomStreamSelectorTests
- "en*"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -248,9 +253,10 @@ public class CustomStreamSelectorTests @@ -248,9 +253,10 @@ public class CustomStreamSelectorTests
""";
_audioVersion = GetTestAudioVersion("en");
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -279,9 +285,10 @@ public class CustomStreamSelectorTests @@ -279,9 +285,10 @@ public class CustomStreamSelectorTests
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -305,9 +312,10 @@ public class CustomStreamSelectorTests @@ -305,9 +312,10 @@ public class CustomStreamSelectorTests
- "eng"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -337,9 +345,10 @@ public class CustomStreamSelectorTests @@ -337,9 +345,10 @@ public class CustomStreamSelectorTests
- "en*"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -378,9 +387,10 @@ public class CustomStreamSelectorTests @@ -378,9 +387,10 @@ public class CustomStreamSelectorTests
}
];
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -413,9 +423,10 @@ public class CustomStreamSelectorTests @@ -413,9 +423,10 @@ public class CustomStreamSelectorTests
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -450,9 +461,10 @@ public class CustomStreamSelectorTests @@ -450,9 +461,10 @@ public class CustomStreamSelectorTests
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -492,9 +504,10 @@ public class CustomStreamSelectorTests @@ -492,9 +504,10 @@ public class CustomStreamSelectorTests
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -528,9 +541,10 @@ public class CustomStreamSelectorTests @@ -528,9 +541,10 @@ public class CustomStreamSelectorTests
disable_subtitles: true
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -568,9 +582,10 @@ public class CustomStreamSelectorTests @@ -568,9 +582,10 @@ public class CustomStreamSelectorTests
- "riff"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -600,9 +615,10 @@ public class CustomStreamSelectorTests @@ -600,9 +615,10 @@ public class CustomStreamSelectorTests
- "movie"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -634,9 +650,10 @@ public class CustomStreamSelectorTests @@ -634,9 +650,10 @@ public class CustomStreamSelectorTests
- "signs"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -668,9 +685,10 @@ public class CustomStreamSelectorTests @@ -668,9 +685,10 @@ public class CustomStreamSelectorTests
- "songs"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -699,9 +717,10 @@ public class CustomStreamSelectorTests @@ -699,9 +717,10 @@ public class CustomStreamSelectorTests
subtitle_condition: "forced"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -730,9 +749,10 @@ public class CustomStreamSelectorTests @@ -730,9 +749,10 @@ public class CustomStreamSelectorTests
subtitle_condition: "lang like 'en%' and external"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -761,9 +781,10 @@ public class CustomStreamSelectorTests @@ -761,9 +781,10 @@ public class CustomStreamSelectorTests
audio_condition: "title like '%movie%'"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -792,9 +813,10 @@ public class CustomStreamSelectorTests @@ -792,9 +813,10 @@ public class CustomStreamSelectorTests
audio_condition: "channels > 2"
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -822,9 +844,10 @@ public class CustomStreamSelectorTests @@ -822,9 +844,10 @@ public class CustomStreamSelectorTests
audio_title_blocklist: ["riff"]
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -853,9 +876,10 @@ public class CustomStreamSelectorTests @@ -853,9 +876,10 @@ public class CustomStreamSelectorTests
subtitle_language: ["jp","en*"]
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
@ -885,9 +909,10 @@ public class CustomStreamSelectorTests @@ -885,9 +909,10 @@ public class CustomStreamSelectorTests
subtitle_language: ["es*","de*"]
""";
var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
_logger);
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(TestFileName).Which(f => f.HasStringContent(YAML));
var streamSelector = new CustomStreamSelector(fileSystem, _logger);
StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,

7
ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Core.Tests.FFmpeg;
@ -61,7 +62,7 @@ public class FFmpegStreamSelectorTests @@ -61,7 +62,7 @@ public class FFmpegStreamSelectorTests
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(),
Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(),
new MockFileSystem(),
languageCodeService,
Substitute.For<ILogger<FFmpegStreamSelector>>());
@ -123,7 +124,7 @@ public class FFmpegStreamSelectorTests @@ -123,7 +124,7 @@ public class FFmpegStreamSelectorTests
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(),
Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(),
new MockFileSystem(),
languageCodeService,
Substitute.For<ILogger<FFmpegStreamSelector>>());
@ -173,7 +174,7 @@ public class FFmpegStreamSelectorTests @@ -173,7 +174,7 @@ public class FFmpegStreamSelectorTests
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
Substitute.For<IStreamSelectorRepository>(),
Substitute.For<IConfigElementRepository>(),
Substitute.For<ILocalFileSystem>(),
new MockFileSystem(),
languageCodeService,
Substitute.For<ILogger<FFmpegStreamSelector>>());

8
ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
namespace ErsatzTV.Core.Tests.Fakes;
public record FakeFileEntry(string Path)
{
public DateTime LastWriteTime { get; set; } = SystemTime.MinValueUtc;
public string Contents { get; set; }
}

3
ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
namespace ErsatzTV.Core.Tests.Fakes;
public record FakeFolderEntry(string Path);

99
ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs

@ -1,99 +0,0 @@ @@ -1,99 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
namespace ErsatzTV.Core.Tests.Fakes;
public class FakeLocalFileSystem : ILocalFileSystem
{
private readonly List<FakeFileEntry> _files;
private readonly List<FakeFolderEntry> _folders;
public FakeLocalFileSystem(List<FakeFileEntry> files) : this(files, new List<FakeFolderEntry>())
{
}
public FakeLocalFileSystem(List<FakeFileEntry> files, List<FakeFolderEntry> folders)
{
_files = files;
var allFolders = new List<string>(folders.Map(f => f.Path));
foreach (FakeFileEntry file in _files)
{
List<DirectoryInfo> moreFolders =
Split(new DirectoryInfo(Path.GetDirectoryName(file.Path) ?? string.Empty));
allFolders.AddRange(moreFolders.Map(i => i.FullName));
}
_folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList();
}
public Unit EnsureFolderExists(string folder) => Unit.Default;
public DateTime GetLastWriteTime(string path) =>
Optional(_files.SingleOrDefault(f => f.Path == path))
.Map(f => f.LastWriteTime)
.IfNone(SystemTime.MinValueUtc);
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
_folders.Any(f => f.Path == libraryPath.Path);
public IEnumerable<string> ListSubdirectories(string folder) =>
_folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder);
public IEnumerable<string> ListFiles(string folder) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
// TODO: this isn't accurate, need to use search pattern
public IEnumerable<string> ListFiles(string folder, string searchPattern) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
public IEnumerable<string> ListFiles(string folder, params string[] searchPatterns) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
public bool FileExists(string path) => _files.Any(f => f.Path == path);
public bool FolderExists(string folder) => false;
public Task<Either<BaseError, Unit>> CopyFile(string source, string destination) =>
Task.FromResult(Right<BaseError, Unit>(Unit.Default));
public Unit EmptyFolder(string folder) => Unit.Default;
public async Task<string> ReadAllText(string path) => await _files
.Filter(f => f.Path == path)
.HeadOrNone()
.Select(f => f.Contents)
.IfNoneAsync(string.Empty);
public async Task<string[]> ReadAllLines(string path) => await _files
.Filter(f => f.Path == path)
.HeadOrNone()
.Select(f => f.Contents)
.IfNoneAsync(string.Empty)
.Map(s => s.Split(Environment.NewLine));
public Task<byte[]> GetHash(string path) => throw new NotSupportedException();
public string GetCustomOrDefaultFile(string folder, string file)
{
string path = Path.Combine(folder, file);
return FileExists(path) ? path : Path.Combine(folder, $"_{file}");
}
private static List<DirectoryInfo> Split(DirectoryInfo path)
{
var result = new List<DirectoryInfo>();
if (path == null || string.IsNullOrWhiteSpace(path.FullName))
{
return result;
}
if (path.Parent != null)
{
result.AddRange(Split(path.Parent));
}
result.Add(path);
return result;
}
}

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

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
@ -8,6 +7,7 @@ using ErsatzTV.Core.Tests.Fakes; @@ -8,6 +7,7 @@ using ErsatzTV.Core.Tests.Fakes;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling;
@ -588,7 +588,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase @@ -588,7 +588,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -596,7 +595,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase @@ -596,7 +595,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -714,7 +713,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase @@ -714,7 +713,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -722,7 +720,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase @@ -722,7 +720,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -842,7 +840,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase @@ -842,7 +840,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -850,7 +847,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase @@ -850,7 +847,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);

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

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
@ -9,6 +8,7 @@ using ErsatzTV.Core.Tests.Fakes; @@ -9,6 +8,7 @@ using ErsatzTV.Core.Tests.Fakes;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling;
@ -597,7 +597,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -597,7 +597,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -605,7 +604,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -605,7 +604,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -717,7 +716,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -717,7 +716,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -725,7 +723,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -725,7 +723,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -888,7 +886,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -888,7 +886,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -896,7 +893,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -896,7 +893,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1018,7 +1015,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1018,7 +1015,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1026,7 +1022,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1026,7 +1022,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1147,7 +1143,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1147,7 +1143,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1155,7 +1150,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1155,7 +1150,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1277,7 +1272,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1277,7 +1272,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1285,7 +1279,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1285,7 +1279,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1412,7 +1406,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1412,7 +1406,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1420,7 +1413,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1420,7 +1413,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1547,7 +1540,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1547,7 +1540,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1555,7 +1547,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1555,7 +1547,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1691,7 +1683,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1691,7 +1683,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1699,7 +1690,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1699,7 +1690,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1828,7 +1819,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1828,7 +1819,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1836,7 +1826,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1836,7 +1826,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -1931,7 +1921,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1931,7 +1921,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -1939,7 +1928,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase @@ -1939,7 +1928,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);

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

@ -2,7 +2,6 @@ using Destructurama; @@ -2,7 +2,6 @@ using Destructurama;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
@ -11,6 +10,7 @@ using Microsoft.Extensions.Logging; @@ -11,6 +10,7 @@ using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Serilog;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling;
@ -66,7 +66,6 @@ public abstract class PlayoutBuilderTestBase @@ -66,7 +66,6 @@ public abstract class PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -74,7 +73,7 @@ public abstract class PlayoutBuilderTestBase @@ -74,7 +73,7 @@ public abstract class PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);
@ -182,7 +181,6 @@ public abstract class PlayoutBuilderTestBase @@ -182,7 +181,6 @@ public abstract class PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -190,7 +188,7 @@ public abstract class PlayoutBuilderTestBase @@ -190,7 +188,7 @@ public abstract class PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);

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

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
@ -8,6 +7,7 @@ using ErsatzTV.Core.Tests.Fakes; @@ -8,6 +7,7 @@ using ErsatzTV.Core.Tests.Fakes;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling;
@ -101,7 +101,6 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase @@ -101,7 +101,6 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
IRerunHelper rerunHelper = Substitute.For<IRerunHelper>();
var builder = new PlayoutBuilder(
configRepo,
@ -109,7 +108,7 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase @@ -109,7 +108,7 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
new MockFileSystem(),
rerunHelper,
Logger);

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

@ -24,6 +24,7 @@ using NUnit.Framework; @@ -24,6 +24,7 @@ using NUnit.Framework;
using Serilog;
using Serilog.Events;
using Serilog.Extensions.Logging;
using MockFileSystem = Testably.Abstractions.Testing.MockFileSystem;
namespace ErsatzTV.Core.Tests.Scheduling;
@ -108,6 +109,7 @@ public class ScheduleIntegrationTests @@ -108,6 +109,7 @@ public class ScheduleIntegrationTests
ISearchIndex searchIndex = provider.GetRequiredService<ISearchIndex>();
await searchIndex.Initialize(
new LocalFileSystem(
new MockFileSystem(),
provider.GetRequiredService<IClient>(),
provider.GetRequiredService<ILogger<LocalFileSystem>>()),
provider.GetRequiredService<IConfigElementRepository>(),
@ -125,7 +127,7 @@ public class ScheduleIntegrationTests @@ -125,7 +127,7 @@ public class ScheduleIntegrationTests
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
new ArtistRepository(factory),
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
Substitute.For<ILocalFileSystem>(),
new MockFileSystem(),
Substitute.For<IRerunHelper>(),
provider.GetRequiredService<ILogger<PlayoutBuilder>>());
@ -321,7 +323,7 @@ public class ScheduleIntegrationTests @@ -321,7 +323,7 @@ public class ScheduleIntegrationTests
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
new ArtistRepository(factory),
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
Substitute.For<ILocalFileSystem>(),
new MockFileSystem(),
Substitute.For<IRerunHelper>(),
provider.GetRequiredService<ILogger<PlayoutBuilder>>());

1
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -32,6 +32,7 @@ @@ -32,6 +32,7 @@
<PackageReference Include="SkiaSharp" Version="3.119.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageReference Include="Testably.Abstractions" Version="10.0.0" />
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

8
ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using System.IO.Abstractions;
using System.IO.Enumeration;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg.Selector;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using Microsoft.Extensions.Logging;
using NCalc;
using YamlDotNet.Serialization;
@ -10,7 +10,7 @@ using YamlDotNet.Serialization.NamingConventions; @@ -10,7 +10,7 @@ using YamlDotNet.Serialization.NamingConventions;
namespace ErsatzTV.Core.FFmpeg;
public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<CustomStreamSelector> logger)
public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSelector> logger)
: ICustomStreamSelector
{
public async Task<StreamSelectorResult> SelectStreams(
@ -25,7 +25,7 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust @@ -25,7 +25,7 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust
FileSystemLayout.ChannelStreamSelectorsFolder,
channel.StreamSelector);
if (!localFileSystem.FileExists(streamSelectorFile))
if (!fileSystem.File.Exists(streamSelectorFile))
{
logger.LogWarning("YAML stream selector file {File} does not exist; aborting.", channel.StreamSelector);
return StreamSelectorResult.None;
@ -327,7 +327,7 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust @@ -327,7 +327,7 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<Cust
{
try
{
string yaml = await localFileSystem.ReadAllText(streamSelectorFile);
string yaml = await fileSystem.File.ReadAllTextAsync(streamSelectorFile);
IDeserializer deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)

11
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Abstractions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
@ -15,7 +16,7 @@ namespace ErsatzTV.Core.FFmpeg; @@ -15,7 +16,7 @@ namespace ErsatzTV.Core.FFmpeg;
public class FFmpegStreamSelector : IFFmpegStreamSelector
{
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly IFileSystem _fileSystem;
private readonly ILanguageCodeService _languageCodeService;
private readonly ILogger<FFmpegStreamSelector> _logger;
private readonly IScriptEngine _scriptEngine;
@ -25,14 +26,14 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -25,14 +26,14 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
IScriptEngine scriptEngine,
IStreamSelectorRepository streamSelectorRepository,
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILanguageCodeService languageCodeService,
ILogger<FFmpegStreamSelector> logger)
{
_scriptEngine = scriptEngine;
_streamSelectorRepository = streamSelectorRepository;
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_languageCodeService = languageCodeService;
_logger = logger;
}
@ -318,7 +319,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -318,7 +319,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
"js");
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
if (!_localFileSystem.FileExists(jsScriptPath))
if (!_fileSystem.File.Exists(jsScriptPath))
{
_logger.LogDebug("Unable to locate episode audio stream selector script; falling back to built-in logic");
return Option<MediaStream>.None;
@ -358,7 +359,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -358,7 +359,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
"js");
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
if (!_localFileSystem.FileExists(jsScriptPath))
if (!_fileSystem.File.Exists(jsScriptPath))
{
_logger.LogDebug(
"Unable to locate movie audio stream selector script; falling back to built-in logic");

9
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs

@ -1,22 +1,15 @@ @@ -1,22 +1,15 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Metadata;
namespace ErsatzTV.Core.Interfaces.Metadata;
public interface ILocalFileSystem
{
Unit EnsureFolderExists(string folder);
DateTime GetLastWriteTime(string path);
bool IsLibraryPathAccessible(LibraryPath libraryPath);
IEnumerable<string> ListSubdirectories(string folder);
IEnumerable<string> ListFiles(string folder);
IEnumerable<string> ListFiles(string folder, string searchPattern);
IEnumerable<string> ListFiles(string folder, params string[] searchPatterns);
bool FileExists(string path);
bool FolderExists(string folder);
Task<Either<BaseError, Unit>> CopyFile(string source, string destination);
Unit EmptyFolder(string folder);
Task<string> ReadAllText(string path);
Task<string[]> ReadAllLines(string path);
Task<byte[]> GetHash(string path);
string GetCustomOrDefaultFile(string folder, string file);
}

59
ErsatzTV.Core/Metadata/LocalFileSystem.cs

@ -1,21 +1,21 @@ @@ -1,21 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
using System.Security.Cryptography;
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Metadata;
public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : ILocalFileSystem
public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger<LocalFileSystem> logger) : ILocalFileSystem
{
public Unit EnsureFolderExists(string folder)
{
try
{
if (folder != null && !Directory.Exists(folder))
if (folder != null && !fileSystem.Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
fileSystem.Directory.CreateDirectory(folder);
}
}
catch (Exception ex)
@ -30,7 +30,7 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -30,7 +30,7 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
{
try
{
return File.GetLastWriteTimeUtc(path);
return fileSystem.File.GetLastWriteTimeUtc(path);
}
catch
{
@ -38,16 +38,13 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -38,16 +38,13 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
}
}
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
Directory.Exists(libraryPath.Path);
public IEnumerable<string> ListSubdirectories(string folder)
{
if (Directory.Exists(folder))
if (fileSystem.Directory.Exists(folder))
{
try
{
return Directory.EnumerateDirectories(folder);
return fileSystem.Directory.EnumerateDirectories(folder);
}
catch (UnauthorizedAccessException)
{
@ -65,11 +62,11 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -65,11 +62,11 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
public IEnumerable<string> ListFiles(string folder)
{
if (Directory.Exists(folder))
if (fileSystem.Directory.Exists(folder))
{
try
{
return Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly)
return fileSystem.Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly)
.Where(path => !Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase));
}
catch (UnauthorizedAccessException)
@ -88,11 +85,11 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -88,11 +85,11 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
public IEnumerable<string> ListFiles(string folder, string searchPattern)
{
if (folder is not null && Directory.Exists(folder))
if (folder is not null && fileSystem.Directory.Exists(folder))
{
try
{
return Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly)
return fileSystem.Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly)
.Where(path => !Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase));
}
catch (UnauthorizedAccessException)
@ -111,14 +108,15 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -111,14 +108,15 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
public IEnumerable<string> ListFiles(string folder, params string[] searchPatterns)
{
if (folder is not null && Directory.Exists(folder))
if (folder is not null && fileSystem.Directory.Exists(folder))
{
try
{
return searchPatterns
.SelectMany(searchPattern =>
Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly)
.Where(path => !Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)))
fileSystem.Directory.EnumerateFiles(folder, searchPattern, SearchOption.TopDirectoryOnly)
.Where(path =>
!Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)))
.Distinct();
}
catch (UnauthorizedAccessException)
@ -135,22 +133,18 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -135,22 +133,18 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
return new List<string>();
}
public bool FileExists(string path) => File.Exists(path);
public bool FolderExists(string folder) => Directory.Exists(folder);
public async Task<Either<BaseError, Unit>> CopyFile(string source, string destination)
{
try
{
string directory = Path.GetDirectoryName(destination) ?? string.Empty;
if (!Directory.Exists(directory))
if (!fileSystem.Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
fileSystem.Directory.CreateDirectory(directory);
}
await using FileStream sourceStream = File.OpenRead(source);
await using FileStream destinationStream = File.Create(destination);
await using FileSystemStream sourceStream = fileSystem.File.OpenRead(source);
await using FileSystemStream destinationStream = fileSystem.File.Create(destination);
await sourceStream.CopyToAsync(destinationStream);
return Unit.Default;
@ -166,14 +160,14 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -166,14 +160,14 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
{
try
{
foreach (string file in Directory.GetFiles(folder))
foreach (string file in fileSystem.Directory.GetFiles(folder))
{
File.Delete(file);
fileSystem.File.Delete(file);
}
foreach (string directory in Directory.GetDirectories(folder))
foreach (string directory in fileSystem.Directory.GetDirectories(folder))
{
Directory.Delete(directory, true);
fileSystem.Directory.Delete(directory, true);
}
}
catch (Exception ex)
@ -184,20 +178,17 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -184,20 +178,17 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
return Unit.Default;
}
public Task<string> ReadAllText(string path) => File.ReadAllTextAsync(path);
public Task<string[]> ReadAllLines(string path) => File.ReadAllLinesAsync(path);
[SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")]
public async Task<byte[]> GetHash(string path)
{
using var md5 = MD5.Create();
await using var stream = File.OpenRead(path);
await using var stream = fileSystem.File.OpenRead(path);
return await md5.ComputeHashAsync(stream);
}
public string GetCustomOrDefaultFile(string folder, string file)
{
string path = Path.Combine(folder, file);
return FileExists(path) ? path : Path.Combine(folder, $"_{file}");
return fileSystem.File.Exists(path) ? path : Path.Combine(folder, $"_{file}");
}
}

16
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
using System.Reflection;
using System.IO.Abstractions;
using System.Reflection;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.Engine;
@ -20,10 +20,10 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -20,10 +20,10 @@ public class PlayoutBuilder : IPlayoutBuilder
{
private readonly IArtistRepository _artistRepository;
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly IRerunHelper _rerunHelper;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory;
private readonly IFileSystem _fileSystem;
private readonly ITelevisionRepository _televisionRepository;
private Playlist _debugPlaylist;
private ILogger<PlayoutBuilder> _logger;
@ -34,7 +34,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -34,7 +34,7 @@ public class PlayoutBuilder : IPlayoutBuilder
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
IMultiEpisodeShuffleCollectionEnumeratorFactory multiEpisodeFactory,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
IRerunHelper rerunHelper,
ILogger<PlayoutBuilder> logger)
{
@ -43,7 +43,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -43,7 +43,7 @@ public class PlayoutBuilder : IPlayoutBuilder
_televisionRepository = televisionRepository;
_artistRepository = artistRepository;
_multiEpisodeFactory = multiEpisodeFactory;
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_rerunHelper = rerunHelper;
_logger = logger;
}
@ -400,12 +400,12 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -400,12 +400,12 @@ public class PlayoutBuilder : IPlayoutBuilder
name =>
{
_logger.LogError(
"Unable to rebuild playout; {CollectionType} {CollectionName} has no valid items!",
"Unable to rebuild playout; {CollectionType} \"{CollectionName}\" has no valid items!",
emptyCollection.CollectionType,
name);
return BaseError.New(
$"Unable to rebuild playout; {emptyCollection.CollectionType} {name} has no valid items!");
$"Unable to rebuild playout; {emptyCollection.CollectionType} \"{name}\" has no valid items!");
},
() =>
{
@ -1403,7 +1403,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -1403,7 +1403,7 @@ public class PlayoutBuilder : IPlayoutBuilder
guid.Guid.Replace("://", "_")),
"js");
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
if (_localFileSystem.FileExists(jsScriptPath))
if (_fileSystem.File.Exists(jsScriptPath))
{
_logger.LogDebug("Found JS Script at {Path}", jsScriptPath);
try

6
ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using System.CommandLine.Parsing;
using System.IO.Abstractions;
using CliWrap;
using CliWrap.Buffered;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.Engine;
@ -14,7 +14,7 @@ public class ScriptedPlayoutBuilder( @@ -14,7 +14,7 @@ public class ScriptedPlayoutBuilder(
IConfigElementRepository configElementRepository,
IScriptedPlayoutBuilderService scriptedPlayoutBuilderService,
ISchedulingEngine schedulingEngine,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILogger<ScriptedPlayoutBuilder> logger)
: IScriptedPlayoutBuilder
{
@ -38,7 +38,7 @@ public class ScriptedPlayoutBuilder( @@ -38,7 +38,7 @@ public class ScriptedPlayoutBuilder(
string scriptFile = args[0];
string[] scriptArgs = args.Skip(1).ToArray();
if (!localFileSystem.FileExists(scriptFile))
if (!fileSystem.File.Exists(scriptFile))
{
logger.LogError(
"Cannot build scripted playout; schedule file {File} does not exist",

6
ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
//using ErsatzTV.Core.Scheduling.Engine;
@ -17,7 +17,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling; @@ -17,7 +17,7 @@ namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public class SequentialPlayoutBuilder(
//ISchedulingEngine schedulingEngine,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
IConfigElementRepository configElementRepository,
IMediaCollectionRepository mediaCollectionRepository,
IChannelRepository channelRepository,
@ -38,7 +38,7 @@ public class SequentialPlayoutBuilder( @@ -38,7 +38,7 @@ public class SequentialPlayoutBuilder(
PlayoutBuildResult result = PlayoutBuildResult.Empty;
if (!localFileSystem.FileExists(playout.ScheduleFile))
if (!fileSystem.File.Exists(playout.ScheduleFile))
{
logger.LogWarning("Sequential schedule file {File} does not exist; aborting.", playout.ScheduleFile);
return BaseError.New($"Sequential schedule file {playout.ScheduleFile} does not exist");

7
ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj

@ -22,10 +22,17 @@ @@ -22,10 +22,17 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\ErsatzTV\Resources\sequential-schedule.schema.json">
<Link>Resources/sequential-schedule.schema.json</Link>
</EmbeddedResource>
</ItemGroup>
</Project>

2
ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Infrastructure.Tests.Metadata;
@ -21,6 +22,7 @@ public class LocalStatisticsProviderTests @@ -21,6 +22,7 @@ public class LocalStatisticsProviderTests
{
var provider = new LocalStatisticsProvider(
Substitute.For<IMetadataRepository>(),
new MockFileSystem(),
Substitute.For<ILocalFileSystem>(),
Substitute.For<IClient>(),
Substitute.For<IHardwareCapabilitiesFactory>(),

200
ErsatzTV.Infrastructure.Tests/Scheduling/SequentialScheduleValidatorTests.cs

@ -0,0 +1,200 @@ @@ -0,0 +1,200 @@
using System.Reflection;
using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Scheduling;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
namespace ErsatzTV.Infrastructure.Tests.Scheduling;
[TestFixture]
public class SequentialScheduleValidatorTests
{
private readonly string _schema;
public SequentialScheduleValidatorTests()
{
var assembly = Assembly.GetAssembly(typeof(SequentialScheduleValidatorTests));
assembly.ShouldNotBeNull();
using var stream = assembly.GetManifestResourceStream(
"ErsatzTV.Infrastructure.Tests.Resources.sequential-schedule.schema.json");
stream.ShouldNotBeNull();
using var reader = new StreamReader(stream);
_schema = reader.ReadToEnd();
}
[CancelAfter(2_000)]
[Test]
public async Task ValidateSchedule_Should_Succeed_Valid_Schedule(CancellationToken cancellationToken)
{
const string YAML =
"""
content:
- show:
key: "SOME_SHOW"
guids:
- source: "imdb"
value: "tt123456"
order: chronological
- search:
key: "FILLER"
query: "type:other_video"
order: "shuffle"
reset:
- wait_until: '8:00am'
tomorrow: false
rewind_on_reset: true
playout:
- duration: "30 minutes"
content: "SOME_SHOW"
discard_attempts: 2
offline_tail: false
- epg_group: true
advance: false
- pad_to_next: 30
content: "FILLER"
filler_kind: postroll
trim: true
- epg_group: false
- repeat: true
""";
string schemaFileName = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "sequential-schedule.schema.json");
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(schemaFileName).Which(f => f.HasStringContent(_schema));
var validator = new SequentialScheduleValidator(
fileSystem,
Substitute.For<ILogger<SequentialScheduleValidator>>());
bool result = await validator.ValidateSchedule(YAML, false);
result.ShouldBeTrue();
}
[CancelAfter(2_000)]
[Test]
public async Task ValidateSchedule_Should_Fail_Invalid_Schedule(CancellationToken cancellationToken)
{
const string YAML =
"""
content:
- show:
key: "SOME_SHOW"
guids22:
- source: "imdb"
value: "tt123456"
order: chronological
- search:
key: "FILLER"
query: "type:other_video"
order: "shuffle"
reset:
- wait_until: '8:00am'
tomorrow: false
rewind_on_reset: true
playout:
- duration: "30 minutes"
content: "SOME_SHOW"
discard_attempts: 2
offline_tail: false
- epg_group: true
advance: false
- pad_to_next: 30
content: "FILLER"
filler_kind: postroll
trim: true
- epg_group: false
- repeat: true
""";
string schemaFileName = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "sequential-schedule.schema.json");
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(schemaFileName).Which(f => f.HasStringContent(_schema));
var validator = new SequentialScheduleValidator(
fileSystem,
Substitute.For<ILogger<SequentialScheduleValidator>>());
bool result = await validator.ValidateSchedule(YAML, false);
result.ShouldBeFalse();
}
[CancelAfter(2_000)]
[Test]
public async Task GetValidationMessages_With_Invalid_Schedule(CancellationToken cancellationToken)
{
const string YAML =
"""
content:
- show:
key: "SOME_SHOW"
guids22:
- source: "imdb"
value: "tt123456"
order: chronological
- search:
key: "FILLER"
query: "type:other_video"
order: "shuffle"
reset:
- wait_until: '8:00am'
tomorrow: false
rewind_on_reset: true
playout:
- duration: "30 minutes"
content: "SOME_SHOW"
discard_attempts: 2
offline_tail: false
- epg_group: true
advance: false
- pad_to_next: 30
content: "FILLER"
filler_kind: postroll
trim: true
- epg_group: false
- repeat: true
""";
string schemaFileName = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "sequential-schedule.schema.json");
var fileSystem = new MockFileSystem();
fileSystem.Initialize()
.WithFile(schemaFileName).Which(f => f.HasStringContent(_schema));
var validator = new SequentialScheduleValidator(
fileSystem,
Substitute.For<ILogger<SequentialScheduleValidator>>());
IList<string> result = await validator.GetValidationMessages(YAML, false);
result.ShouldNotBeNull();
result.Count.ShouldBe(1);
result[0].ShouldContain("line 3");
result[0].ShouldContain("position 5");
}
}

46
ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs

@ -1,26 +1,18 @@ @@ -1,26 +1,18 @@
using Dapper;
using System.IO.Abstractions;
using Dapper;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class LibraryRepository : ILibraryRepository
public class LibraryRepository(IFileSystem fileSystem, IDbContextFactory<TvContext> dbContextFactory)
: ILibraryRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
public LibraryRepository(ILocalFileSystem localFileSystem, IDbContextFactory<TvContext> dbContextFactory)
{
_localFileSystem = localFileSystem;
_dbContextFactory = dbContextFactory;
}
public async Task<LibraryPath> Add(LibraryPath libraryPath)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
await dbContext.LibraryPaths.AddAsync(libraryPath);
await dbContext.SaveChangesAsync();
return libraryPath;
@ -28,7 +20,7 @@ public class LibraryRepository : ILibraryRepository @@ -28,7 +20,7 @@ public class LibraryRepository : ILibraryRepository
public async Task<Option<Library>> GetLibrary(int libraryId)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Libraries
.Include(l => l.Paths)
.ThenInclude(p => p.LibraryFolders)
@ -40,7 +32,7 @@ public class LibraryRepository : ILibraryRepository @@ -40,7 +32,7 @@ public class LibraryRepository : ILibraryRepository
public async Task<Option<LocalLibrary>> GetLocal(int libraryId)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.LocalLibraries
.OrderBy(l => l.Id)
.SingleOrDefaultAsync(l => l.Id == libraryId)
@ -49,7 +41,7 @@ public class LibraryRepository : ILibraryRepository @@ -49,7 +41,7 @@ public class LibraryRepository : ILibraryRepository
public async Task<List<Library>> GetAll()
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Libraries
.AsNoTracking()
.Include(l => l.MediaSource)
@ -59,7 +51,7 @@ public class LibraryRepository : ILibraryRepository @@ -59,7 +51,7 @@ public class LibraryRepository : ILibraryRepository
public async Task<Unit> UpdateLastScan(Library library)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.ExecuteAsync(
"UPDATE Library SET LastScan = @LastScan WHERE Id = @Id",
new { library.LastScan, library.Id }).ToUnit();
@ -67,7 +59,7 @@ public class LibraryRepository : ILibraryRepository @@ -67,7 +59,7 @@ public class LibraryRepository : ILibraryRepository
public async Task<Unit> UpdateLastScan(LibraryPath libraryPath)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.ExecuteAsync(
"UPDATE LibraryPath SET LastScan = @LastScan WHERE Id = @Id",
new { libraryPath.LastScan, libraryPath.Id }).ToUnit();
@ -75,7 +67,7 @@ public class LibraryRepository : ILibraryRepository @@ -75,7 +67,7 @@ public class LibraryRepository : ILibraryRepository
public async Task<List<LibraryPath>> GetLocalPaths(int libraryId)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.LocalLibraries
.Include(l => l.Paths)
.OrderBy(l => l.Id)
@ -86,7 +78,7 @@ public class LibraryRepository : ILibraryRepository @@ -86,7 +78,7 @@ public class LibraryRepository : ILibraryRepository
public async Task<int> CountMediaItemsByPath(int libraryPathId)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QuerySingleAsync<int>(
@"SELECT COUNT(*) FROM MediaItem WHERE LibraryPathId = @LibraryPathId",
new { LibraryPathId = libraryPathId });
@ -98,7 +90,7 @@ public class LibraryRepository : ILibraryRepository @@ -98,7 +90,7 @@ public class LibraryRepository : ILibraryRepository
string path,
string etag)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
foreach (LibraryFolder folder in knownFolder)
{
@ -123,10 +115,10 @@ public class LibraryRepository : ILibraryRepository @@ -123,10 +115,10 @@ public class LibraryRepository : ILibraryRepository
public async Task CleanEtagsForLibraryPath(LibraryPath libraryPath)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
IOrderedEnumerable<LibraryFolder> orderedFolders = libraryPath.LibraryFolders
.Where(f => !_localFileSystem.FolderExists(f.Path))
.Where(f => !fileSystem.Directory.Exists(f.Path))
.OrderByDescending(lp => lp.Path.Length);
foreach (LibraryFolder folder in orderedFolders)
@ -152,7 +144,7 @@ public class LibraryRepository : ILibraryRepository @@ -152,7 +144,7 @@ public class LibraryRepository : ILibraryRepository
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.LibraryFolders
.AsNoTracking()
@ -166,7 +158,7 @@ public class LibraryRepository : ILibraryRepository @@ -166,7 +158,7 @@ public class LibraryRepository : ILibraryRepository
Option<int> maybeParentFolder,
string folder)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
// load from db or create new folder
LibraryFolder knownFolder = await libraryPath.LibraryFolders
@ -199,7 +191,7 @@ public class LibraryRepository : ILibraryRepository @@ -199,7 +191,7 @@ public class LibraryRepository : ILibraryRepository
public async Task UpdateLibraryFolderId(MediaFile mediaFile, int libraryFolderId)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
mediaFile.LibraryFolderId = libraryFolderId;
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaFile SET LibraryFolderId = @LibraryFolderId WHERE Id = @Id",
@ -208,7 +200,7 @@ public class LibraryRepository : ILibraryRepository @@ -208,7 +200,7 @@ public class LibraryRepository : ILibraryRepository
public async Task UpdatePath(LibraryPath libraryPath, string normalizedLibraryPath)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
libraryPath.Path = normalizedLibraryPath;
await dbContext.Connection.ExecuteAsync(
"UPDATE LibraryPath SET Path = @Path WHERE Id = @Id",

5
ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs

@ -1087,7 +1087,10 @@ public class MediaCollectionRepository : IMediaCollectionRepository @@ -1087,7 +1087,10 @@ public class MediaCollectionRepository : IMediaCollectionRepository
.AsNoTracking()
.SelectOneAsync(a => a.Id, a => a.Id == emptyCollection.MediaItemId.Value, cancellationToken)
.MapT(s => s.ShowMetadata.Head().Title),
// TODO: get playlist name
CollectionType.Playlist => await dbContext.Playlists
.AsNoTracking()
.SelectOneAsync(p => p.Id, p => p.Id == emptyCollection.PlaylistId.Value, cancellationToken)
.MapT(p => p.Name),
_ => None
};
}

8
ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using System.Globalization;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Epg;
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore; @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class TemplateDataRepository(ILocalFileSystem localFileSystem, IDbContextFactory<TvContext> dbContextFactory)
public class TemplateDataRepository(IFileSystem fileSystem, IDbContextFactory<TvContext> dbContextFactory)
: ITemplateDataRepository
{
public async Task<Option<Dictionary<string, object>>> GetMediaItemTemplateData(
@ -65,9 +65,9 @@ public class TemplateDataRepository(ILocalFileSystem localFileSystem, IDbContext @@ -65,9 +65,9 @@ public class TemplateDataRepository(ILocalFileSystem localFileSystem, IDbContext
}
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (localFileSystem.FileExists(targetFile))
if (fileSystem.File.Exists(targetFile))
{
await using FileStream stream = File.OpenRead(targetFile);
await using FileSystemStream stream = fileSystem.File.OpenRead(targetFile);
List<EpgProgramme> xmlProgrammes = EpgReader.FindProgrammesAt(stream, time, count);
var result = new List<Dictionary<string, object>>();

1
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.2.1" />
<PackageReference Include="Jint" Version="4.4.2" />
<PackageReference Include="JsonSchema.Net" Version="7.4.0" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00017" />

8
ErsatzTV.Infrastructure/FFmpeg/MpegTsScriptService.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.IO.Abstractions;
using System.Runtime.InteropServices;
using CliWrap;
using ErsatzTV.Core;
@ -15,6 +16,7 @@ using YamlDotNet.Serialization.NamingConventions; @@ -15,6 +16,7 @@ using YamlDotNet.Serialization.NamingConventions;
namespace ErsatzTV.Infrastructure.FFmpeg;
public class MpegTsScriptService(
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ITempFilePool tempFilePool,
ILogger<MpegTsScriptService> logger) : IMpegTsScriptService
@ -26,9 +28,9 @@ public class MpegTsScriptService( @@ -26,9 +28,9 @@ public class MpegTsScriptService(
foreach (string folder in localFileSystem.ListSubdirectories(FileSystemLayout.MpegTsScriptsFolder))
{
string definition = Path.Combine(folder, "mpegts.yml");
if (!Scripts.ContainsKey(folder) && localFileSystem.FileExists(definition))
if (!Scripts.ContainsKey(folder) && fileSystem.File.Exists(definition))
{
Option<MpegTsScript> maybeScript = FromYaml(await localFileSystem.ReadAllText(definition));
Option<MpegTsScript> maybeScript = FromYaml(await fileSystem.File.ReadAllTextAsync(definition));
foreach (var script in maybeScript)
{
script.Id = Path.GetFileName(folder);
@ -94,7 +96,7 @@ public class MpegTsScriptService( @@ -94,7 +96,7 @@ public class MpegTsScriptService(
string channelName,
string ffmpegPath)
{
string script = await localFileSystem.ReadAllText(fileName);
string script = await fileSystem.File.ReadAllTextAsync(fileName);
try
{
var data = new Dictionary<string, string>

30
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Abstractions;
using System.Security.Cryptography;
using System.Text;
using Blurhash.SkiaSharp;
@ -14,27 +15,18 @@ using SkiaSharp; @@ -14,27 +15,18 @@ using SkiaSharp;
namespace ErsatzTV.Infrastructure.Images;
[SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms")]
public class ImageCache : IImageCache
public class ImageCache(IFileSystem fileSystem, ILocalFileSystem localFileSystem, ITempFilePool tempFilePool)
: IImageCache
{
private static readonly SHA1 Crypto;
private readonly ILocalFileSystem _localFileSystem;
private readonly ITempFilePool _tempFilePool;
static ImageCache() => Crypto = SHA1.Create();
public ImageCache(
ILocalFileSystem localFileSystem,
ITempFilePool tempFilePool)
{
_localFileSystem = localFileSystem;
_tempFilePool = tempFilePool;
}
public async Task<Either<BaseError, string>> SaveArtworkToCache(Stream stream, ArtworkKind artworkKind)
{
try
{
string tempFileName = _tempFilePool.GetNextTempFile(TempFileCategory.CachedArtwork);
string tempFileName = tempFilePool.GetNextTempFile(TempFileCategory.CachedArtwork);
// ReSharper disable once UseAwaitUsing
using (var fs = new FileStream(tempFileName, FileMode.OpenOrCreate, FileAccess.Write))
{
@ -63,7 +55,7 @@ public class ImageCache : IImageCache @@ -63,7 +55,7 @@ public class ImageCache : IImageCache
Directory.CreateDirectory(baseFolder);
}
await _localFileSystem.CopyFile(tempFileName, target);
await localFileSystem.CopyFile(tempFileName, target);
return hex;
}
@ -77,7 +69,7 @@ public class ImageCache : IImageCache @@ -77,7 +69,7 @@ public class ImageCache : IImageCache
{
try
{
var filenameKey = $"{path}:{_localFileSystem.GetLastWriteTime(path).ToFileTimeUtc()}";
var filenameKey = $"{path}:{localFileSystem.GetLastWriteTime(path).ToFileTimeUtc()}";
byte[] hash = Crypto.ComputeHash(Encoding.UTF8.GetBytes(filenameKey));
string hex = Convert.ToHexString(hash);
string subfolder = hex[..2];
@ -90,7 +82,7 @@ public class ImageCache : IImageCache @@ -90,7 +82,7 @@ public class ImageCache : IImageCache
_ => FileSystemLayout.LegacyImageCacheFolder
};
string target = Path.Combine(baseFolder, hex);
Either<BaseError, Unit> maybeResult = await _localFileSystem.CopyFile(path, target);
Either<BaseError, Unit> maybeResult = await localFileSystem.CopyFile(path, target);
return maybeResult.Match<Either<BaseError, string>>(
_ => hex,
error => error);
@ -159,14 +151,14 @@ public class ImageCache : IImageCache @@ -159,14 +151,14 @@ public class ImageCache : IImageCache
{
byte[] bytes = Encoding.UTF8.GetBytes(blurHash);
string base64 = Convert.ToBase64String(bytes).Replace("+", "_").Replace("/", "-").Replace("=", "");
string targetFile = GetPathForImage(base64, ArtworkKind.Poster, targetSize.Height);
if (!_localFileSystem.FileExists(targetFile))
string targetFile = GetPathForImage(base64, ArtworkKind.Poster, targetSize.Height) ?? string.Empty;
if (!fileSystem.File.Exists(targetFile))
{
string folder = Path.GetDirectoryName(targetFile);
_localFileSystem.EnsureFolderExists(folder);
localFileSystem.EnsureFolderExists(folder);
// ReSharper disable once ConvertToUsingDeclaration
using (FileStream fs = File.OpenWrite(targetFile))
using (FileSystemStream fs = fileSystem.File.OpenWrite(targetFile))
{
using (SKBitmap image = Blurhasher.Decode(blurHash, targetSize.Width, targetSize.Height))
{

6
ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Abstractions;
using System.Text;
using System.Text.RegularExpressions;
using Bugsnag;
@ -25,15 +26,18 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider @@ -25,15 +26,18 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<LocalStatisticsProvider> _logger;
private readonly IMetadataRepository _metadataRepository;
private readonly IFileSystem _fileSystem;
public LocalStatisticsProvider(
IMetadataRepository metadataRepository,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
IClient client,
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
ILogger<LocalStatisticsProvider> logger)
{
_metadataRepository = metadataRepository;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_client = client;
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
@ -146,7 +150,7 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider @@ -146,7 +150,7 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
}
if (filePath.StartsWith("http", StringComparison.OrdinalIgnoreCase) ||
!_localFileSystem.FileExists(filePath))
!_fileSystem.File.Exists(filePath))
{
_logger.LogDebug("Skipping interlaced ratio check for remote content");
return Option<double>.None;

62
ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
using System.Globalization;
using System.IO.Abstractions;
using System.Text.Json;
using System.Text.Json.Nodes;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
@ -8,30 +10,30 @@ using Newtonsoft.Json.Linq; @@ -8,30 +10,30 @@ using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using YamlDotNet.RepresentationModel;
using JsonSerializer = System.Text.Json.JsonSerializer;
using JsonSchemaNet = Json.Schema;
namespace ErsatzTV.Infrastructure.Scheduling;
public class SequentialScheduleValidator(ILogger<SequentialScheduleValidator> logger) : ISequentialScheduleValidator
public class SequentialScheduleValidator(IFileSystem fileSystem, ILogger<SequentialScheduleValidator> logger)
: ISequentialScheduleValidator
{
public async Task<bool> ValidateSchedule(string yaml, bool isImport)
{
try
{
string schemaFileName = Path.Combine(
FileSystemLayout.ResourcesCacheFolder,
isImport ? "sequential-schedule-import.schema.json" : "sequential-schedule.schema.json");
using StreamReader sr = File.OpenText(schemaFileName);
await using var reader = new JsonTextReader(sr);
var schema = JSchema.Load(reader);
string schemaFileName = GetSchemaPath(isImport);
string schemaText = await fileSystem.File.ReadAllTextAsync(schemaFileName);
JsonSchemaNet.JsonSchema schema = JsonSchemaNet.JsonSchema.FromText(schemaText);
string jsonString = ConvertYamlToJsonString(yaml);
JsonNode jsonNode = JsonNode.Parse(jsonString);
using var textReader = new StringReader(yaml);
var yamlStream = new YamlStream();
yamlStream.Load(textReader);
var schedule = JObject.Parse(Convert(yamlStream));
JsonSchemaNet.EvaluationResults result = schema.Evaluate(jsonNode);
if (!schedule.IsValid(schema, out IList<string> errorMessages))
if (!result.IsValid)
{
logger.LogWarning("Failed to validate sequential schedule definition: {ErrorMessages}", errorMessages);
logger.LogWarning("Sequential schedule definition failed validation");
return false;
}
@ -47,30 +49,27 @@ public class SequentialScheduleValidator(ILogger<SequentialScheduleValidator> lo @@ -47,30 +49,27 @@ public class SequentialScheduleValidator(ILogger<SequentialScheduleValidator> lo
public string ToJson(string yaml)
{
using var textReader = new StringReader(yaml);
var yamlStream = new YamlStream();
yamlStream.Load(textReader);
var schedule = JObject.Parse(Convert(yamlStream));
string jsonString = ConvertYamlToJsonString(yaml);
var schedule = JObject.Parse(jsonString);
string formatted = JsonConvert.SerializeObject(schedule, Formatting.Indented);
string[] lines = formatted.Split('\n');
return string.Join('\n', lines.Select((line, index) => $"{index + 1,4}: {line}"));
}
// limited to 1000/hr, but only called manually from UI
public async Task<IList<string>> GetValidationMessages(string yaml, bool isImport)
{
try
{
string schemaFileName = Path.Combine(
FileSystemLayout.ResourcesCacheFolder,
isImport ? "sequential-schedule-import.schema.json" : "sequential-schedule.schema.json");
using StreamReader sr = File.OpenText(schemaFileName);
string schemaFileName = GetSchemaPath(isImport);
using StreamReader sr = fileSystem.File.OpenText(schemaFileName);
await using var reader = new JsonTextReader(sr);
var schema = JSchema.Load(reader);
using var textReader = new StringReader(yaml);
var yamlStream = new YamlStream();
yamlStream.Load(textReader);
var schedule = JObject.Parse(Convert(yamlStream));
string jsonString = ConvertYamlToJsonString(yaml);
var schedule = JObject.Parse(jsonString);
return schedule.IsValid(schema, out IList<string> errorMessages) ? [] : errorMessages;
}
@ -80,13 +79,24 @@ public class SequentialScheduleValidator(ILogger<SequentialScheduleValidator> lo @@ -80,13 +79,24 @@ public class SequentialScheduleValidator(ILogger<SequentialScheduleValidator> lo
}
}
private static string Convert(YamlStream yamlStream)
private static string ConvertYamlToJsonString(string yaml)
{
using var textReader = new StringReader(yaml);
var yamlStream = new YamlStream();
yamlStream.Load(textReader);
var visitor = new YamlToJsonVisitor();
yamlStream.Accept(visitor);
return JsonConvert.SerializeObject(JsonConvert.DeserializeObject(visitor.JsonString), Formatting.Indented);
}
private static string GetSchemaPath(bool isImport)
{
return Path.Combine(
FileSystemLayout.ResourcesCacheFolder,
isImport ? "sequential-schedule-import.schema.json" : "sequential-schedule.schema.json");
}
private sealed class YamlToJsonVisitor : IYamlVisitor
{
private readonly JsonSerializerOptions _options = new() { WriteIndented = false };

13
ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Globalization;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
@ -19,7 +20,7 @@ namespace ErsatzTV.Infrastructure.Streaming; @@ -19,7 +20,7 @@ namespace ErsatzTV.Infrastructure.Streaming;
public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly IFileSystem _fileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger<ExternalJsonPlayoutItemProvider> _logger;
private readonly IPlexPathReplacementService _plexPathReplacementService;
@ -28,7 +29,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider @@ -28,7 +29,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
public ExternalJsonPlayoutItemProvider(
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
IPlexPathReplacementService plexPathReplacementService,
IPlexServerApiClient plexServerApiClient,
IPlexSecretStore plexSecretStore,
@ -36,7 +37,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider @@ -36,7 +37,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
ILogger<ExternalJsonPlayoutItemProvider> logger)
{
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_plexPathReplacementService = plexPathReplacementService;
_plexServerApiClient = plexServerApiClient;
_plexSecretStore = plexSecretStore;
@ -62,7 +63,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider @@ -62,7 +63,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
if (playout.ScheduleKind == PlayoutScheduleKind.ExternalJson)
{
// json file must exist
if (_localFileSystem.FileExists(playout.ScheduleFile))
if (_fileSystem.File.Exists(playout.ScheduleFile))
{
return await GetExternalJsonPlayoutItem(dbContext, playout, now, ffprobePath, cancellationToken);
}
@ -86,7 +87,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider @@ -86,7 +87,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
CancellationToken cancellationToken)
{
Option<ExternalJsonChannel> maybeChannel = JsonConvert.DeserializeObject<ExternalJsonChannel>(
await File.ReadAllTextAsync(playout.ScheduleFile, cancellationToken));
await _fileSystem.File.ReadAllTextAsync(playout.ScheduleFile, cancellationToken));
// must deserialize channel from json
foreach (ExternalJsonChannel channel in maybeChannel)
@ -139,7 +140,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider @@ -139,7 +140,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
program.File,
cancellationToken);
if (_localFileSystem.FileExists(localPath))
if (_fileSystem.File.Exists(localPath))
{
return await StreamLocally(startTime, program, ffprobePath, localPath);
}

10
ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using System.IO.Abstractions;
using System.Text;
using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Graphics;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Metadata;
@ -18,7 +18,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics; @@ -18,7 +18,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public partial class GraphicsElementLoader(
TemplateFunctions templateFunctions,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ITemplateDataRepository templateDataRepository,
ILogger<GraphicsElementLoader> logger)
: IGraphicsElementLoader
@ -147,7 +147,7 @@ public partial class GraphicsElementLoader( @@ -147,7 +147,7 @@ public partial class GraphicsElementLoader(
{
try
{
string yaml = await localFileSystem.ReadAllText(fileName);
string yaml = await fileSystem.File.ReadAllTextAsync(fileName, cancellationToken);
var template = Template.Parse(yaml);
var builder = new StringBuilder();
@ -187,7 +187,7 @@ public partial class GraphicsElementLoader( @@ -187,7 +187,7 @@ public partial class GraphicsElementLoader(
foreach (var reference in elementsWithEpg)
{
foreach (string line in await localFileSystem.ReadAllLines(reference.GraphicsElement.Path))
foreach (string line in await fileSystem.File.ReadAllLinesAsync(reference.GraphicsElement.Path))
{
Match match = EpgEntriesRegex().Match(line);
if (!match.Success || !int.TryParse(match.Groups[1].Value, out int value))
@ -257,7 +257,7 @@ public partial class GraphicsElementLoader( @@ -257,7 +257,7 @@ public partial class GraphicsElementLoader(
private async Task<Option<string>> GetTemplatedYaml(string fileName, Dictionary<string, object> variables)
{
string yaml = await localFileSystem.ReadAllText(fileName);
string yaml = await fileSystem.File.ReadAllTextAsync(fileName);
try
{
var scriptObject = new ScriptObject();

18
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -34,6 +34,7 @@ using NSubstitute; @@ -34,6 +34,7 @@ using NSubstitute;
using NUnit.Framework;
using Serilog;
using Shouldly;
using Testably.Abstractions.Testing;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
namespace ErsatzTV.Scanner.Tests.Core.FFmpeg;
@ -235,8 +236,10 @@ public class TranscodingTests @@ -235,8 +236,10 @@ public class TranscodingTests
StreamingMode streamingMode)
{
var localFileSystem = new LocalFileSystem(
new MockFileSystem(),
Substitute.For<IClient>(),
LoggerFactory.CreateLogger<LocalFileSystem>());
var fileSystem = new MockFileSystem();
var tempFilePool = new TempFilePool();
ImageCache mockImageCache = Substitute.For<ImageCache>(localFileSystem, tempFilePool);
@ -353,7 +356,8 @@ public class TranscodingTests @@ -353,7 +356,8 @@ public class TranscodingTests
var localStatisticsProvider = new LocalStatisticsProvider(
metadataRepository,
new LocalFileSystem(Substitute.For<IClient>(), LoggerFactory.CreateLogger<LocalFileSystem>()),
fileSystem,
localFileSystem,
Substitute.For<IClient>(),
Substitute.For<IHardwareCapabilitiesFactory>(),
LoggerFactory.CreateLogger<LocalStatisticsProvider>());
@ -460,6 +464,12 @@ public class TranscodingTests @@ -460,6 +464,12 @@ public class TranscodingTests
// do nothing
}
var localFileSystem = new LocalFileSystem(
new MockFileSystem(),
Substitute.For<IClient>(),
LoggerFactory.CreateLogger<LocalFileSystem>());
var fileSystem = new MockFileSystem();
string file = fileToTest;
if (string.IsNullOrWhiteSpace(file))
{
@ -503,7 +513,8 @@ public class TranscodingTests @@ -503,7 +513,8 @@ public class TranscodingTests
var localStatisticsProvider = new LocalStatisticsProvider(
metadataRepository,
new LocalFileSystem(Substitute.For<IClient>(), LoggerFactory.CreateLogger<LocalFileSystem>()),
fileSystem,
localFileSystem,
Substitute.For<IClient>(),
Substitute.For<IHardwareCapabilitiesFactory>(),
LoggerFactory.CreateLogger<LocalStatisticsProvider>());
@ -669,9 +680,6 @@ public class TranscodingTests @@ -669,9 +680,6 @@ public class TranscodingTests
SubtitleMode = subtitleMode
};
var localFileSystem = new LocalFileSystem(
Substitute.For<IClient>(),
LoggerFactory.CreateLogger<LocalFileSystem>());
var tempFilePool = new TempFilePool();
ImageCache mockImageCache = Substitute.For<ImageCache>(localFileSystem, tempFilePool);

88
ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs

@ -1,88 +0,0 @@ @@ -1,88 +0,0 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
namespace ErsatzTV.Scanner.Tests.Core.Fakes;
public class FakeLocalFileSystem : ILocalFileSystem
{
private readonly List<FakeFileEntry> _files;
private readonly List<FakeFolderEntry> _folders;
public FakeLocalFileSystem(List<FakeFileEntry> files) : this(files, new List<FakeFolderEntry>())
{
}
public FakeLocalFileSystem(List<FakeFileEntry> files, List<FakeFolderEntry> folders)
{
_files = files;
var allFolders = new List<string>(folders.Map(f => f.Path));
foreach (FakeFileEntry file in _files)
{
List<DirectoryInfo> moreFolders =
Split(new DirectoryInfo(Path.GetDirectoryName(file.Path) ?? string.Empty));
allFolders.AddRange(moreFolders.Map(i => i.FullName));
}
_folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList();
}
public Unit EnsureFolderExists(string folder) => Unit.Default;
public DateTime GetLastWriteTime(string path) =>
Optional(_files.SingleOrDefault(f => f.Path == path))
.Map(f => f.LastWriteTime)
.IfNone(SystemTime.MinValueUtc);
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
_folders.Any(f => f.Path == libraryPath.Path);
public IEnumerable<string> ListSubdirectories(string folder) =>
_folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder);
public IEnumerable<string> ListFiles(string folder) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
// TODO: this isn't accurate, need to use search pattern
public IEnumerable<string> ListFiles(string folder, string searchPattern) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
public IEnumerable<string> ListFiles(string folder, params string[] searchPatterns) =>
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
public bool FileExists(string path) => _files.Any(f => f.Path == path);
public bool FolderExists(string folder) => false;
public Task<Either<BaseError, Unit>> CopyFile(string source, string destination) =>
Task.FromResult(Right<BaseError, Unit>(Unit.Default));
public Unit EmptyFolder(string folder) => Unit.Default;
public Task<string> ReadAllText(string path) => throw new NotImplementedException();
public Task<string[]> ReadAllLines(string path) => throw new NotImplementedException();
public Task<byte[]> GetHash(string path) => throw new NotImplementedException();
public string GetCustomOrDefaultFile(string folder, string file)
{
string path = Path.Combine(folder, file);
return FileExists(path) ? path : Path.Combine(folder, $"_{file}");
}
private static List<DirectoryInfo> Split(DirectoryInfo path)
{
var result = new List<DirectoryInfo>();
if (path == null || string.IsNullOrWhiteSpace(path.FullName))
{
return result;
}
if (path.Parent != null)
{
result.AddRange(Split(path.Parent));
}
result.Add(path);
return result;
}
}

22
ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs

@ -1,12 +1,16 @@ @@ -1,12 +1,16 @@
using System.Globalization;
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using ErsatzTV.Scanner.Tests.Core.Fakes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
using Testably.Abstractions.Testing.Initializer;
namespace ErsatzTV.Scanner.Tests.Core.Metadata;
@ -46,10 +50,17 @@ public class LocalSubtitlesProviderTests @@ -46,10 +50,17 @@ public class LocalSubtitlesProviderTests
new(@"/Movies/Avatar (2009)/Avatar (2009).DE.SDH.FORCED.SRT")
};
var fileSystem = new MockFileSystem();
IFileSystemInitializer<MockFileSystem> init = fileSystem.Initialize();
foreach (var file in fakeFiles)
{
init.WithFile(file.Path);
}
var provider = new LocalSubtitlesProvider(
Substitute.For<IMediaItemRepository>(),
Substitute.For<IMetadataRepository>(),
new FakeLocalFileSystem(fakeFiles),
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
Substitute.For<ILogger<LocalSubtitlesProvider>>());
List<Subtitle> result = provider.LocateExternalSubtitles(
@ -91,10 +102,17 @@ public class LocalSubtitlesProviderTests @@ -91,10 +102,17 @@ public class LocalSubtitlesProviderTests
new(@"/Movies/Avatar (2009)/Avatar (2009).DE.SDH.FORCED.SRT")
};
var fileSystem = new MockFileSystem();
IFileSystemInitializer<MockFileSystem> init = fileSystem.Initialize();
foreach (var file in fakeFiles)
{
init.WithFile(file.Path);
}
var provider = new LocalSubtitlesProvider(
Substitute.For<IMediaItemRepository>(),
Substitute.For<IMetadataRepository>(),
new FakeLocalFileSystem(fakeFiles),
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
Substitute.For<ILogger<LocalSubtitlesProvider>>());
List<Subtitle> result = provider.LocateExternalSubtitles(

38
ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs

@ -17,16 +17,14 @@ using NSubstitute; @@ -17,16 +17,14 @@ using NSubstitute;
using NUnit.Framework;
using Serilog;
using Shouldly;
using Testably.Abstractions.Testing;
using Testably.Abstractions.Testing.Initializer;
namespace ErsatzTV.Scanner.Tests.Core.Metadata;
[TestFixture]
public class MovieFolderScannerTests
{
private static readonly string BadFakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? @"C:\Movies-That-Dont-Exist"
: @"/movies-that-dont-exist";
private static readonly string FakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? @"C:\Movies"
: "/movies";
@ -718,10 +716,19 @@ public class MovieFolderScannerTests @@ -718,10 +716,19 @@ public class MovieFolderScannerTests
await _mediaItemRepository.Received(1).FlagFileNotFound(libraryPath, oldMoviePath);
}
private MovieFolderScanner GetService(params FakeFileEntry[] files) =>
new(
private MovieFolderScanner GetService(params FakeFileEntry[] files)
{
var fileSystem = new MockFileSystem();
IFileSystemInitializer<MockFileSystem> init = fileSystem.Initialize();
foreach (var file in files)
{
init.WithFile(file.Path).Which(f => f.File.LastWriteTime = file.LastWriteTime);
}
return new MovieFolderScanner(
_scannerProxy,
new FakeLocalFileSystem([..files]),
fileSystem,
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
_movieRepository,
_localStatisticsProvider,
Substitute.For<ILocalSubtitlesProvider>(),
@ -735,11 +742,21 @@ public class MovieFolderScannerTests @@ -735,11 +742,21 @@ public class MovieFolderScannerTests
Substitute.For<ITempFilePool>(),
Substitute.For<IClient>(),
Logger);
}
private MovieFolderScanner GetService(params FakeFolderEntry[] folders) =>
new(
private MovieFolderScanner GetService(params FakeFolderEntry[] folders)
{
var fileSystem = new MockFileSystem();
IFileSystemInitializer<MockFileSystem> init = fileSystem.Initialize();
foreach (var folder in folders)
{
init.WithSubdirectory(folder.Path);
}
return new MovieFolderScanner(
_scannerProxy,
new FakeLocalFileSystem([], [..folders]),
fileSystem,
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
_movieRepository,
_localStatisticsProvider,
Substitute.For<ILocalSubtitlesProvider>(),
@ -753,5 +770,6 @@ public class MovieFolderScannerTests @@ -753,5 +770,6 @@ public class MovieFolderScannerTests
Substitute.For<ITempFilePool>(),
Substitute.For<IClient>(),
Logger);
}
}
}

1
ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj

@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

8
ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces;
@ -29,13 +29,13 @@ public class EmbyMovieLibraryScanner : @@ -29,13 +29,13 @@ public class EmbyMovieLibraryScanner :
IMediaSourceRepository mediaSourceRepository,
IEmbyMovieRepository embyMovieRepository,
IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
ILogger<EmbyMovieLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
fileSystem,
localChaptersProvider,
metadataRepository,
logger)

8
ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces;
@ -30,13 +30,13 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< @@ -30,13 +30,13 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
IMediaSourceRepository mediaSourceRepository,
IEmbyTelevisionRepository televisionRepository,
IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
ILogger<EmbyTelevisionLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
fileSystem,
localChaptersProvider,
metadataRepository,
logger)

8
ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
@ -29,13 +29,13 @@ public class JellyfinMovieLibraryScanner : @@ -29,13 +29,13 @@ public class JellyfinMovieLibraryScanner :
IJellyfinMovieRepository jellyfinMovieRepository,
IJellyfinPathReplacementService pathReplacementService,
IMediaSourceRepository mediaSourceRepository,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
ILogger<JellyfinMovieLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
fileSystem,
localChaptersProvider,
metadataRepository,
logger)

8
ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
@ -31,13 +31,13 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -31,13 +31,13 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
IMediaSourceRepository mediaSourceRepository,
IJellyfinTelevisionRepository televisionRepository,
IJellyfinPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
ILogger<JellyfinTelevisionLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
fileSystem,
localChaptersProvider,
metadataRepository,
logger)

8
ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -22,6 +23,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -22,6 +23,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
private readonly IImageRepository _imageRepository;
private readonly ILibraryRepository _libraryRepository;
private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<ImageFolderScanner> _logger;
@ -29,6 +31,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -29,6 +31,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
public ImageFolderScanner(
IScannerProxy scannerProxy,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
@ -41,7 +44,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -41,7 +44,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
ITempFilePool tempFilePool,
IClient client,
ILogger<ImageFolderScanner> logger) : base(
localFileSystem,
fileSystem,
localStatisticsProvider,
metadataRepository,
mediaItemRepository,
@ -52,6 +55,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -52,6 +55,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
logger)
{
_scannerProxy = scannerProxy;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider;
_imageRepository = imageRepository;
@ -226,7 +230,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -226,7 +230,7 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
foreach (string path in await _imageRepository.FindImagePaths(libraryPath))
{
if (!_localFileSystem.FileExists(path))
if (!_fileSystem.File.Exists(path))
{
_logger.LogInformation("Flagging missing image at {Path}", path);
List<int> imageIds = await FlagFileNotFound(libraryPath, path);

13
ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using CliWrap;
using ErsatzTV.Core;
@ -59,7 +60,7 @@ public abstract class LocalFolderScanner @@ -59,7 +60,7 @@ public abstract class LocalFolderScanner
private readonly IImageCache _imageCache;
private readonly ILocalFileSystem _localFileSystem;
private readonly IFileSystem _fileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger _logger;
private readonly IMediaItemRepository _mediaItemRepository;
@ -67,7 +68,7 @@ public abstract class LocalFolderScanner @@ -67,7 +68,7 @@ public abstract class LocalFolderScanner
private readonly ITempFilePool _tempFilePool;
protected LocalFolderScanner(
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalStatisticsProvider localStatisticsProvider,
IMetadataRepository metadataRepository,
IMediaItemRepository mediaItemRepository,
@ -77,7 +78,7 @@ public abstract class LocalFolderScanner @@ -77,7 +78,7 @@ public abstract class LocalFolderScanner
IClient client,
ILogger logger)
{
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_localStatisticsProvider = localStatisticsProvider;
_metadataRepository = metadataRepository;
_mediaItemRepository = mediaItemRepository;
@ -100,7 +101,7 @@ public abstract class LocalFolderScanner @@ -100,7 +101,7 @@ public abstract class LocalFolderScanner
string path = version.MediaFiles.Head().Path;
if (version.DateUpdated != _localFileSystem.GetLastWriteTime(path) || version.Streams.Count == 0)
if (version.DateUpdated != _fileSystem.File.GetLastWriteTime(path) || version.Streams.Count == 0)
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path);
Either<BaseError, bool> refreshResult =
@ -141,7 +142,7 @@ public abstract class LocalFolderScanner @@ -141,7 +142,7 @@ public abstract class LocalFolderScanner
Option<int> attachedPicIndex,
CancellationToken cancellationToken)
{
DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile);
DateTime lastWriteTime = _fileSystem.File.GetLastWriteTime(artworkFile);
metadata.Artwork ??= new List<Artwork>();
@ -311,5 +312,5 @@ public abstract class LocalFolderScanner @@ -311,5 +312,5 @@ public abstract class LocalFolderScanner
protected bool ShouldIncludeFolder(string folder) =>
!string.IsNullOrWhiteSpace(folder) &&
!Path.GetFileName(folder).StartsWith('.') &&
!_localFileSystem.FileExists(Path.Combine(folder, ".etvignore"));
!_fileSystem.File.Exists(Path.Combine(folder, ".etvignore"));
}

10
ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using Bugsnag;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
@ -22,6 +23,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -22,6 +23,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
private readonly IClient _client;
private readonly IEpisodeNfoReader _episodeNfoReader;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly IFileSystem _fileSystem;
private readonly IImageRepository _imageRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
@ -49,6 +51,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -49,6 +51,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
IImageRepository imageRepository,
IRemoteStreamRepository remoteStreamRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
IMovieNfoReader movieNfoReader,
IEpisodeNfoReader episodeNfoReader,
@ -70,6 +73,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -70,6 +73,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
_imageRepository = imageRepository;
_remoteStreamRepository = remoteStreamRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_movieNfoReader = movieNfoReader;
_episodeNfoReader = episodeNfoReader;
@ -86,7 +90,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -86,7 +90,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
string nfoFileName = Path.Combine(showFolder, "tvshow.nfo");
Option<ShowMetadata> maybeMetadata = None;
if (_localFileSystem.FileExists(nfoFileName))
if (_fileSystem.File.Exists(nfoFileName))
{
maybeMetadata = await LoadTelevisionShowMetadata(nfoFileName);
}
@ -106,7 +110,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -106,7 +110,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
string nfoFileName = Path.Combine(artistFolder, "artist.nfo");
Option<ArtistMetadata> maybeMetadata = None;
if (_localFileSystem.FileExists(nfoFileName))
if (_fileSystem.File.Exists(nfoFileName))
{
maybeMetadata = await LoadArtistMetadata(nfoFileName);
}

2
ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs

@ -10,7 +10,7 @@ namespace ErsatzTV.Scanner.Core.Metadata; @@ -10,7 +10,7 @@ namespace ErsatzTV.Scanner.Core.Metadata;
public class LocalSubtitlesProvider : ILocalSubtitlesProvider
{
private readonly List<CultureInfo> _languageCodes = new();
private readonly List<CultureInfo> _languageCodes = [];
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<LocalSubtitlesProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository;

14
ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.MediaServer;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces;
@ -21,19 +21,19 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -21,19 +21,19 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly ILocalFileSystem _localFileSystem;
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
private readonly IMetadataRepository _metadataRepository;
protected MediaServerMovieLibraryScanner(
IScannerProxy scannerProxy,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
ILogger logger)
{
_scannerProxy = scannerProxy;
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_logger = logger;
@ -167,7 +167,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -167,7 +167,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{
await movieRepository.SetEtag(result.Item, MediaServerEtag(incoming));
if (_localFileSystem.FileExists(result.LocalPath))
if (_fileSystem.File.Exists(result.LocalPath))
{
Option<int> flagResult = await movieRepository.FlagNormal(library, result.Item);
if (flagResult.IsSome)
@ -270,7 +270,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -270,7 +270,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
existingEtag == MediaServerEtag(incoming))
{
// skip scanning unavailable/file not found items that are unchanged and still don't exist locally
if (!_localFileSystem.FileExists(localPath) && !ServerSupportsRemoteStreaming)
if (!_fileSystem.File.Exists(localPath) && !ServerSupportsRemoteStreaming)
{
return false;
}
@ -279,7 +279,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -279,7 +279,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{
// item is unchanged, but file does not exist
// don't scan, but mark as unavailable
if (!_localFileSystem.FileExists(localPath))
if (!_fileSystem.File.Exists(localPath))
{
if (ServerSupportsRemoteStreaming)
{

14
ErsatzTV.Scanner/Core/Metadata/MediaServerOtherVideoLibraryScanner.cs

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.MediaServer;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces;
@ -21,19 +21,19 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters, @@ -21,19 +21,19 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
{
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly ILocalFileSystem _localFileSystem;
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
private readonly IMetadataRepository _metadataRepository;
protected MediaServerOtherVideoLibraryScanner(
IScannerProxy scannerProxy,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
ILogger logger)
{
_scannerProxy = scannerProxy;
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_logger = logger;
@ -174,7 +174,7 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters, @@ -174,7 +174,7 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
{
await otherVideoRepository.SetEtag(result.Item, MediaServerEtag(incoming));
if (_localFileSystem.FileExists(result.LocalPath))
if (_fileSystem.File.Exists(result.LocalPath))
{
Option<int> flagResult = await otherVideoRepository.FlagNormal(library, result.Item);
if (flagResult.IsSome)
@ -277,7 +277,7 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters, @@ -277,7 +277,7 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
existingEtag == MediaServerEtag(incoming))
{
// skip scanning unavailable/file not found items that are unchanged and still don't exist locally
if (!_localFileSystem.FileExists(localPath) && !ServerSupportsRemoteStreaming)
if (!_fileSystem.File.Exists(localPath) && !ServerSupportsRemoteStreaming)
{
return false;
}
@ -286,7 +286,7 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters, @@ -286,7 +286,7 @@ public abstract class MediaServerOtherVideoLibraryScanner<TConnectionParameters,
{
// item is unchanged, but file does not exist
// don't scan, but mark as unavailable
if (!_localFileSystem.FileExists(localPath))
if (!_fileSystem.File.Exists(localPath))
{
if (ServerSupportsRemoteStreaming)
{

18
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.MediaServer;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces;
@ -23,19 +23,19 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -23,19 +23,19 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly ILocalFileSystem _localFileSystem;
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
private readonly IMetadataRepository _metadataRepository;
protected MediaServerTelevisionLibraryScanner(
IScannerProxy scannerProxy,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
IMetadataRepository metadataRepository,
ILogger logger)
{
_scannerProxy = scannerProxy;
_localFileSystem = localFileSystem;
_fileSystem = fileSystem;
_localChaptersProvider = localChaptersProvider;
_metadataRepository = metadataRepository;
_logger = logger;
@ -469,7 +469,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -469,7 +469,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
await televisionRepository.SetEtag(result.Item, MediaServerEtag(incoming), cancellationToken);
if (_localFileSystem.FileExists(result.LocalPath))
if (_fileSystem.File.Exists(result.LocalPath))
{
Option<int> flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken);
if (flagResult.IsSome)
@ -541,7 +541,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -541,7 +541,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
existingEtag == MediaServerEtag(incoming))
{
// skip scanning unavailable/file not found items that are unchanged and still don't exist locally
if (!_localFileSystem.FileExists(localPath) && !ServerSupportsRemoteStreaming)
if (!_fileSystem.File.Exists(localPath) && !ServerSupportsRemoteStreaming)
{
return false;
}
@ -550,7 +550,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -550,7 +550,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
// item is unchanged, but file does not exist
// don't scan, but mark as unavailable
if (!_localFileSystem.FileExists(localPath))
if (!_fileSystem.File.Exists(localPath))
{
if (ServerSupportsRemoteStreaming)
{
@ -741,7 +741,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -741,7 +741,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
if (deepScan || result.IsAdded || MediaServerEtag(existing) != MediaServerEtag(incoming) ||
existing.MediaVersions.Head().Streams.Count == 0)
{
// if (maybeMediaVersion.IsNone && _localFileSystem.FileExists(result.LocalPath))
// if (maybeMediaVersion.IsNone && _fileSystem.File.Exists(result.LocalPath))
// {
// _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", result.LocalPath);
// Either<BaseError, bool> refreshResult =

14
ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -23,6 +24,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -23,6 +24,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@ -32,6 +34,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -32,6 +34,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
public MovieFolderScanner(
IScannerProxy scannerProxy,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
IMovieRepository movieRepository,
ILocalStatisticsProvider localStatisticsProvider,
@ -47,7 +50,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -47,7 +50,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
IClient client,
ILogger<MovieFolderScanner> logger)
: base(
localFileSystem,
fileSystem,
localStatisticsProvider,
metadataRepository,
mediaItemRepository,
@ -58,6 +61,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -58,6 +61,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
logger)
{
_scannerProxy = scannerProxy;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_movieRepository = movieRepository;
_localSubtitlesProvider = localSubtitlesProvider;
@ -209,7 +213,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -209,7 +213,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
foreach (string path in await _movieRepository.FindMoviePaths(libraryPath))
{
if (!_localFileSystem.FileExists(path))
if (!_fileSystem.File.Exists(path))
{
_logger.LogInformation("Flagging missing movie at {Path}", path);
List<int> ids = await FlagFileNotFound(libraryPath, path);
@ -362,7 +366,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -362,7 +366,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
string movieAsNfo = Path.ChangeExtension(path, "nfo");
string movieNfo = Path.Combine(Path.GetDirectoryName(path) ?? string.Empty, "movie.nfo");
return Seq.create(movieAsNfo, movieNfo)
.Filter(s => _localFileSystem.FileExists(s))
.Filter(s => _fileSystem.File.Exists(s))
.HeadOrNone();
}
@ -380,12 +384,12 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -380,12 +384,12 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect(ext =>
new[] { $"{segment}.{ext}", Path.GetFileNameWithoutExtension(path) + $"-{segment}.{ext}" })
.Map(f => Path.Combine(folder, f));
Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone();
Option<string> result = possibleMoviePosters.Filter(p => _fileSystem.File.Exists(p)).HeadOrNone();
if (result.IsNone && artworkKind == ArtworkKind.Poster)
{
IEnumerable<string> possibleFolderPosters = ImageFileExtensions.Collect(ext => new[] { $"folder.{ext}" })
.Map(f => Path.Combine(folder, f));
result = possibleFolderPosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone();
result = possibleFolderPosters.Filter(p => _fileSystem.File.Exists(p)).HeadOrNone();
}
return result;

16
ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -23,6 +24,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -23,6 +24,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@ -32,6 +34,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -32,6 +34,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
public MusicVideoFolderScanner(
IScannerProxy scannerProxy,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
@ -47,7 +50,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -47,7 +50,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
ITempFilePool tempFilePool,
IClient client,
ILogger<MusicVideoFolderScanner> logger) : base(
localFileSystem,
fileSystem,
localStatisticsProvider,
metadataRepository,
mediaItemRepository,
@ -58,6 +61,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -58,6 +61,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
logger)
{
_scannerProxy = scannerProxy;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider;
_localSubtitlesProvider = localSubtitlesProvider;
@ -173,7 +177,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -173,7 +177,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
foreach (string path in await _musicVideoRepository.FindMusicVideoPaths(libraryPath))
{
if (!_localFileSystem.FileExists(path))
if (!_fileSystem.File.Exists(path))
{
_logger.LogInformation("Flagging missing music video at {Path}", path);
List<int> musicVideoIds = await FlagFileNotFound(libraryPath, path);
@ -459,7 +463,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -459,7 +463,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
}
private Option<string> LocateNfoFileForArtist(string artistFolder) =>
Optional(Path.Combine(artistFolder, "artist.nfo")).Filter(s => _localFileSystem.FileExists(s));
Optional(Path.Combine(artistFolder, "artist.nfo")).Filter(s => _fileSystem.File.Exists(s));
private Option<string> LocateArtworkForArtist(string artistFolder, ArtworkKind artworkKind)
{
@ -473,7 +477,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -473,7 +477,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
return ImageFileExtensions
.Map(ext => $"{segment}.{ext}")
.Map(f => Path.Combine(artistFolder, f))
.Filter(s => _localFileSystem.FileExists(s))
.Filter(s => _fileSystem.File.Exists(s))
.HeadOrNone();
}
@ -481,7 +485,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -481,7 +485,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
return Optional(Path.ChangeExtension(path, "nfo"))
.Filter(s => _localFileSystem.FileExists(s))
.Filter(s => _fileSystem.File.Exists(s))
.HeadOrNone();
}
@ -554,7 +558,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -554,7 +558,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
return ImageFileExtensions
.SelectMany(ext => new[] { Path.ChangeExtension(path, ext), Path.ChangeExtension(thumbPath, ext) })
.Filter(f => _localFileSystem.FileExists(f))
.Filter(f => _fileSystem.File.Exists(f))
.HeadOrNone();
}
}

12
ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -22,6 +23,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -22,6 +23,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@ -31,6 +33,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -31,6 +33,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
public OtherVideoFolderScanner(
IScannerProxy scannerProxy,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
@ -45,7 +48,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -45,7 +48,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
ITempFilePool tempFilePool,
IClient client,
ILogger<OtherVideoFolderScanner> logger) : base(
localFileSystem,
fileSystem,
localStatisticsProvider,
metadataRepository,
mediaItemRepository,
@ -56,6 +59,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -56,6 +59,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
logger)
{
_scannerProxy = scannerProxy;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider;
_localSubtitlesProvider = localSubtitlesProvider;
@ -220,7 +224,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -220,7 +224,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
foreach (string path in await _otherVideoRepository.FindOtherVideoPaths(libraryPath))
{
if (!_localFileSystem.FileExists(path))
if (!_fileSystem.File.Exists(path))
{
_logger.LogInformation("Flagging missing other video at {Path}", path);
List<int> otherVideoIds = await FlagFileNotFound(libraryPath, path);
@ -274,7 +278,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -274,7 +278,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path;
Option<string> maybeNfoFile = new List<string> { Path.ChangeExtension(path, "nfo") }
.Filter(_localFileSystem.FileExists)
.Filter(_fileSystem.File.Exists)
.HeadOrNone();
if (maybeNfoFile.IsNone)
@ -376,7 +380,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -376,7 +380,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path;
return ImageFileExtensions
.Map(ext => Path.ChangeExtension(path, ext))
.Filter(f => _localFileSystem.FileExists(f))
.Filter(f => _fileSystem.File.Exists(f))
.HeadOrNone();
}
}

8
ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -24,6 +25,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -24,6 +25,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
private readonly IClient _client;
private readonly ILibraryRepository _libraryRepository;
private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<RemoteStreamFolderScanner> _logger;
@ -32,6 +34,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -32,6 +34,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
public RemoteStreamFolderScanner(
IScannerProxy scannerProxy,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
@ -44,7 +47,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -44,7 +47,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
ITempFilePool tempFilePool,
IClient client,
ILogger<RemoteStreamFolderScanner> logger) : base(
localFileSystem,
fileSystem,
localStatisticsProvider,
metadataRepository,
mediaItemRepository,
@ -55,6 +58,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -55,6 +58,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
logger)
{
_scannerProxy = scannerProxy;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider;
_remoteStreamRepository = remoteStreamRepository;
@ -213,7 +217,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder @@ -213,7 +217,7 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
foreach (string path in await _remoteStreamRepository.FindRemoteStreamPaths(libraryPath, cancellationToken))
{
if (!_localFileSystem.FileExists(path))
if (!_fileSystem.File.Exists(path))
{
_logger.LogInformation("Flagging missing remote stream at {Path}", path);
List<int> remoteStreamIds = await FlagFileNotFound(libraryPath, path);

10
ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -21,6 +22,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -21,6 +22,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
private readonly IClient _client;
private readonly ILibraryRepository _libraryRepository;
private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILogger<SongFolderScanner> _logger;
@ -29,6 +31,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -29,6 +31,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
public SongFolderScanner(
IScannerProxy scannerProxy,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider,
@ -41,7 +44,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -41,7 +44,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
ITempFilePool tempFilePool,
IClient client,
ILogger<SongFolderScanner> logger) : base(
localFileSystem,
fileSystem,
localStatisticsProvider,
metadataRepository,
mediaItemRepository,
@ -52,6 +55,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -52,6 +55,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
logger)
{
_scannerProxy = scannerProxy;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_localMetadataProvider = localMetadataProvider;
_songRepository = songRepository;
@ -201,7 +205,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -201,7 +205,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
foreach (string path in await _songRepository.FindSongPaths(libraryPath))
{
if (!_localFileSystem.FileExists(path))
if (!_fileSystem.File.Exists(path))
{
_logger.LogInformation("Flagging missing song at {Path}", path);
List<int> songIds = await FlagFileNotFound(libraryPath, path);
@ -336,7 +340,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -336,7 +340,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
string coverPath = Path.Combine(di.FullName, "cover.jpg");
return ImageFileExtensions
.Map(ext => Path.ChangeExtension(coverPath, ext))
.Filter(f => _localFileSystem.FileExists(f))
.Filter(f => _fileSystem.File.Exists(f))
.HeadOrNone();
}).Flatten();
}

18
ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -23,6 +24,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -23,6 +24,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private readonly ILibraryRepository _libraryRepository;
private readonly ILocalChaptersProvider _localChaptersProvider;
private readonly IScannerProxy _scannerProxy;
private readonly IFileSystem _fileSystem;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
@ -33,6 +35,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -33,6 +35,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
public TelevisionFolderScanner(
IScannerProxy scannerProxy,
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ITelevisionRepository televisionRepository,
ILocalStatisticsProvider localStatisticsProvider,
@ -48,7 +51,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -48,7 +51,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
IClient client,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger<TelevisionFolderScanner> logger) : base(
localFileSystem,
fileSystem,
localStatisticsProvider,
metadataRepository,
mediaItemRepository,
@ -59,6 +62,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -59,6 +62,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
logger)
{
_scannerProxy = scannerProxy;
_fileSystem = fileSystem;
_localFileSystem = localFileSystem;
_televisionRepository = televisionRepository;
_localMetadataProvider = localMetadataProvider;
@ -169,7 +173,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -169,7 +173,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath))
{
if (!_localFileSystem.FileExists(path))
if (!_fileSystem.File.Exists(path))
{
_logger.LogInformation("Flagging missing episode at {Path}", path);
@ -584,12 +588,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -584,12 +588,12 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
}
private Option<string> LocateNfoFileForShow(string showFolder) =>
Optional(Path.Combine(showFolder, "tvshow.nfo")).Filter(s => _localFileSystem.FileExists(s));
Optional(Path.Combine(showFolder, "tvshow.nfo")).Filter(s => _fileSystem.File.Exists(s));
private Option<string> LocateNfoFile(Episode episode)
{
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
return Optional(Path.ChangeExtension(path, "nfo")).Filter(s => _localFileSystem.FileExists(s));
return Optional(Path.ChangeExtension(path, "nfo")).Filter(s => _fileSystem.File.Exists(s));
}
private Option<string> LocateArtworkForShow(string showFolder, ArtworkKind artworkKind)
@ -606,7 +610,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -606,7 +610,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
.Map(ext => segments.Map(segment => $"{segment}.{ext}"))
.Flatten()
.Map(f => Path.Combine(showFolder, f))
.Filter(s => _localFileSystem.FileExists(s))
.Filter(s => _fileSystem.File.Exists(s))
.HeadOrNone();
}
@ -615,7 +619,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -615,7 +619,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
string folder = Path.GetDirectoryName(seasonFolder) ?? string.Empty;
return ImageFileExtensions
.Map(ext => Path.Combine(folder, $"season{season.SeasonNumber:00}-poster.{ext}"))
.Filter(s => _localFileSystem.FileExists(s))
.Filter(s => _fileSystem.File.Exists(s))
.HeadOrNone();
}
@ -626,7 +630,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -626,7 +630,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
return ImageFileExtensions
.Map(ext => Path.GetFileNameWithoutExtension(path) + $"-thumb.{ext}")
.Map(f => Path.Combine(folder, f))
.Filter(f => _localFileSystem.FileExists(f))
.Filter(f => _fileSystem.File.Exists(f))
.HeadOrNone();
}
}

8
ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
@ -33,12 +33,12 @@ public class PlexMovieLibraryScanner : @@ -33,12 +33,12 @@ public class PlexMovieLibraryScanner :
IMediaSourceRepository mediaSourceRepository,
IPlexMovieRepository plexMovieRepository,
IPlexPathReplacementService plexPathReplacementService,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
ILogger<PlexMovieLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
fileSystem,
localChaptersProvider,
metadataRepository,
logger)

6
ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
@ -33,12 +33,12 @@ public class PlexOtherVideoLibraryScanner : @@ -33,12 +33,12 @@ public class PlexOtherVideoLibraryScanner :
IMediaSourceRepository mediaSourceRepository,
IPlexOtherVideoRepository plexOtherVideoRepository,
IPlexPathReplacementService plexPathReplacementService,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
ILogger<PlexOtherVideoLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
fileSystem,
localChaptersProvider,
metadataRepository,
logger)

8
ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
using System.Text.RegularExpressions;
using System.IO.Abstractions;
using System.Text.RegularExpressions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
@ -37,12 +37,12 @@ public partial class PlexTelevisionLibraryScanner : @@ -37,12 +37,12 @@ public partial class PlexTelevisionLibraryScanner :
IMediaSourceRepository mediaSourceRepository,
IPlexPathReplacementService plexPathReplacementService,
IPlexTelevisionRepository plexTelevisionRepository,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
ILocalChaptersProvider localChaptersProvider,
ILogger<PlexTelevisionLibraryScanner> logger)
: base(
scannerProxy,
localFileSystem,
fileSystem,
localChaptersProvider,
metadataRepository,
logger)

4
ErsatzTV.Scanner/Program.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using System.IO.Abstractions;
using Bugsnag;
using Bugsnag.Payload;
using Dapper;
@ -47,6 +48,7 @@ using Microsoft.IO; @@ -47,6 +48,7 @@ using Microsoft.IO;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
using Testably.Abstractions;
using Exception = System.Exception;
using IConfiguration = Bugsnag.IConfiguration;
@ -252,6 +254,8 @@ public class Program @@ -252,6 +254,8 @@ public class Program
services.AddSingleton<IScannerProxy, ScannerProxy>();
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
services.AddSingleton<IFileSystem, RealFileSystem>();
services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<Worker>());
services.AddMemoryCache();

6
ErsatzTV/Controllers/Api/TroubleshootController.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.MediaItems;
@ -6,7 +7,6 @@ using ErsatzTV.Application.Troubleshooting.Queries; @@ -6,7 +7,6 @@ using ErsatzTV.Application.Troubleshooting.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Troubleshooting;
using MediatR;
@ -18,7 +18,7 @@ namespace ErsatzTV.Controllers.Api; @@ -18,7 +18,7 @@ namespace ErsatzTV.Controllers.Api;
[ApiController]
public class TroubleshootController(
ChannelWriter<IFFmpegWorkerRequest> channelWriter,
ILocalFileSystem localFileSystem,
IFileSystem fileSystem,
IConfigElementRepository configElementRepository,
ITroubleshootingNotifier notifier,
IMediator mediator) : ControllerBase
@ -104,7 +104,7 @@ public class TroubleshootController( @@ -104,7 +104,7 @@ public class TroubleshootController(
cancellationToken);
string playlistFile = Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "live.m3u8");
while (!localFileSystem.FileExists(playlistFile))
while (!fileSystem.File.Exists(playlistFile))
{
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
if (cancellationToken.IsCancellationRequested || notifier.IsFailed(sessionId))

6
ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
@page "/system/troubleshooting/playback"
@using System.Globalization
@using System.IO.Abstractions
@using ErsatzTV.Application.Channels
@using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.Graphics
@ -7,7 +8,6 @@ @@ -7,7 +8,6 @@
@using ErsatzTV.Application.Troubleshooting
@using ErsatzTV.Application.Troubleshooting.Queries
@using ErsatzTV.Application.Watermarks
@using ErsatzTV.Core.Interfaces.Metadata
@using ErsatzTV.Core.Notifications
@using MediatR.Courier
@using Microsoft.AspNetCore.WebUtilities
@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
@inject IEntityLocker Locker
@inject ICourier Courier;
@inject ISnackbar Snackbar;
@inject ILocalFileSystem LocalFileSystem;
@inject IFileSystem FileSystem;
<MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
@ -422,7 +422,7 @@ @@ -422,7 +422,7 @@
}
string logFileName = Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "logs.txt");
if (LocalFileSystem.FileExists(logFileName))
if (FileSystem.File.Exists(logFileName))
{
string text = await File.ReadAllTextAsync(logFileName);
await InvokeAsync(async () => { await _logsField.SetText(text); });

46
ErsatzTV/Services/RunOnce/CacheCleanerService.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
@ -6,39 +7,30 @@ using Microsoft.EntityFrameworkCore; @@ -6,39 +7,30 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Services.RunOnce;
public class CacheCleanerService : BackgroundService
public class CacheCleanerService(
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<CacheCleanerService> logger)
: BackgroundService
{
private readonly ILogger<CacheCleanerService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
public CacheCleanerService(
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<CacheCleanerService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
await _systemStartup.WaitForDatabase(stoppingToken);
await systemStartup.WaitForDatabase(stoppingToken);
if (stoppingToken.IsCancellationRequested)
{
return;
}
using IServiceScope scope = _serviceScopeFactory.CreateScope();
using IServiceScope scope = serviceScopeFactory.CreateScope();
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
IFileSystem fileSystem = scope.ServiceProvider.GetRequiredService<IFileSystem>();
if (localFileSystem.FolderExists(FileSystemLayout.LegacyImageCacheFolder))
if (fileSystem.Directory.Exists(FileSystemLayout.LegacyImageCacheFolder))
{
_logger.LogInformation("Migrating channel logos from legacy image cache folder");
logger.LogInformation("Migrating channel logos from legacy image cache folder");
List<string> logos = await dbContext.Channels
.AsNoTracking()
@ -50,7 +42,7 @@ public class CacheCleanerService : BackgroundService @@ -50,7 +42,7 @@ public class CacheCleanerService : BackgroundService
foreach (string logo in logos)
{
string legacyPath = Path.Combine(FileSystemLayout.LegacyImageCacheFolder, logo);
if (localFileSystem.FileExists(legacyPath))
if (fileSystem.File.Exists(legacyPath))
{
string subfolder = logo[..2];
string newPath = Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder, logo);
@ -58,20 +50,20 @@ public class CacheCleanerService : BackgroundService @@ -58,20 +50,20 @@ public class CacheCleanerService : BackgroundService
}
}
_logger.LogInformation("Deleting legacy image cache folder");
logger.LogInformation("Deleting legacy image cache folder");
Directory.Delete(FileSystemLayout.LegacyImageCacheFolder, true);
}
if (localFileSystem.FolderExists(FileSystemLayout.TranscodeFolder))
if (fileSystem.Directory.Exists(FileSystemLayout.TranscodeFolder))
{
_logger.LogInformation("Emptying transcode cache folder");
logger.LogInformation("Emptying transcode cache folder");
localFileSystem.EmptyFolder(FileSystemLayout.TranscodeFolder);
_logger.LogInformation("Done emptying transcode cache folder");
logger.LogInformation("Done emptying transcode cache folder");
}
if (localFileSystem.FolderExists(FileSystemLayout.ChannelGuideCacheFolder))
if (fileSystem.Directory.Exists(FileSystemLayout.ChannelGuideCacheFolder))
{
_logger.LogInformation("Cleaning channel cache");
logger.LogInformation("Cleaning channel cache");
List<string> channelFiles = await dbContext.Channels
.AsNoTracking()

4
ErsatzTV/Startup.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Abstractions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
@ -99,6 +100,7 @@ using Refit; @@ -99,6 +100,7 @@ using Refit;
using Scalar.AspNetCore;
using Serilog;
using Serilog.Events;
using Testably.Abstractions;
namespace ErsatzTV;
@ -692,6 +694,8 @@ public class Startup @@ -692,6 +694,8 @@ public class Startup
{
services.AddSingleton<IEnvironmentValidator, EnvironmentValidator>();
services.AddSingleton<IFileSystem, RealFileSystem>();
services.AddSingleton<IDatabaseMigrations, DatabaseMigrations>();
services.AddSingleton<IPlexSecretStore, PlexSecretStore>();
services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton?

Loading…
Cancel
Save