diff --git a/CHANGELOG.md b/CHANGELOG.md index b104e8f13..b10e43b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs index b397daf5b..a91ca1706 100644 --- a/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs @@ -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; public class DeleteChannelHandler : IRequestHandler> { private readonly IDbContextFactory _dbContextFactory; - private readonly ILocalFileSystem _localFileSystem; + private readonly IFileSystem _fileSystem; private readonly ISearchTargets _searchTargets; private readonly ChannelWriter _workerChannel; public DeleteChannelHandler( ChannelWriter workerChannel, IDbContextFactory 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 { private readonly IConfigElementRepository _configElementRepository; private readonly IDbContextFactory _dbContextFactory; + private readonly IFileSystem _fileSystem; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; @@ -33,12 +35,14 @@ public class RefreshChannelDataHandler : IRequestHandler public RefreshChannelDataHandler( RecyclableMemoryStreamManager recyclableMemoryStreamManager, IDbContextFactory dbContextFactory, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository, ILogger logger) { _recyclableMemoryStreamManager = recyclableMemoryStreamManager; _dbContextFactory = dbContextFactory; + _fileSystem = fileSystem; _localFileSystem = localFileSystem; _configElementRepository = configElementRepository; _logger = logger; @@ -886,7 +890,7 @@ public class RefreshChannelDataHandler : IRequestHandler "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 "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 "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 "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 "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 { var result = new List(); - if (_localFileSystem.FileExists(path)) + if (_fileSystem.File.Exists(path)) { Option maybeChannel = JsonConvert.DeserializeObject( await File.ReadAllTextAsync(path)); diff --git a/ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs b/ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs index 92c540eca..04cce52db 100644 --- a/ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/RefreshChannelListHandler.cs @@ -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; public class RefreshChannelListHandler : IRequestHandler { private readonly IDbContextFactory _dbContextFactory; + private readonly IFileSystem _fileSystem; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; @@ -26,11 +28,13 @@ public class RefreshChannelListHandler : IRequestHandler public RefreshChannelListHandler( RecyclableMemoryStreamManager recyclableMemoryStreamManager, IDbContextFactory dbContextFactory, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, ILogger logger) { _recyclableMemoryStreamManager = recyclableMemoryStreamManager; _dbContextFactory = dbContextFactory; + _fileSystem = fileSystem; _localFileSystem = localFileSystem; _logger = logger; } @@ -44,13 +48,13 @@ public class RefreshChannelListHandler : IRequestHandler 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", diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs index d088c32d2..ed654a1c7 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs @@ -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 _dbContextFactory; private readonly ILocalFileSystem _localFileSystem; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + private readonly IFileSystem _fileSystem; public GetChannelGuideHandler( IDbContextFactory dbContextFactory, RecyclableMemoryStreamManager recyclableMemoryStreamManager, + IFileSystem fileSystem, ILocalFileSystem localFileSystem) { _dbContextFactory = dbContextFactory; _recyclableMemoryStreamManager = recyclableMemoryStreamManager; + _fileSystem = fileSystem; _localFileSystem = localFileSystem; } @@ -39,7 +43,7 @@ public partial class GetChannelGuideHandler : IRequestHandler workerChannel) : IRequestHandler> { @@ -35,7 +35,7 @@ public class UpdateFFmpegSettingsHandler( private async Task> ValidateToolPath(string path, string name) { - if (!localFileSystem.FileExists(path)) + if (!fileSystem.File.Exists(path)) { return BaseError.New($"{name} path does not exist"); } diff --git a/ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs b/ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs index b30ebd18d..ed5a8f4d9 100644 --- a/ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs +++ b/ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs @@ -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; public class RefreshGraphicsElementsHandler( IDbContextFactory dbContextFactory, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, IGraphicsElementLoader graphicsElementLoader, ILogger logger) @@ -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) diff --git a/ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs index 763d473e1..82f944bac 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs @@ -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; public class CreateExternalJsonPlayoutHandler : IRequestHandler> { + private readonly IFileSystem _fileSystem; private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; - private readonly ILocalFileSystem _localFileSystem; public CreateExternalJsonPlayoutHandler( - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ChannelWriter channel, IDbContextFactory dbContextFactory) { - _localFileSystem = localFileSystem; + _fileSystem = fileSystem; _channel = channel; _dbContextFactory = dbContextFactory; } @@ -76,7 +76,7 @@ public class CreateExternalJsonPlayoutHandler private Validation ValidateExternalJsonFile(CreateExternalJsonPlayout request) { - if (!_localFileSystem.FileExists(request.ScheduleFile)) + if (!_fileSystem.File.Exists(request.ScheduleFile)) { return BaseError.New("External Json File does not exist!"); } diff --git a/ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs index 9e3bf759e..88dc41e20 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs @@ -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; namespace ErsatzTV.Application.Playouts; -public class CreateScriptedPlayoutHandler +public class CreateScriptedPlayoutHandler( + IFileSystem fileSystem, + ChannelWriter channel, + IDbContextFactory dbContextFactory) : IRequestHandler> { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly ILocalFileSystem _localFileSystem; - - public CreateScriptedPlayoutHandler( - ILocalFileSystem localFileSystem, - ChannelWriter channel, - IDbContextFactory dbContextFactory) - { - _localFileSystem = localFileSystem; - _channel = channel; - _dbContextFactory = dbContextFactory; - } - public async Task> Handle( CreateScriptedPlayout request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request, cancellationToken); return await validation.Apply(playout => PersistPlayout(dbContext, playout, cancellationToken)); } @@ -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 { 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!"); } diff --git a/ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs index 48e4ef31c..267a22098 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs @@ -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; namespace ErsatzTV.Application.Playouts; -public class CreateSequentialPlayoutHandler +public class CreateSequentialPlayoutHandler( + IFileSystem fileSystem, + ChannelWriter channel, + IDbContextFactory dbContextFactory) : IRequestHandler> { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly ILocalFileSystem _localFileSystem; - - public CreateSequentialPlayoutHandler( - ILocalFileSystem localFileSystem, - ChannelWriter channel, - IDbContextFactory dbContextFactory) - { - _localFileSystem = localFileSystem; - _channel = channel; - _dbContextFactory = dbContextFactory; - } - public async Task> Handle( CreateSequentialPlayout request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request, cancellationToken); return await validation.Apply(playout => PersistPlayout(dbContext, playout, cancellationToken)); } @@ -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 private Validation ValidateYamlFile(CreateSequentialPlayout request) { - if (!_localFileSystem.FileExists(request.ScheduleFile)) + if (!fileSystem.File.Exists(request.ScheduleFile)) { return BaseError.New("Sequential schedule does not exist!"); } diff --git a/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs index 2467aade2..b47193413 100644 --- a/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs @@ -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; namespace ErsatzTV.Application.Playouts; -public class DeletePlayoutHandler : IRequestHandler> +public class DeletePlayoutHandler( + ChannelWriter workerChannel, + IDbContextFactory dbContextFactory, + IFileSystem fileSystem, + IMediator mediator) + : IRequestHandler> { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILocalFileSystem _localFileSystem; - private readonly IMediator _mediator; - private readonly ChannelWriter _workerChannel; - - public DeletePlayoutHandler( - ChannelWriter workerChannel, - IDbContextFactory dbContextFactory, - ILocalFileSystem localFileSystem, - IMediator mediator) - { - _workerChannel = workerChannel; - _dbContextFactory = dbContextFactory; - _localFileSystem = localFileSystem; - _mediator = mediator; - } - public async Task> Handle(DeletePlayout request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybePlayout = await dbContext.Playouts .Include(p => p.Channel) @@ -44,15 +32,15 @@ public class DeletePlayoutHandler : IRequestHandler dbContextFactory, ChannelWriter workerChannel, - ILocalFileSystem localFileSystem) + IFileSystem fileSystem) : IRequestHandler> { @@ -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!"); } diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs index c7b5cab98..d6ec000d3 100644 --- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs +++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs @@ -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; public class StartFFmpegSessionHandler : IRequestHandler> { 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 logger, ILogger sessionWorkerLogger, @@ -54,6 +57,7 @@ public class StartFFmpegSessionHandler : IRequestHandler _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 IHlsPlaylistFilter hlsPlaylistFilter, IHlsInitSegmentCache hlsInitSegmentCache, IConfigElementRepository configElementRepository, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, ILogger logger, Option targetFramerate) @@ -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 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 var generatedAtHash = new System.Collections.Generic.HashSet(); // 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 }) .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.None) @@ -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 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 private async Task> 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 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"); diff --git a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs index 3ec37c461..33e881b3e 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs @@ -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; public class GetLastPtsTimeHandler( IClient client, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ITempFilePool tempFilePool, IConfigElementRepository configElementRepository, ILogger logger) @@ -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); } diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 834ebbdef..2e042f889 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -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< 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 _logger; @@ -47,6 +49,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< public GetPlayoutItemProcessByChannelNumberHandler( IDbContextFactory dbContextFactory, IFFmpegProcessService ffmpegProcessService, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, IExternalJsonPlayoutItemProvider externalJsonPlayoutItemProvider, IPlexPathReplacementService plexPathReplacementService, @@ -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< } // check filesystem first - if (_localFileSystem.FileExists(path)) + if (_fileSystem.File.Exists(path)) { if (playoutItem.MediaItem is RemoteStream remoteStream) { diff --git a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedShowSubtitlesHandler.cs b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedShowSubtitlesHandler.cs index 4cc7293d7..769dca6af 100644 --- a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedShowSubtitlesHandler.cs +++ b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedShowSubtitlesHandler.cs @@ -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; 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( IDbContextFactory dbContextFactory, ChannelWriter workerChannel, IConfigElementRepository configElementRepository, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILogger logger) - : ExtractEmbeddedSubtitlesHandlerBase(localFileSystem, logger), + : ExtractEmbeddedSubtitlesHandlerBase(fileSystem, logger), IRequestHandler> { public async Task> Handle( diff --git a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs index 4e39a81a2..163bab787 100644 --- a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs +++ b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs @@ -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 public ExtractEmbeddedSubtitlesHandler( IDbContextFactory dbContextFactory, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, IEntityLocker entityLocker, IConfigElementRepository configElementRepository, ChannelWriter workerChannel, ILogger logger) - : base(localFileSystem, logger) + : base(fileSystem, logger) { _dbContextFactory = dbContextFactory; _entityLocker = entityLocker; diff --git a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs index 2cf895b77..99d08b768 100644 --- a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs +++ b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs @@ -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; 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; 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> FFmpegPathMustExist( TvContext dbContext, @@ -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 } 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 // 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 { foreach (string path in GetRelativeOutputPath(mediaItemId, subtitle)) { - return !localFileSystem.FileExists(path); + return !fileSystem.File.Exists(path); } return false; diff --git a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs index 0f5afb374..48a824da7 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions; using Dapper; using ErsatzTV.Application.Streaming; using ErsatzTV.Core; @@ -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( string path = await GetLocalPath(mediaItem, cancellationToken); // check filesystem first - if (localFileSystem.FileExists(path)) + if (fileSystem.File.Exists(path)) { if (mediaItem is RemoteStream remoteStream) { diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index 2099edcd5..c4e69d2eb 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs index 5eb6c7458..dc58bc84e 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs @@ -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 - "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 - 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 - 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 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 - "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 """; _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 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 - "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 - "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 } ]; - 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 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 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 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 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 - "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 - "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 - "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 - "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 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 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 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 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 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 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 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, diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs index c74dc1359..07a2cba0a 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs @@ -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 new ScriptEngine(Substitute.For>()), Substitute.For(), Substitute.For(), - Substitute.For(), + new MockFileSystem(), languageCodeService, Substitute.For>()); @@ -123,7 +124,7 @@ public class FFmpegStreamSelectorTests new ScriptEngine(Substitute.For>()), Substitute.For(), Substitute.For(), - Substitute.For(), + new MockFileSystem(), languageCodeService, Substitute.For>()); @@ -173,7 +174,7 @@ public class FFmpegStreamSelectorTests new ScriptEngine(Substitute.For>()), Substitute.For(), Substitute.For(), - Substitute.For(), + new MockFileSystem(), languageCodeService, Substitute.For>()); diff --git a/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs b/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs deleted file mode 100644 index bb4427c5e..000000000 --- a/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs +++ /dev/null @@ -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; } -} diff --git a/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs b/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs deleted file mode 100644 index 6b3efc223..000000000 --- a/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ErsatzTV.Core.Tests.Fakes; - -public record FakeFolderEntry(string Path); diff --git a/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs b/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs deleted file mode 100644 index 02874a635..000000000 --- a/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs +++ /dev/null @@ -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 _files; - private readonly List _folders; - - public FakeLocalFileSystem(List files) : this(files, new List()) - { - } - - public FakeLocalFileSystem(List files, List folders) - { - _files = files; - - var allFolders = new List(folders.Map(f => f.Path)); - foreach (FakeFileEntry file in _files) - { - List 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 ListSubdirectories(string folder) => - _folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder); - - public IEnumerable 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 ListFiles(string folder, string searchPattern) => - _files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder); - - public IEnumerable 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> CopyFile(string source, string destination) => - Task.FromResult(Right(Unit.Default)); - - public Unit EmptyFolder(string folder) => Unit.Default; - - public async Task ReadAllText(string path) => await _files - .Filter(f => f.Path == path) - .HeadOrNone() - .Select(f => f.Contents) - .IfNoneAsync(string.Empty); - - public async Task 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 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 Split(DirectoryInfo path) - { - var result = new List(); - if (path == null || string.IsNullOrWhiteSpace(path.FullName)) - { - return result; - } - - if (path.Parent != null) - { - result.AddRange(Split(path.Parent)); - } - - result.Add(path); - - return result; - } -} diff --git a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs index 3571032d7..9aaf4545c 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/ContinuePlayoutTests.cs @@ -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; 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 IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -596,7 +595,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -714,7 +713,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -722,7 +720,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -842,7 +840,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -850,7 +847,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); diff --git a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs index 17e14d416..9749e21ed 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs @@ -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; 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 IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -605,7 +604,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -717,7 +716,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -725,7 +723,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -888,7 +886,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -896,7 +893,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1018,7 +1015,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1026,7 +1022,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1147,7 +1143,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1155,7 +1150,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1277,7 +1272,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1285,7 +1279,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1412,7 +1406,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1420,7 +1413,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1547,7 +1540,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1555,7 +1547,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1691,7 +1683,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1699,7 +1690,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1828,7 +1819,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1836,7 +1826,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -1931,7 +1921,6 @@ public class NewPlayoutTests : PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -1939,7 +1928,7 @@ public class NewPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); diff --git a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs index c087ab8e9..a44657672 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/PlayoutBuilderTestBase.cs @@ -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; 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 IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -74,7 +73,7 @@ public abstract class PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); @@ -182,7 +181,6 @@ public abstract class PlayoutBuilderTestBase IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -190,7 +188,7 @@ public abstract class PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); diff --git a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs index 0332a1e51..72e3ea490 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/RefreshPlayoutTests.cs @@ -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; 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 IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); - ILocalFileSystem localFileSystem = Substitute.For(); IRerunHelper rerunHelper = Substitute.For(); var builder = new PlayoutBuilder( configRepo, @@ -109,7 +108,7 @@ public class RefreshPlayoutTests : PlayoutBuilderTestBase televisionRepo, artistRepo, factory, - localFileSystem, + new MockFileSystem(), rerunHelper, Logger); diff --git a/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs b/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs index 3d3db22ce..3344494e1 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs @@ -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 ISearchIndex searchIndex = provider.GetRequiredService(); await searchIndex.Initialize( new LocalFileSystem( + new MockFileSystem(), provider.GetRequiredService(), provider.GetRequiredService>()), provider.GetRequiredService(), @@ -125,7 +127,7 @@ public class ScheduleIntegrationTests new TelevisionRepository(factory, provider.GetRequiredService>()), new ArtistRepository(factory), Substitute.For(), - Substitute.For(), + new MockFileSystem(), Substitute.For(), provider.GetRequiredService>()); @@ -321,7 +323,7 @@ public class ScheduleIntegrationTests new TelevisionRepository(factory, provider.GetRequiredService>()), new ArtistRepository(factory), Substitute.For(), - Substitute.For(), + new MockFileSystem(), Substitute.For(), provider.GetRequiredService>()); diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj index 14d26a497..db6f919c7 100644 --- a/ErsatzTV.Core/ErsatzTV.Core.csproj +++ b/ErsatzTV.Core/ErsatzTV.Core.csproj @@ -32,6 +32,7 @@ + diff --git a/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs b/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs index 04cdde1e6..150d183f3 100644 --- a/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs +++ b/ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs @@ -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; namespace ErsatzTV.Core.FFmpeg; -public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger logger) +public class CustomStreamSelector(IFileSystem fileSystem, ILogger logger) : ICustomStreamSelector { public async Task SelectStreams( @@ -25,7 +25,7 @@ public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger _logger; private readonly IScriptEngine _scriptEngine; @@ -25,14 +26,14 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector IScriptEngine scriptEngine, IStreamSelectorRepository streamSelectorRepository, IConfigElementRepository configElementRepository, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILanguageCodeService languageCodeService, ILogger logger) { _scriptEngine = scriptEngine; _streamSelectorRepository = streamSelectorRepository; _configElementRepository = configElementRepository; - _localFileSystem = localFileSystem; + _fileSystem = fileSystem; _languageCodeService = languageCodeService; _logger = logger; } @@ -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.None; @@ -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"); diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs index 6c0c492bf..ef566f6e5 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs @@ -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 ListSubdirectories(string folder); IEnumerable ListFiles(string folder); IEnumerable ListFiles(string folder, string searchPattern); IEnumerable ListFiles(string folder, params string[] searchPatterns); - bool FileExists(string path); - bool FolderExists(string folder); Task> CopyFile(string source, string destination); Unit EmptyFolder(string folder); - Task ReadAllText(string path); - Task ReadAllLines(string path); Task GetHash(string path); string GetCustomOrDefaultFile(string folder, string file); } diff --git a/ErsatzTV.Core/Metadata/LocalFileSystem.cs b/ErsatzTV.Core/Metadata/LocalFileSystem.cs index d57d239c5..c81a02ab8 100644 --- a/ErsatzTV.Core/Metadata/LocalFileSystem.cs +++ b/ErsatzTV.Core/Metadata/LocalFileSystem.cs @@ -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 logger) : ILocalFileSystem +public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger 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 logger) : { try { - return File.GetLastWriteTimeUtc(path); + return fileSystem.File.GetLastWriteTimeUtc(path); } catch { @@ -38,16 +38,13 @@ public class LocalFileSystem(IClient client, ILogger logger) : } } - public bool IsLibraryPathAccessible(LibraryPath libraryPath) => - Directory.Exists(libraryPath.Path); - public IEnumerable 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 logger) : public IEnumerable 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 logger) : public IEnumerable 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 logger) : public IEnumerable 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 logger) : return new List(); } - public bool FileExists(string path) => File.Exists(path); - - public bool FolderExists(string folder) => Directory.Exists(folder); - public async Task> 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 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 logger) : return Unit.Default; } - public Task ReadAllText(string path) => File.ReadAllTextAsync(path); - public Task ReadAllLines(string path) => File.ReadAllLinesAsync(path); - [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")] public async Task 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}"); } } diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 4e7dbc8da..99e2bd092 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -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 { 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 _logger; @@ -34,7 +34,7 @@ public class PlayoutBuilder : IPlayoutBuilder ITelevisionRepository televisionRepository, IArtistRepository artistRepository, IMultiEpisodeShuffleCollectionEnumeratorFactory multiEpisodeFactory, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, IRerunHelper rerunHelper, ILogger logger) { @@ -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 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 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 diff --git a/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs index 7ff714f8f..2345d5344 100644 --- a/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs @@ -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( IConfigElementRepository configElementRepository, IScriptedPlayoutBuilderService scriptedPlayoutBuilderService, ISchedulingEngine schedulingEngine, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILogger logger) : IScriptedPlayoutBuilder { @@ -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", diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs index adc5c0a33..232fb6c58 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs @@ -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; public class SequentialPlayoutBuilder( //ISchedulingEngine schedulingEngine, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, IConfigElementRepository configElementRepository, IMediaCollectionRepository mediaCollectionRepository, IChannelRepository channelRepository, @@ -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"); diff --git a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj index a96dfa840..80ed692fa 100644 --- a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj +++ b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj @@ -22,10 +22,17 @@ all + + + + Resources/sequential-schedule.schema.json + + + diff --git a/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs b/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs index 038670d68..b9c1a4999 100644 --- a/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs +++ b/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs @@ -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 { var provider = new LocalStatisticsProvider( Substitute.For(), + new MockFileSystem(), Substitute.For(), Substitute.For(), Substitute.For(), diff --git a/ErsatzTV.Infrastructure.Tests/Scheduling/SequentialScheduleValidatorTests.cs b/ErsatzTV.Infrastructure.Tests/Scheduling/SequentialScheduleValidatorTests.cs new file mode 100644 index 000000000..b0092e41b --- /dev/null +++ b/ErsatzTV.Infrastructure.Tests/Scheduling/SequentialScheduleValidatorTests.cs @@ -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>()); + + 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>()); + + 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>()); + + IList result = await validator.GetValidationMessages(YAML, false); + result.ShouldNotBeNull(); + result.Count.ShouldBe(1); + result[0].ShouldContain("line 3"); + result[0].ShouldContain("position 5"); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs index 95954776b..cdf5aa9ed 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs @@ -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 dbContextFactory) + : ILibraryRepository { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILocalFileSystem _localFileSystem; - - public LibraryRepository(ILocalFileSystem localFileSystem, IDbContextFactory dbContextFactory) - { - _localFileSystem = localFileSystem; - _dbContextFactory = dbContextFactory; - } - public async Task 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 public async Task> 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 public async Task> 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 public async Task> 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 public async Task 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 public async Task 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 public async Task> 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 public async Task CountMediaItemsByPath(int libraryPathId) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); return await dbContext.Connection.QuerySingleAsync( @"SELECT COUNT(*) FROM MediaItem WHERE LibraryPathId = @LibraryPathId", new { LibraryPathId = libraryPathId }); @@ -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 public async Task CleanEtagsForLibraryPath(LibraryPath libraryPath) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); IOrderedEnumerable 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 return Option.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 Option 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 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 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", diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 731a988ea..c865c5eda 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -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 }; } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs index 8092660a7..fe5883ee5 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs @@ -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; namespace ErsatzTV.Infrastructure.Data.Repositories; -public class TemplateDataRepository(ILocalFileSystem localFileSystem, IDbContextFactory dbContextFactory) +public class TemplateDataRepository(IFileSystem fileSystem, IDbContextFactory dbContextFactory) : ITemplateDataRepository { public async Task>> GetMediaItemTemplateData( @@ -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 xmlProgrammes = EpgReader.FindProgrammesAt(stream, time, count); var result = new List>(); diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index a4749fe99..209bf6f44 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -16,6 +16,7 @@ + diff --git a/ErsatzTV.Infrastructure/FFmpeg/MpegTsScriptService.cs b/ErsatzTV.Infrastructure/FFmpeg/MpegTsScriptService.cs index f17a67efd..266ac289c 100644 --- a/ErsatzTV.Infrastructure/FFmpeg/MpegTsScriptService.cs +++ b/ErsatzTV.Infrastructure/FFmpeg/MpegTsScriptService.cs @@ -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; namespace ErsatzTV.Infrastructure.FFmpeg; public class MpegTsScriptService( + IFileSystem fileSystem, ILocalFileSystem localFileSystem, ITempFilePool tempFilePool, ILogger logger) : IMpegTsScriptService @@ -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 maybeScript = FromYaml(await localFileSystem.ReadAllText(definition)); + Option maybeScript = FromYaml(await fileSystem.File.ReadAllTextAsync(definition)); foreach (var script in maybeScript) { script.Id = Path.GetFileName(folder); @@ -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 diff --git a/ErsatzTV.Infrastructure/Images/ImageCache.cs b/ErsatzTV.Infrastructure/Images/ImageCache.cs index db5bee074..1c46feb57 100644 --- a/ErsatzTV.Infrastructure/Images/ImageCache.cs +++ b/ErsatzTV.Infrastructure/Images/ImageCache.cs @@ -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; 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> 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 Directory.CreateDirectory(baseFolder); } - await _localFileSystem.CopyFile(tempFileName, target); + await localFileSystem.CopyFile(tempFileName, target); return hex; } @@ -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 _ => FileSystemLayout.LegacyImageCacheFolder }; string target = Path.Combine(baseFolder, hex); - Either maybeResult = await _localFileSystem.CopyFile(path, target); + Either maybeResult = await localFileSystem.CopyFile(path, target); return maybeResult.Match>( _ => hex, error => error); @@ -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)) { diff --git a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs index e89e04b83..cff4a5f58 100644 --- a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs +++ b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs @@ -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 private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMetadataRepository _metadataRepository; + private readonly IFileSystem _fileSystem; public LocalStatisticsProvider( IMetadataRepository metadataRepository, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, IClient client, IHardwareCapabilitiesFactory hardwareCapabilitiesFactory, ILogger logger) { _metadataRepository = metadataRepository; + _fileSystem = fileSystem; _localFileSystem = localFileSystem; _client = client; _hardwareCapabilitiesFactory = hardwareCapabilitiesFactory; @@ -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.None; diff --git a/ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs b/ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs index 1b5e6421c..ea994c858 100644 --- a/ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs +++ b/ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs @@ -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; 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 logger) : ISequentialScheduleValidator +public class SequentialScheduleValidator(IFileSystem fileSystem, ILogger logger) + : ISequentialScheduleValidator { public async Task 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 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 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> 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 errorMessages) ? [] : errorMessages; } @@ -80,13 +79,24 @@ public class SequentialScheduleValidator(ILogger 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 }; diff --git a/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs b/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs index f3e3412d9..7e73234c4 100644 --- a/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs +++ b/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs @@ -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; public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider { private readonly IDbContextFactory _dbContextFactory; - private readonly ILocalFileSystem _localFileSystem; + private readonly IFileSystem _fileSystem; private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly ILogger _logger; private readonly IPlexPathReplacementService _plexPathReplacementService; @@ -28,7 +29,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider public ExternalJsonPlayoutItemProvider( IDbContextFactory dbContextFactory, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, IPlexPathReplacementService plexPathReplacementService, IPlexServerApiClient plexServerApiClient, IPlexSecretStore plexSecretStore, @@ -36,7 +37,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider ILogger logger) { _dbContextFactory = dbContextFactory; - _localFileSystem = localFileSystem; + _fileSystem = fileSystem; _plexPathReplacementService = plexPathReplacementService; _plexServerApiClient = plexServerApiClient; _plexSecretStore = plexSecretStore; @@ -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 CancellationToken cancellationToken) { Option maybeChannel = JsonConvert.DeserializeObject( - 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 program.File, cancellationToken); - if (_localFileSystem.FileExists(localPath)) + if (_fileSystem.File.Exists(localPath)) { return await StreamLocally(startTime, program, ffprobePath, localPath); } diff --git a/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs index a82585294..d9572f9b3 100644 --- a/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs +++ b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs @@ -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; public partial class GraphicsElementLoader( TemplateFunctions templateFunctions, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ITemplateDataRepository templateDataRepository, ILogger logger) : IGraphicsElementLoader @@ -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( 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( private async Task> GetTemplatedYaml(string fileName, Dictionary variables) { - string yaml = await localFileSystem.ReadAllText(fileName); + string yaml = await fileSystem.File.ReadAllTextAsync(fileName); try { var scriptObject = new ScriptObject(); diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index fb60b21f4..a3a0065e9 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -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 StreamingMode streamingMode) { var localFileSystem = new LocalFileSystem( + new MockFileSystem(), Substitute.For(), LoggerFactory.CreateLogger()); + var fileSystem = new MockFileSystem(); var tempFilePool = new TempFilePool(); ImageCache mockImageCache = Substitute.For(localFileSystem, tempFilePool); @@ -353,7 +356,8 @@ public class TranscodingTests var localStatisticsProvider = new LocalStatisticsProvider( metadataRepository, - new LocalFileSystem(Substitute.For(), LoggerFactory.CreateLogger()), + fileSystem, + localFileSystem, Substitute.For(), Substitute.For(), LoggerFactory.CreateLogger()); @@ -460,6 +464,12 @@ public class TranscodingTests // do nothing } + var localFileSystem = new LocalFileSystem( + new MockFileSystem(), + Substitute.For(), + LoggerFactory.CreateLogger()); + var fileSystem = new MockFileSystem(); + string file = fileToTest; if (string.IsNullOrWhiteSpace(file)) { @@ -503,7 +513,8 @@ public class TranscodingTests var localStatisticsProvider = new LocalStatisticsProvider( metadataRepository, - new LocalFileSystem(Substitute.For(), LoggerFactory.CreateLogger()), + fileSystem, + localFileSystem, Substitute.For(), Substitute.For(), LoggerFactory.CreateLogger()); @@ -669,9 +680,6 @@ public class TranscodingTests SubtitleMode = subtitleMode }; - var localFileSystem = new LocalFileSystem( - Substitute.For(), - LoggerFactory.CreateLogger()); var tempFilePool = new TempFilePool(); ImageCache mockImageCache = Substitute.For(localFileSystem, tempFilePool); diff --git a/ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs b/ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs deleted file mode 100644 index c6a216816..000000000 --- a/ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs +++ /dev/null @@ -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 _files; - private readonly List _folders; - - public FakeLocalFileSystem(List files) : this(files, new List()) - { - } - - public FakeLocalFileSystem(List files, List folders) - { - _files = files; - - var allFolders = new List(folders.Map(f => f.Path)); - foreach (FakeFileEntry file in _files) - { - List 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 ListSubdirectories(string folder) => - _folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder); - - public IEnumerable 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 ListFiles(string folder, string searchPattern) => - _files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder); - - public IEnumerable 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> CopyFile(string source, string destination) => - Task.FromResult(Right(Unit.Default)); - - public Unit EmptyFolder(string folder) => Unit.Default; - public Task ReadAllText(string path) => throw new NotImplementedException(); - public Task ReadAllLines(string path) => throw new NotImplementedException(); - public Task 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 Split(DirectoryInfo path) - { - var result = new List(); - if (path == null || string.IsNullOrWhiteSpace(path.FullName)) - { - return result; - } - - if (path.Parent != null) - { - result.AddRange(Split(path.Parent)); - } - - result.Add(path); - - return result; - } -} diff --git a/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs index c0c9ba2ed..0a9330741 100644 --- a/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs @@ -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 new(@"/Movies/Avatar (2009)/Avatar (2009).DE.SDH.FORCED.SRT") }; + var fileSystem = new MockFileSystem(); + IFileSystemInitializer init = fileSystem.Initialize(); + foreach (var file in fakeFiles) + { + init.WithFile(file.Path); + } + var provider = new LocalSubtitlesProvider( Substitute.For(), Substitute.For(), - new FakeLocalFileSystem(fakeFiles), + new LocalFileSystem(fileSystem, Substitute.For(), Substitute.For>()), Substitute.For>()); List result = provider.LocateExternalSubtitles( @@ -91,10 +102,17 @@ public class LocalSubtitlesProviderTests new(@"/Movies/Avatar (2009)/Avatar (2009).DE.SDH.FORCED.SRT") }; + var fileSystem = new MockFileSystem(); + IFileSystemInitializer init = fileSystem.Initialize(); + foreach (var file in fakeFiles) + { + init.WithFile(file.Path); + } + var provider = new LocalSubtitlesProvider( Substitute.For(), Substitute.For(), - new FakeLocalFileSystem(fakeFiles), + new LocalFileSystem(fileSystem, Substitute.For(), Substitute.For>()), Substitute.For>()); List result = provider.LocateExternalSubtitles( diff --git a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs index 639be8c17..6a5d73092 100644 --- a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs @@ -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 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 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(), Substitute.For>()), _movieRepository, _localStatisticsProvider, Substitute.For(), @@ -735,11 +742,21 @@ public class MovieFolderScannerTests Substitute.For(), Substitute.For(), Logger); + } - private MovieFolderScanner GetService(params FakeFolderEntry[] folders) => - new( + private MovieFolderScanner GetService(params FakeFolderEntry[] folders) + { + var fileSystem = new MockFileSystem(); + IFileSystemInitializer 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(), Substitute.For>()), _movieRepository, _localStatisticsProvider, Substitute.For(), @@ -753,5 +770,6 @@ public class MovieFolderScannerTests Substitute.For(), Substitute.For(), Logger); + } } } diff --git a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj index 874f98463..b312911f8 100644 --- a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj +++ b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj @@ -23,6 +23,7 @@ all + diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs index 098d9ef8c..77f142e22 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs @@ -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 : IMediaSourceRepository mediaSourceRepository, IEmbyMovieRepository embyMovieRepository, IEmbyPathReplacementService pathReplacementService, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, IMetadataRepository metadataRepository, ILogger logger) : base( scannerProxy, - localFileSystem, + fileSystem, localChaptersProvider, metadataRepository, logger) diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs index 8a45e72d0..2f3a2f0eb 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs @@ -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< IMediaSourceRepository mediaSourceRepository, IEmbyTelevisionRepository televisionRepository, IEmbyPathReplacementService pathReplacementService, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, IMetadataRepository metadataRepository, ILogger logger) : base( scannerProxy, - localFileSystem, + fileSystem, localChaptersProvider, metadataRepository, logger) diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs index 215cbfbb9..a5c3c6e9e 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs @@ -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 : IJellyfinMovieRepository jellyfinMovieRepository, IJellyfinPathReplacementService pathReplacementService, IMediaSourceRepository mediaSourceRepository, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, IMetadataRepository metadataRepository, ILogger logger) : base( scannerProxy, - localFileSystem, + fileSystem, localChaptersProvider, metadataRepository, logger) diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs index b26255f0c..82957f505 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs @@ -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 IMediaSourceRepository mediaSourceRepository, IJellyfinTelevisionRepository televisionRepository, IJellyfinPathReplacementService pathReplacementService, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, IMetadataRepository metadataRepository, ILogger logger) : base( scannerProxy, - localFileSystem, + fileSystem, localChaptersProvider, metadataRepository, logger) diff --git a/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs index 9401e6327..e6df0d782 100644 --- a/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs @@ -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 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 _logger; @@ -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 ITempFilePool tempFilePool, IClient client, ILogger logger) : base( - localFileSystem, + fileSystem, localStatisticsProvider, metadataRepository, mediaItemRepository, @@ -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 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 imageIds = await FlagFileNotFound(libraryPath, path); diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs index 02e9ca7ef..1823833be 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs @@ -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 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 private readonly ITempFilePool _tempFilePool; protected LocalFolderScanner( - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalStatisticsProvider localStatisticsProvider, IMetadataRepository metadataRepository, IMediaItemRepository mediaItemRepository, @@ -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 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 refreshResult = @@ -141,7 +142,7 @@ public abstract class LocalFolderScanner Option attachedPicIndex, CancellationToken cancellationToken) { - DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile); + DateTime lastWriteTime = _fileSystem.File.GetLastWriteTime(artworkFile); metadata.Artwork ??= new List(); @@ -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")); } diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs index 9f4d4a2b6..513b9fb6d 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs @@ -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 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 IImageRepository imageRepository, IRemoteStreamRepository remoteStreamRepository, IFallbackMetadataProvider fallbackMetadataProvider, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, IMovieNfoReader movieNfoReader, IEpisodeNfoReader episodeNfoReader, @@ -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 { string nfoFileName = Path.Combine(showFolder, "tvshow.nfo"); Option maybeMetadata = None; - if (_localFileSystem.FileExists(nfoFileName)) + if (_fileSystem.File.Exists(nfoFileName)) { maybeMetadata = await LoadTelevisionShowMetadata(nfoFileName); } @@ -106,7 +110,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider { string nfoFileName = Path.Combine(artistFolder, "artist.nfo"); Option maybeMetadata = None; - if (_localFileSystem.FileExists(nfoFileName)) + if (_fileSystem.File.Exists(nfoFileName)) { maybeMetadata = await LoadArtistMetadata(nfoFileName); } diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs index faab7296a..abbb8486c 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs @@ -10,7 +10,7 @@ namespace ErsatzTV.Scanner.Core.Metadata; public class LocalSubtitlesProvider : ILocalSubtitlesProvider { - private readonly List _languageCodes = new(); + private readonly List _languageCodes = []; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; diff --git a/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs index 01010b402..cacedbf8a 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs @@ -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 flagResult = await movieRepository.FlagNormal(library, result.Item); if (flagResult.IsSome) @@ -270,7 +270,7 @@ public abstract class MediaServerMovieLibraryScanner flagResult = await otherVideoRepository.FlagNormal(library, result.Item); if (flagResult.IsSome) @@ -277,7 +277,7 @@ public abstract class MediaServerOtherVideoLibraryScanner flagResult = await televisionRepository.FlagNormal(library, result.Item, cancellationToken); if (flagResult.IsSome) @@ -541,7 +541,7 @@ public abstract class MediaServerTelevisionLibraryScanner refreshResult = diff --git a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs index ab3364d18..60d627e9f 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs @@ -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 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 public MovieFolderScanner( IScannerProxy scannerProxy, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, IMovieRepository movieRepository, ILocalStatisticsProvider localStatisticsProvider, @@ -47,7 +50,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner IClient client, ILogger logger) : base( - localFileSystem, + fileSystem, localStatisticsProvider, metadataRepository, mediaItemRepository, @@ -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 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 ids = await FlagFileNotFound(libraryPath, path); @@ -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 IEnumerable possibleMoviePosters = ImageFileExtensions.Collect(ext => new[] { $"{segment}.{ext}", Path.GetFileNameWithoutExtension(path) + $"-{segment}.{ext}" }) .Map(f => Path.Combine(folder, f)); - Option result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone(); + Option result = possibleMoviePosters.Filter(p => _fileSystem.File.Exists(p)).HeadOrNone(); if (result.IsNone && artworkKind == ArtworkKind.Poster) { IEnumerable 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; diff --git a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs index 7aaaf441f..4969679bf 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs @@ -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 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 public MusicVideoFolderScanner( IScannerProxy scannerProxy, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, @@ -47,7 +50,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan ITempFilePool tempFilePool, IClient client, ILogger logger) : base( - localFileSystem, + fileSystem, localStatisticsProvider, metadataRepository, mediaItemRepository, @@ -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 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 musicVideoIds = await FlagFileNotFound(libraryPath, path); @@ -459,7 +463,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan } private Option 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 LocateArtworkForArtist(string artistFolder, ArtworkKind artworkKind) { @@ -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 { 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 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(); } } diff --git a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs index 7d42856e4..ecbacbdc3 100644 --- a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs @@ -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 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 public OtherVideoFolderScanner( IScannerProxy scannerProxy, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, @@ -45,7 +48,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan ITempFilePool tempFilePool, IClient client, ILogger logger) : base( - localFileSystem, + fileSystem, localStatisticsProvider, metadataRepository, mediaItemRepository, @@ -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 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 otherVideoIds = await FlagFileNotFound(libraryPath, path); @@ -274,7 +278,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path; Option maybeNfoFile = new List { Path.ChangeExtension(path, "nfo") } - .Filter(_localFileSystem.FileExists) + .Filter(_fileSystem.File.Exists) .HeadOrNone(); if (maybeNfoFile.IsNone) @@ -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(); } } diff --git a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs index e6f0fdba0..edf91b42e 100644 --- a/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/RemoteStreamFolderScanner.cs @@ -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 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 _logger; @@ -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 ITempFilePool tempFilePool, IClient client, ILogger logger) : base( - localFileSystem, + fileSystem, localStatisticsProvider, metadataRepository, mediaItemRepository, @@ -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 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 remoteStreamIds = await FlagFileNotFound(libraryPath, path); diff --git a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs index 994bab319..45b862680 100644 --- a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs @@ -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 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 _logger; @@ -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 ITempFilePool tempFilePool, IClient client, ILogger logger) : base( - localFileSystem, + fileSystem, localStatisticsProvider, metadataRepository, mediaItemRepository, @@ -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 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 songIds = await FlagFileNotFound(libraryPath, path); @@ -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(); } diff --git a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs index 9dcd79f3d..7bf9e73df 100644 --- a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs @@ -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 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 public TelevisionFolderScanner( IScannerProxy scannerProxy, + IFileSystem fileSystem, ILocalFileSystem localFileSystem, ITelevisionRepository televisionRepository, ILocalStatisticsProvider localStatisticsProvider, @@ -48,7 +51,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan IClient client, IFallbackMetadataProvider fallbackMetadataProvider, ILogger logger) : base( - localFileSystem, + fileSystem, localStatisticsProvider, metadataRepository, mediaItemRepository, @@ -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 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 } private Option 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 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 LocateArtworkForShow(string showFolder, ArtworkKind artworkKind) @@ -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 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 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(); } } diff --git a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs index d20c8bfb2..7aaa8fb77 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs @@ -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 : IMediaSourceRepository mediaSourceRepository, IPlexMovieRepository plexMovieRepository, IPlexPathReplacementService plexPathReplacementService, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, ILogger logger) : base( scannerProxy, - localFileSystem, + fileSystem, localChaptersProvider, metadataRepository, logger) diff --git a/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs index a9052dc74..36a006a8c 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexOtherVideoLibraryScanner.cs @@ -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 : IMediaSourceRepository mediaSourceRepository, IPlexOtherVideoRepository plexOtherVideoRepository, IPlexPathReplacementService plexPathReplacementService, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, ILogger logger) : base( scannerProxy, - localFileSystem, + fileSystem, localChaptersProvider, metadataRepository, logger) diff --git a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs index a94180034..5f20b7c69 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs @@ -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 : IMediaSourceRepository mediaSourceRepository, IPlexPathReplacementService plexPathReplacementService, IPlexTelevisionRepository plexTelevisionRepository, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, ILocalChaptersProvider localChaptersProvider, ILogger logger) : base( scannerProxy, - localFileSystem, + fileSystem, localChaptersProvider, metadataRepository, logger) diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index febbb37c1..478b83173 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions; using Bugsnag; using Bugsnag.Payload; using Dapper; @@ -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 services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining()); services.AddMemoryCache(); diff --git a/ErsatzTV/Controllers/Api/TroubleshootController.cs b/ErsatzTV/Controllers/Api/TroubleshootController.cs index 48eb93512..7b3360411 100644 --- a/ErsatzTV/Controllers/Api/TroubleshootController.cs +++ b/ErsatzTV/Controllers/Api/TroubleshootController.cs @@ -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; 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; [ApiController] public class TroubleshootController( ChannelWriter channelWriter, - ILocalFileSystem localFileSystem, + IFileSystem fileSystem, IConfigElementRepository configElementRepository, ITroubleshootingNotifier notifier, IMediator mediator) : ControllerBase @@ -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)) diff --git a/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor b/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor index 5b27d6ca0..e005930ad 100644 --- a/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor +++ b/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor @@ -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 @@ @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 @@ @inject IEntityLocker Locker @inject ICourier Courier; @inject ISnackbar Snackbar; -@inject ILocalFileSystem LocalFileSystem; +@inject IFileSystem FileSystem; @@ -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); }); diff --git a/ErsatzTV/Services/RunOnce/CacheCleanerService.cs b/ErsatzTV/Services/RunOnce/CacheCleanerService.cs index 1a4731ba4..3bb768cdf 100644 --- a/ErsatzTV/Services/RunOnce/CacheCleanerService.cs +++ b/ErsatzTV/Services/RunOnce/CacheCleanerService.cs @@ -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; namespace ErsatzTV.Services.RunOnce; -public class CacheCleanerService : BackgroundService +public class CacheCleanerService( + IServiceScopeFactory serviceScopeFactory, + SystemStartup systemStartup, + ILogger logger) + : BackgroundService { - private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly SystemStartup _systemStartup; - - public CacheCleanerService( - IServiceScopeFactory serviceScopeFactory, - SystemStartup systemStartup, - ILogger 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(); ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService(); + IFileSystem fileSystem = scope.ServiceProvider.GetRequiredService(); - 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 logos = await dbContext.Channels .AsNoTracking() @@ -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 } } - _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 channelFiles = await dbContext.Channels .AsNoTracking() diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index ec20b6643..3544118a5 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -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; using Scalar.AspNetCore; using Serilog; using Serilog.Events; +using Testably.Abstractions; namespace ErsatzTV; @@ -692,6 +694,8 @@ public class Startup { services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // TODO: does this need to be singleton?