diff --git a/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs index bee9914ae..92ed9521c 100644 --- a/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs @@ -74,7 +74,7 @@ public class FFmpegCapabilities( VideoFormat.Vp9 => new DecoderVp9(), VideoFormat.Av1 => new DecoderAv1(ffmpegDecoders), - VideoFormat.Raw => new DecoderRawVideo(), + VideoFormat.Raw or VideoFormat.RawVideo => new DecoderRawVideo(), VideoFormat.Undetermined => new DecoderImplicit(), VideoFormat.Copy => new DecoderImplicit(), VideoFormat.GeneratedImage => new DecoderImplicit(), diff --git a/ErsatzTV.FFmpeg/Format/VideoFormat.cs b/ErsatzTV.FFmpeg/Format/VideoFormat.cs index 76b3df7ac..591b01f07 100644 --- a/ErsatzTV.FFmpeg/Format/VideoFormat.cs +++ b/ErsatzTV.FFmpeg/Format/VideoFormat.cs @@ -15,6 +15,7 @@ public static class VideoFormat public const string Av1 = "av1"; public const string MpegTs = "mpegts"; public const string Raw = "raw"; + public const string RawVideo = "rawvideo"; public const string Copy = "copy"; public const string GeneratedImage = "generated-image"; diff --git a/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs b/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs index 9a0e78ef2..038670d68 100644 --- a/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs +++ b/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs @@ -2,6 +2,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.Infrastructure.Metadata; using Microsoft.Extensions.Logging; using NSubstitute; @@ -22,6 +23,7 @@ public class LocalStatisticsProviderTests Substitute.For(), Substitute.For(), Substitute.For(), + Substitute.For(), Substitute.For>()); var input = new LocalStatisticsProvider.FFprobe( diff --git a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs index 4da341eaa..cd14d2218 100644 --- a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs +++ b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs @@ -11,6 +11,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.FFmpeg.Capabilities; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using File = TagLib.File; @@ -20,6 +21,7 @@ namespace ErsatzTV.Infrastructure.Metadata; public class LocalStatisticsProvider : ILocalStatisticsProvider { private readonly IClient _client; + private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMetadataRepository _metadataRepository; @@ -28,11 +30,13 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider IMetadataRepository metadataRepository, ILocalFileSystem localFileSystem, IClient client, + IHardwareCapabilitiesFactory hardwareCapabilitiesFactory, ILogger logger) { _metadataRepository = metadataRepository; _localFileSystem = localFileSystem; _client = client; + _hardwareCapabilitiesFactory = hardwareCapabilitiesFactory; _logger = logger; } @@ -52,6 +56,12 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider try { string filePath = await PathForMediaItem(mediaItem); + + if (Path.GetExtension(filePath) == ".avs" && !_hardwareCapabilitiesFactory.IsAviSynthInstalled()) + { + return BaseError.New(".avs files are not supported; compatible ffmpeg and avisynth are both required"); + } + return await RefreshStatistics(ffmpegPath, ffprobePath, mediaItem, filePath); } catch (Exception ex) diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index cf3bdfc37..18127ca6c 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -352,6 +352,7 @@ public class TranscodingTests metadataRepository, new LocalFileSystem(Substitute.For(), LoggerFactory.CreateLogger()), Substitute.For(), + Substitute.For(), LoggerFactory.CreateLogger()); await localStatisticsProvider.RefreshStatistics(ExecutableName("ffmpeg"), ExecutableName("ffprobe"), song); @@ -500,6 +501,7 @@ public class TranscodingTests metadataRepository, new LocalFileSystem(Substitute.For(), LoggerFactory.CreateLogger()), Substitute.For(), + Substitute.For(), LoggerFactory.CreateLogger()); await localStatisticsProvider.RefreshStatistics( diff --git a/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs new file mode 100644 index 000000000..b9804e8ce --- /dev/null +++ b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Scanner.Application.FFmpeg; + +public record RefreshFFmpegCapabilities : IRequest; diff --git a/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs new file mode 100644 index 000000000..918415e07 --- /dev/null +++ b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs @@ -0,0 +1,45 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.FFmpeg.Capabilities; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Scanner.Application.FFmpeg; + +public class RefreshFFmpegCapabilitiesHandler( + IDbContextFactory dbContextFactory, + IHardwareCapabilitiesFactory hardwareCapabilitiesFactory, + ILocalStatisticsProvider localStatisticsProvider) + : IRequestHandler +{ + public async Task Handle(RefreshFFmpegCapabilities request, CancellationToken cancellationToken) + { + hardwareCapabilitiesFactory.ClearCache(); + + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybeFFmpegPath = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegPath, cancellationToken) + .FilterT(File.Exists); + + foreach (string ffmpegPath in maybeFFmpegPath) + { + _ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath); + + Option maybeFFprobePath = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFprobePath, cancellationToken) + .FilterT(File.Exists); + + foreach (string ffprobePath in maybeFFprobePath) + { + Either result = await localStatisticsProvider.GetStatistics( + ffprobePath, + Path.Combine(FileSystemLayout.ResourcesCacheFolder, "test.avs")); + + hardwareCapabilitiesFactory.SetAviSynthInstalled(result.IsRight); + } + } + } +} diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs index 2c4cd76e4..02e9ca7ef 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs @@ -19,7 +19,7 @@ public abstract class LocalFolderScanner { public static readonly ImmutableHashSet VideoFileExtensions = new[] { - ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".ogv", ".mp4", + ".avs", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".ogv", ".mp4", ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".m2ts", ".ts", ".webm" }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); diff --git a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings index a21ca000b..e89e1b47b 100644 --- a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings +++ b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings @@ -1,5 +1,6 @@  True + True True True True \ No newline at end of file diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index 7f608e2ad..500c2abd7 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -17,6 +17,7 @@ using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; using ErsatzTV.Core.Search; +using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; @@ -207,6 +208,7 @@ public class Program services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/ErsatzTV.Scanner/Worker.cs b/ErsatzTV.Scanner/Worker.cs index 123b385dd..1a79ec136 100644 --- a/ErsatzTV.Scanner/Worker.cs +++ b/ErsatzTV.Scanner/Worker.cs @@ -1,6 +1,7 @@ using System.CommandLine; using System.Diagnostics; using ErsatzTV.Scanner.Application.Emby; +using ErsatzTV.Scanner.Application.FFmpeg; using ErsatzTV.Scanner.Application.Jellyfin; using ErsatzTV.Scanner.Application.MediaSources; using ErsatzTV.Scanner.Application.Plex; @@ -28,6 +29,10 @@ public class Worker : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + await mediator.Send(new RefreshFFmpegCapabilities(), stoppingToken); + RootCommand rootCommand = ConfigureCommandLine(); // need to strip program name (head) from command line args