Browse Source

scan avs files in all local libraries

pull/2612/head
Jason Dove 9 months ago
parent
commit
4e90ddd436
No known key found for this signature in database
  1. 2
      ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs
  2. 1
      ErsatzTV.FFmpeg/Format/VideoFormat.cs
  3. 2
      ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs
  4. 10
      ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs
  5. 2
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  6. 3
      ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs
  7. 45
      ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs
  8. 2
      ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs
  9. 1
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings
  10. 2
      ErsatzTV.Scanner/Program.cs
  11. 5
      ErsatzTV.Scanner/Worker.cs

2
ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs

@ -74,7 +74,7 @@ public class FFmpegCapabilities(
VideoFormat.Vp9 => new DecoderVp9(), VideoFormat.Vp9 => new DecoderVp9(),
VideoFormat.Av1 => new DecoderAv1(ffmpegDecoders), VideoFormat.Av1 => new DecoderAv1(ffmpegDecoders),
VideoFormat.Raw => new DecoderRawVideo(), VideoFormat.Raw or VideoFormat.RawVideo => new DecoderRawVideo(),
VideoFormat.Undetermined => new DecoderImplicit(), VideoFormat.Undetermined => new DecoderImplicit(),
VideoFormat.Copy => new DecoderImplicit(), VideoFormat.Copy => new DecoderImplicit(),
VideoFormat.GeneratedImage => new DecoderImplicit(), VideoFormat.GeneratedImage => new DecoderImplicit(),

1
ErsatzTV.FFmpeg/Format/VideoFormat.cs

@ -15,6 +15,7 @@ public static class VideoFormat
public const string Av1 = "av1"; public const string Av1 = "av1";
public const string MpegTs = "mpegts"; public const string MpegTs = "mpegts";
public const string Raw = "raw"; public const string Raw = "raw";
public const string RawVideo = "rawvideo";
public const string Copy = "copy"; public const string Copy = "copy";
public const string GeneratedImage = "generated-image"; public const string GeneratedImage = "generated-image";

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

@ -2,6 +2,7 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.Infrastructure.Metadata; using ErsatzTV.Infrastructure.Metadata;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
@ -22,6 +23,7 @@ public class LocalStatisticsProviderTests
Substitute.For<IMetadataRepository>(), Substitute.For<IMetadataRepository>(),
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
Substitute.For<IClient>(), Substitute.For<IClient>(),
Substitute.For<IHardwareCapabilitiesFactory>(),
Substitute.For<ILogger<LocalStatisticsProvider>>()); Substitute.For<ILogger<LocalStatisticsProvider>>());
var input = new LocalStatisticsProvider.FFprobe( var input = new LocalStatisticsProvider.FFprobe(

10
ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs

@ -11,6 +11,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Capabilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using File = TagLib.File; using File = TagLib.File;
@ -20,6 +21,7 @@ namespace ErsatzTV.Infrastructure.Metadata;
public class LocalStatisticsProvider : ILocalStatisticsProvider public class LocalStatisticsProvider : ILocalStatisticsProvider
{ {
private readonly IClient _client; private readonly IClient _client;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<LocalStatisticsProvider> _logger; private readonly ILogger<LocalStatisticsProvider> _logger;
private readonly IMetadataRepository _metadataRepository; private readonly IMetadataRepository _metadataRepository;
@ -28,11 +30,13 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
IMetadataRepository metadataRepository, IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IClient client, IClient client,
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
ILogger<LocalStatisticsProvider> logger) ILogger<LocalStatisticsProvider> logger)
{ {
_metadataRepository = metadataRepository; _metadataRepository = metadataRepository;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_client = client; _client = client;
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
_logger = logger; _logger = logger;
} }
@ -52,6 +56,12 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
try try
{ {
string filePath = await PathForMediaItem(mediaItem); 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); return await RefreshStatistics(ffmpegPath, ffprobePath, mediaItem, filePath);
} }
catch (Exception ex) catch (Exception ex)

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

@ -352,6 +352,7 @@ public class TranscodingTests
metadataRepository, metadataRepository,
new LocalFileSystem(Substitute.For<IClient>(), LoggerFactory.CreateLogger<LocalFileSystem>()), new LocalFileSystem(Substitute.For<IClient>(), LoggerFactory.CreateLogger<LocalFileSystem>()),
Substitute.For<IClient>(), Substitute.For<IClient>(),
Substitute.For<IHardwareCapabilitiesFactory>(),
LoggerFactory.CreateLogger<LocalStatisticsProvider>()); LoggerFactory.CreateLogger<LocalStatisticsProvider>());
await localStatisticsProvider.RefreshStatistics(ExecutableName("ffmpeg"), ExecutableName("ffprobe"), song); await localStatisticsProvider.RefreshStatistics(ExecutableName("ffmpeg"), ExecutableName("ffprobe"), song);
@ -500,6 +501,7 @@ public class TranscodingTests
metadataRepository, metadataRepository,
new LocalFileSystem(Substitute.For<IClient>(), LoggerFactory.CreateLogger<LocalFileSystem>()), new LocalFileSystem(Substitute.For<IClient>(), LoggerFactory.CreateLogger<LocalFileSystem>()),
Substitute.For<IClient>(), Substitute.For<IClient>(),
Substitute.For<IHardwareCapabilitiesFactory>(),
LoggerFactory.CreateLogger<LocalStatisticsProvider>()); LoggerFactory.CreateLogger<LocalStatisticsProvider>());
await localStatisticsProvider.RefreshStatistics( await localStatisticsProvider.RefreshStatistics(

3
ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs

@ -0,0 +1,3 @@
namespace ErsatzTV.Scanner.Application.FFmpeg;
public record RefreshFFmpegCapabilities : IRequest;

45
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<TvContext> dbContextFactory,
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
ILocalStatisticsProvider localStatisticsProvider)
: IRequestHandler<RefreshFFmpegCapabilities>
{
public async Task Handle(RefreshFFmpegCapabilities request, CancellationToken cancellationToken)
{
hardwareCapabilitiesFactory.ClearCache();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<string> maybeFFmpegPath = await dbContext.ConfigElements
.GetValue<string>(ConfigElementKey.FFmpegPath, cancellationToken)
.FilterT(File.Exists);
foreach (string ffmpegPath in maybeFFmpegPath)
{
_ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
Option<string> maybeFFprobePath = await dbContext.ConfigElements
.GetValue<string>(ConfigElementKey.FFprobePath, cancellationToken)
.FilterT(File.Exists);
foreach (string ffprobePath in maybeFFprobePath)
{
Either<BaseError, MediaVersion> result = await localStatisticsProvider.GetStatistics(
ffprobePath,
Path.Combine(FileSystemLayout.ResourcesCacheFolder, "test.avs"));
hardwareCapabilitiesFactory.SetAviSynthInstalled(result.IsRight);
}
}
}
}

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

@ -19,7 +19,7 @@ public abstract class LocalFolderScanner
{ {
public static readonly ImmutableHashSet<string> VideoFileExtensions = new[] public static readonly ImmutableHashSet<string> 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" ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".m2ts", ".ts", ".webm"
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);

1
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings

@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cemby_005Ccommands/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cemby_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cffmpeg_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cjellyfin_005Ccommands/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cjellyfin_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cmediasources_005Ccommands/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cmediasources_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cplex_005Ccommands/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=application_005Cplex_005Ccommands/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

2
ErsatzTV.Scanner/Program.cs

@ -17,6 +17,7 @@ using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex; using ErsatzTV.Core.Plex;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
@ -207,6 +208,7 @@ public class Program
services.AddScoped<IOtherVideoNfoReader, OtherVideoNfoReader>(); services.AddScoped<IOtherVideoNfoReader, OtherVideoNfoReader>();
services.AddScoped<IFFmpegPngService, FFmpegPngService>(); services.AddScoped<IFFmpegPngService, FFmpegPngService>();
services.AddScoped<IRuntimeInfo, RuntimeInfo>(); services.AddScoped<IRuntimeInfo, RuntimeInfo>();
services.AddScoped<IHardwareCapabilitiesFactory, HardwareCapabilitiesFactory>();
services.AddScoped<IPlexMovieLibraryScanner, PlexMovieLibraryScanner>(); services.AddScoped<IPlexMovieLibraryScanner, PlexMovieLibraryScanner>();
services.AddScoped<IPlexOtherVideoLibraryScanner, PlexOtherVideoLibraryScanner>(); services.AddScoped<IPlexOtherVideoLibraryScanner, PlexOtherVideoLibraryScanner>();

5
ErsatzTV.Scanner/Worker.cs

@ -1,6 +1,7 @@
using System.CommandLine; using System.CommandLine;
using System.Diagnostics; using System.Diagnostics;
using ErsatzTV.Scanner.Application.Emby; using ErsatzTV.Scanner.Application.Emby;
using ErsatzTV.Scanner.Application.FFmpeg;
using ErsatzTV.Scanner.Application.Jellyfin; using ErsatzTV.Scanner.Application.Jellyfin;
using ErsatzTV.Scanner.Application.MediaSources; using ErsatzTV.Scanner.Application.MediaSources;
using ErsatzTV.Scanner.Application.Plex; using ErsatzTV.Scanner.Application.Plex;
@ -28,6 +29,10 @@ public class Worker : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Send(new RefreshFFmpegCapabilities(), stoppingToken);
RootCommand rootCommand = ConfigureCommandLine(); RootCommand rootCommand = ConfigureCommandLine();
// need to strip program name (head) from command line args // need to strip program name (head) from command line args

Loading…
Cancel
Save