Explorar el Código

Merge remote-tracking branch 'upstream/master' into warn-259810

Cody Robibero hace 3 años
padre
commit
ec13412155
Se han modificado 47 ficheros con 999 adiciones y 342 borrados
  1. 3 0
      .gitignore
  2. 1 1
      Dockerfile
  3. 1 1
      Dockerfile.arm
  4. 1 1
      Dockerfile.arm64
  5. 0 1
      Emby.Server.Implementations/ApplicationHost.cs
  6. 1 1
      Emby.Server.Implementations/Collections/CollectionManager.cs
  7. 11 1
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  8. 2 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  9. 1 4
      Emby.Server.Implementations/Library/LibraryManager.cs
  10. 17 0
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  11. 3 5
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  12. 19 26
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  13. 0 5
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  14. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
  15. 5 7
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  16. 2 2
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  17. 1 1
      Emby.Server.Implementations/Localization/Core/tr.json
  18. 1 0
      Emby.Server.Implementations/Properties/AssemblyInfo.cs
  19. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
  20. 2 2
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  21. 1 1
      Jellyfin.Server/Jellyfin.Server.csproj
  22. 298 91
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  23. 23 0
      MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
  24. 13 7
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  25. 94 12
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  26. 69 89
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  27. 17 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  28. 6 2
      MediaBrowser.Model/Configuration/MediaPathInfo.cs
  29. 5 0
      MediaBrowser.Model/Entities/MediaStream.cs
  30. 1 0
      MediaBrowser.Model/System/SystemInfo.cs
  31. 1 17
      MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
  32. 2 2
      MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs
  33. 70 52
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  34. 7 0
      fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
  35. 30 0
      fuzz/Emby.Server.Implementations.Fuzz/Program.cs
  36. 1 0
      fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt
  37. 1 1
      tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
  38. 1 1
      tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
  39. 8 4
      tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
  40. 24 0
      tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
  41. 80 0
      tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
  42. 1 1
      tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
  43. 1 1
      tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
  44. 3 0
      tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
  45. 122 0
      tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
  46. 14 0
      tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
  47. 33 0
      tests/Jellyfin.XbmcMetadata.Tests/Test Data/Fanart.nfo

+ 3 - 0
.gitignore

@@ -278,3 +278,6 @@ web/
 web-src.*
 web-src.*
 MediaBrowser.WebDashboard/jellyfin-web
 MediaBrowser.WebDashboard/jellyfin-web
 apiclient/generated
 apiclient/generated
+
+# Omnisharp crash logs
+mono_crash.*.json

+ 1 - 1
Dockerfile

@@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
  && mv dist /dist
 
 
-FROM debian:buster-slim as app
+FROM debian:bullseye-slim as app
 
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 ARG DEBIAN_FRONTEND="noninteractive"

+ 1 - 1
Dockerfile.arm

@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && mv dist /dist
  && mv dist /dist
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:buster-slim as app
+FROM arm32v7/debian:bullseye-slim as app
 
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 ARG DEBIAN_FRONTEND="noninteractive"

+ 1 - 1
Dockerfile.arm64

@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && mv dist /dist
  && mv dist /dist
 
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:buster-slim as app
+FROM arm64v8/debian:bullseye-slim as app
 
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 ARG DEBIAN_FRONTEND="noninteractive"

+ 0 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -1100,7 +1100,6 @@ namespace Emby.Server.Implementations
                 ServerName = FriendlyName,
                 ServerName = FriendlyName,
                 LocalAddress = GetSmartApiUrl(source),
                 LocalAddress = GetSmartApiUrl(source),
                 SupportsLibraryMonitor = true,
                 SupportsLibraryMonitor = true,
-                EncoderLocation = _mediaEncoder.EncoderLocation,
                 SystemArchitecture = RuntimeInformation.OSArchitecture,
                 SystemArchitecture = RuntimeInformation.OSArchitecture,
                 PackageName = _startupOptions.PackageName
                 PackageName = _startupOptions.PackageName
             };
             };

+ 1 - 1
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
 
 
             var libraryOptions = new LibraryOptions
             var libraryOptions = new LibraryOptions
             {
             {
-                PathInfos = new[] { new MediaPathInfo { Path = path } },
+                PathInfos = new[] { new MediaPathInfo(path) },
                 EnableRealtimeMonitor = false,
                 EnableRealtimeMonitor = false,
                 SaveLocalMetadata = true
                 SaveLocalMetadata = true
             };
             };

+ 11 - 1
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -1141,15 +1141,25 @@ namespace Emby.Server.Implementations.Data
                 Path = RestorePath(path.ToString())
                 Path = RestorePath(path.ToString())
             };
             };
 
 
-            if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks))
+            if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)
+                && ticks >= DateTime.MinValue.Ticks
+                && ticks <= DateTime.MaxValue.Ticks)
             {
             {
                 image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
                 image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
             }
             }
+            else
+            {
+                return null;
+            }
 
 
             if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
             if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
             {
             {
                 image.Type = type;
                 image.Type = type;
             }
             }
+            else
+            {
+                return null;
+            }
 
 
             // Optional parameters: width*height*blurhash
             // Optional parameters: width*height*blurhash
             if (nextSegment + 1 < value.Length - 1)
             if (nextSegment + 1 < value.Length - 1)

+ 2 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -23,6 +23,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
@@ -30,7 +31,7 @@
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
     <PackageReference Include="sharpcompress" Version="0.28.3" />
     <PackageReference Include="sharpcompress" Version="0.28.3" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />

+ 1 - 4
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -3173,10 +3173,7 @@ namespace Emby.Server.Implementations.Library
                 {
                 {
                     if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
                     if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
                     {
                     {
-                        list.Add(new MediaPathInfo
-                        {
-                            Path = location
-                        });
+                        list.Add(new MediaPathInfo(location));
                     }
                     }
                 }
                 }
 
 

+ 17 - 0
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -5,6 +5,7 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using DiscUtils.Udf;
 using Emby.Naming.Video;
 using Emby.Naming.Video;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -201,6 +202,22 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 {
                 {
                     video.IsoType = IsoType.BluRay;
                     video.IsoType = IsoType.BluRay;
                 }
                 }
+                else
+                {
+                    // use disc-utils, both DVDs and BDs use UDF filesystem
+                    using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
+                    {
+                        UdfReader udfReader = new UdfReader(videoFileStream);
+                        if (udfReader.DirectoryExists("VIDEO_TS"))
+                        {
+                            video.IsoType = IsoType.Dvd;
+                        }
+                        else if (udfReader.DirectoryExists("BDMV"))
+                        {
+                            video.IsoType = IsoType.BluRay;
+                        }
+                    }
+                }
             }
             }
         }
         }
 
 

+ 3 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return targetFile;
             return targetFile;
         }
         }
 
 
-        public Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
+        public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
         {
             if (directStreamProvider != null)
             if (directStreamProvider != null)
             {
             {
@@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
         private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
         {
-            Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+            Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
 
 
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
             using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
             using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
@@ -71,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
             _logger.LogInformation("Opened recording stream from tuner provider");
             _logger.LogInformation("Opened recording stream from tuner provider");
 
 
-            Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+            Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
 
 
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
             // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
             await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
             await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);

+ 19 - 26
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -159,8 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             try
             try
             {
             {
                 var recordingFolders = GetRecordingFolders().ToArray();
                 var recordingFolders = GetRecordingFolders().ToArray();
-                var virtualFolders = _libraryManager.GetVirtualFolders()
-                    .ToList();
+                var virtualFolders = _libraryManager.GetVirtualFolders();
 
 
                 var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
                 var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
 
 
@@ -177,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                         continue;
                         continue;
                     }
                     }
 
 
-                    var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
+                    var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray();
 
 
                     var libraryOptions = new LibraryOptions
                     var libraryOptions = new LibraryOptions
                     {
                     {
@@ -210,7 +209,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 foreach (var path in pathsToRemove)
                 foreach (var path in pathsToRemove)
                 {
                 {
-                    await RemovePathFromLibrary(path).ConfigureAwait(false);
+                    await RemovePathFromLibraryAsync(path).ConfigureAwait(false);
                 }
                 }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
@@ -219,13 +218,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        private async Task RemovePathFromLibrary(string path)
+        private async Task RemovePathFromLibraryAsync(string path)
         {
         {
             _logger.LogDebug("Removing path from library: {0}", path);
             _logger.LogDebug("Removing path from library: {0}", path);
 
 
             var requiresRefresh = false;
             var requiresRefresh = false;
-            var virtualFolders = _libraryManager.GetVirtualFolders()
-               .ToList();
+            var virtualFolders = _libraryManager.GetVirtualFolders();
 
 
             foreach (var virtualFolder in virtualFolders)
             foreach (var virtualFolder in virtualFolders)
             {
             {
@@ -460,7 +458,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
             if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
             {
             {
                 var tunerChannelId = tunerChannel.TunerChannelId;
                 var tunerChannelId = tunerChannel.TunerChannelId;
-                if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
+                if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
                     tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
                 }
                 }
@@ -620,8 +618,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
             if (existingTimer != null)
             if (existingTimer != null)
             {
             {
-                if (existingTimer.Status == RecordingStatus.Cancelled ||
-                    existingTimer.Status == RecordingStatus.Completed)
+                if (existingTimer.Status == RecordingStatus.Cancelled
+                    || existingTimer.Status == RecordingStatus.Completed)
                 {
                 {
                     existingTimer.Status = RecordingStatus.New;
                     existingTimer.Status = RecordingStatus.New;
                     existingTimer.IsManual = true;
                     existingTimer.IsManual = true;
@@ -913,18 +911,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
                 var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
 
 
-                List<ProgramInfo> programs;
-
                 if (epgChannel == null)
                 if (epgChannel == null)
                 {
                 {
                     _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
                     _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
-                    programs = new List<ProgramInfo>();
+                    continue;
                 }
                 }
-                else
-                {
-                    programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
+
+                List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
                            .ConfigureAwait(false)).ToList();
                            .ConfigureAwait(false)).ToList();
-                }
 
 
                 // Replace the value that came from the provider with a normalized value
                 // Replace the value that came from the provider with a normalized value
                 foreach (var program in programs)
                 foreach (var program in programs)
@@ -940,7 +934,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 }
                 }
             }
             }
 
 
-            return new List<ProgramInfo>();
+            return Enumerable.Empty<ProgramInfo>();
         }
         }
 
 
         private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
         private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
@@ -1292,7 +1286,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 _logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
                 _logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
 
 
-                _logger.LogInformation("Writing file to path: " + recordPath);
+                _logger.LogInformation("Writing file to: {Path}", recordPath);
 
 
                 Action onStarted = async () =>
                 Action onStarted = async () =>
                 {
                 {
@@ -1417,13 +1411,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
         private void TriggerRefresh(string path)
         private void TriggerRefresh(string path)
         {
         {
-            _logger.LogInformation("Triggering refresh on {path}", path);
+            _logger.LogInformation("Triggering refresh on {Path}", path);
 
 
             var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
             var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
 
 
             if (item != null)
             if (item != null)
             {
             {
-                _logger.LogInformation("Refreshing recording parent {path}", item.Path);
+                _logger.LogInformation("Refreshing recording parent {Path}", item.Path);
 
 
                 _providerManager.QueueRefresh(
                 _providerManager.QueueRefresh(
                     item.Id,
                     item.Id,
@@ -1512,8 +1506,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 DeleteLibraryItemsForTimers(timersToDelete);
                 DeleteLibraryItemsForTimers(timersToDelete);
 
 
-                var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
-                if (librarySeries == null)
+                if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries)
                 {
                 {
                     return;
                     return;
                 }
                 }
@@ -1667,7 +1660,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
                 _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
 
 
-                process.Exited += Process_Exited;
+                process.Exited += OnProcessExited;
                 process.Start();
                 process.Start();
             }
             }
             catch (Exception ex)
             catch (Exception ex)
@@ -1681,7 +1674,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
             return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
-        private void Process_Exited(object sender, EventArgs e)
+        private void OnProcessExited(object sender, EventArgs e)
         {
         {
             using (var process = (Process)sender)
             using (var process = (Process)sender)
             {
             {
@@ -2239,7 +2232,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             var enabledTimersForSeries = new List<TimerInfo>();
             var enabledTimersForSeries = new List<TimerInfo>();
             foreach (var timer in allTimers)
             foreach (var timer in allTimers)
             {
             {
-                var existingTimer = _timerProvider.GetTimer(timer.Id) 
+                var existingTimer = _timerProvider.GetTimer(timer.Id)
                     ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
                     ?? (string.IsNullOrWhiteSpace(timer.ProgramId)
                         ? null
                         ? null
                         : _timerProvider.GetTimerByProgramId(timer.ProgramId));
                         : _timerProvider.GetTimerByProgramId(timer.ProgramId));

+ 0 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -319,11 +319,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                     }
                 }
                 }
             }
             }
-            catch (ObjectDisposedException)
-            {
-                // TODO Investigate and properly fix.
-                // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
-            }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 _logger.LogError(ex, "Error reading ffmpeg recording log");
                 _logger.LogError(ex, "Error reading ffmpeg recording log");

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         /// <summary>
         /// <summary>
         /// Records the specified media source.
         /// Records the specified media source.
         /// </summary>
         /// </summary>
-        Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
+        Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
 
 
         string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
         string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
     }
     }

+ 5 - 7
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -23,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
         {
         }
         }
 
 
-        public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
+        public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired;
 
 
         public void RestartTimers()
         public void RestartTimers()
         {
         {
@@ -145,9 +143,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        private void TimerCallback(object state)
+        private void TimerCallback(object? state)
         {
         {
-            var timerId = (string)state;
+            var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state));
 
 
             var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
             var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
             if (timer != null)
             if (timer != null)
@@ -156,12 +154,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
             }
         }
         }
 
 
-        public TimerInfo GetTimer(string id)
+        public TimerInfo? GetTimer(string id)
         {
         {
             return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
             return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
         }
         }
 
 
-        public TimerInfo GetTimerByProgramId(string programId)
+        public TimerInfo? GetTimerByProgramId(string programId)
         {
         {
             return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
             return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
         }
         }

+ 2 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -295,11 +295,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 }
                 }
             }
             }
 
 
-            attributes.TryGetValue("tvg-name", out string name);
+            string name = nameInExtInf;
 
 
             if (string.IsNullOrWhiteSpace(name))
             if (string.IsNullOrWhiteSpace(name))
             {
             {
-                name = nameInExtInf;
+                attributes.TryGetValue("tvg-name", out name);
             }
             }
 
 
             if (string.IsNullOrWhiteSpace(name))
             if (string.IsNullOrWhiteSpace(name))

+ 1 - 1
Emby.Server.Implementations/Localization/Core/tr.json

@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Canlı TV",
     "HeaderLiveTV": "Canlı TV",
     "HeaderNextUp": "Gelecek Hafta",
     "HeaderNextUp": "Gelecek Hafta",
     "HeaderRecordingGroups": "Kayıt Grupları",
     "HeaderRecordingGroups": "Kayıt Grupları",
-    "HomeVideos": "Ev videoları",
+    "HomeVideos": "Ana sayfa videoları",
     "Inherit": "Devral",
     "Inherit": "Devral",
     "ItemAddedWithName": "{0} kütüphaneye eklendi",
     "ItemAddedWithName": "{0} kütüphaneye eklendi",
     "ItemRemovedWithName": "{0} kütüphaneden silindi",
     "ItemRemovedWithName": "{0} kütüphaneden silindi",

+ 1 - 0
Emby.Server.Implementations/Properties/AssemblyInfo.cs

@@ -16,6 +16,7 @@ using System.Runtime.InteropServices;
 [assembly: AssemblyCulture("")]
 [assembly: AssemblyCulture("")]
 [assembly: NeutralResourcesLanguage("en")]
 [assembly: NeutralResourcesLanguage("en")]
 [assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
 [assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
+[assembly: InternalsVisibleTo("Emby.Server.Implementations.Fuzz")]
 
 
 // Setting ComVisible to false makes the types in this assembly not visible
 // Setting ComVisible to false makes the types in this assembly not visible
 // to COM components.  If you need to access a type in this assembly from
 // to COM components.  If you need to access a type in this assembly from

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs

@@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
         public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
         {
         {
             var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
             var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
-            if (!retentionDays.HasValue || retentionDays <= 0)
+            if (!retentionDays.HasValue || retentionDays < 0)
             {
             {
                 throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
                 throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
             }
             }

+ 2 - 2
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
 
 
             if (paths != null && paths.Length > 0)
             if (paths != null && paths.Length > 0)
             {
             {
-                libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
+                libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
             }
             }
 
 
             await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
             await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
@@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
 
 
             try
             try
             {
             {
-                var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path };
+                var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null."));
 
 
                 _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
                 _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
             }
             }

+ 1 - 1
Jellyfin.Server/Jellyfin.Server.csproj

@@ -44,7 +44,7 @@
     <PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
     <PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
     <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
     <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
     <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" />
     <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" />
-    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
+    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.5" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 298 - 91
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -37,6 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding
             "ConstrainedHigh"
             "ConstrainedHigh"
         };
         };
 
 
+        private static readonly Version minVersionForCudaOverlay = new Version(4, 4);
+
         public EncodingHelper(
         public EncodingHelper(
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
             ISubtitleEncoder subtitleEncoder)
             ISubtitleEncoder subtitleEncoder)
@@ -106,17 +108,41 @@ namespace MediaBrowser.Controller.MediaEncoding
         private bool IsCudaSupported()
         private bool IsCudaSupported()
         {
         {
             return _mediaEncoder.SupportsHwaccel("cuda")
             return _mediaEncoder.SupportsHwaccel("cuda")
-                   && _mediaEncoder.SupportsFilter("scale_cuda", null)
-                   && _mediaEncoder.SupportsFilter("yadif_cuda", null);
+                   && _mediaEncoder.SupportsFilter("scale_cuda")
+                   && _mediaEncoder.SupportsFilter("yadif_cuda")
+                   && _mediaEncoder.SupportsFilter("hwupload_cuda");
         }
         }
 
 
-        private bool IsTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
+        private bool IsOpenclTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
         {
         {
             var videoStream = state.VideoStream;
             var videoStream = state.VideoStream;
-            return IsColorDepth10(state)
+            if (videoStream == null)
+            {
+                return false;
+            }
+
+            return options.EnableTonemapping
+                   && (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+                       || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+                   && IsColorDepth10(state)
                    && _mediaEncoder.SupportsHwaccel("opencl")
                    && _mediaEncoder.SupportsHwaccel("opencl")
-                   && options.EnableTonemapping
-                   && string.Equals(videoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase);
+                   && _mediaEncoder.SupportsFilter("tonemap_opencl");
+        }
+
+        private bool IsCudaTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
+        {
+            var videoStream = state.VideoStream;
+            if (videoStream == null)
+            {
+                return false;
+            }
+
+            return options.EnableTonemapping
+                   && (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+                       || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+                   && IsColorDepth10(state)
+                   && _mediaEncoder.SupportsHwaccel("cuda")
+                   && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapCudaName);
         }
         }
 
 
         private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
         private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
@@ -132,23 +158,25 @@ namespace MediaBrowser.Controller.MediaEncoding
             if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
             {
             {
                 // Limited to HEVC for now since the filter doesn't accept master data from VP9.
                 // Limited to HEVC for now since the filter doesn't accept master data from VP9.
-                return IsColorDepth10(state)
+                return options.EnableVppTonemapping
+                       && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+                       && IsColorDepth10(state)
                        && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
                        && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
                        && _mediaEncoder.SupportsHwaccel("vaapi")
                        && _mediaEncoder.SupportsHwaccel("vaapi")
-                       && options.EnableVppTonemapping
-                       && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase);
+                       && _mediaEncoder.SupportsFilter("tonemap_vaapi");
             }
             }
 
 
             // Hybrid VPP tonemapping for QSV with VAAPI
             // Hybrid VPP tonemapping for QSV with VAAPI
             if (OperatingSystem.IsLinux() && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
             if (OperatingSystem.IsLinux() && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
             {
             {
                 // Limited to HEVC for now since the filter doesn't accept master data from VP9.
                 // Limited to HEVC for now since the filter doesn't accept master data from VP9.
-                return IsColorDepth10(state)
+                return options.EnableVppTonemapping
+                       && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+                       && IsColorDepth10(state)
                        && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
                        && string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
                        && _mediaEncoder.SupportsHwaccel("vaapi")
                        && _mediaEncoder.SupportsHwaccel("vaapi")
-                       && _mediaEncoder.SupportsHwaccel("qsv")
-                       && options.EnableVppTonemapping
-                       && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase);
+                       && _mediaEncoder.SupportsFilter("tonemap_vaapi")
+                       && _mediaEncoder.SupportsHwaccel("qsv");
             }
             }
 
 
             // Native VPP tonemapping may come to QSV in the future.
             // Native VPP tonemapping may come to QSV in the future.
@@ -497,13 +525,16 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// Gets the input argument.
         /// Gets the input argument.
         /// </summary>
         /// </summary>
         /// <param name="state">Encoding state.</param>
         /// <param name="state">Encoding state.</param>
-        /// <param name="encodingOptions">Encoding options.</param>
+        /// <param name="options">Encoding options.</param>
         /// <returns>Input arguments.</returns>
         /// <returns>Input arguments.</returns>
-        public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions)
+        public string GetInputArgument(EncodingJobInfo state, EncodingOptions options)
         {
         {
             var arg = new StringBuilder();
             var arg = new StringBuilder();
-            var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
-            var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+            var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
+            var outputVideoCodec = GetVideoEncoder(state, options) ?? string.Empty;
+            var isWindows = OperatingSystem.IsWindows();
+            var isLinux = OperatingSystem.IsLinux();
+            var isMacOS = OperatingSystem.IsMacOS();
 #pragma warning disable CA1508 // Defaults to string.Empty
 #pragma warning disable CA1508 // Defaults to string.Empty
             var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
             var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
 #pragma warning restore CA1508
 #pragma warning restore CA1508
@@ -514,26 +545,24 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
             var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
             var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
             var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
-            var isWindows = OperatingSystem.IsWindows();
-            var isLinux = OperatingSystem.IsLinux();
-            var isMacOS = OperatingSystem.IsMacOS();
-            var isTonemappingSupported = IsTonemappingSupported(state, encodingOptions);
-            var isVppTonemappingSupported = IsVppTonemappingSupported(state, encodingOptions);
+            var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase);
+            var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+            var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
+            var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
 
 
             if (!IsCopyCodec(outputVideoCodec))
             if (!IsCopyCodec(outputVideoCodec))
             {
             {
                 if (state.IsVideoRequest
                 if (state.IsVideoRequest
                     && _mediaEncoder.SupportsHwaccel("vaapi")
                     && _mediaEncoder.SupportsHwaccel("vaapi")
-                    && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+                    && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     if (isVaapiDecoder)
                     if (isVaapiDecoder)
                     {
                     {
-                        if (isTonemappingSupported && !isVppTonemappingSupported)
+                        if (isOpenclTonemappingSupported && !isVppTonemappingSupported)
                         {
                         {
                            arg.Append("-init_hw_device vaapi=va:")
                            arg.Append("-init_hw_device vaapi=va:")
-                                .Append(encodingOptions.VaapiDevice)
-                                .Append(' ')
-                                .Append("-init_hw_device opencl=ocl@va ")
+                                .Append(options.VaapiDevice)
+                                .Append(" -init_hw_device opencl=ocl@va ")
                                 .Append("-hwaccel_device va ")
                                 .Append("-hwaccel_device va ")
                                 .Append("-hwaccel_output_format vaapi ")
                                 .Append("-hwaccel_output_format vaapi ")
                                 .Append("-filter_hw_device ocl ");
                                 .Append("-filter_hw_device ocl ");
@@ -542,14 +571,14 @@ namespace MediaBrowser.Controller.MediaEncoding
                         {
                         {
                             arg.Append("-hwaccel_output_format vaapi ")
                             arg.Append("-hwaccel_output_format vaapi ")
                                 .Append("-vaapi_device ")
                                 .Append("-vaapi_device ")
-                                .Append(encodingOptions.VaapiDevice)
+                                .Append(options.VaapiDevice)
                                 .Append(' ');
                                 .Append(' ');
                         }
                         }
                     }
                     }
                     else if (!isVaapiDecoder && isVaapiEncoder)
                     else if (!isVaapiDecoder && isVaapiEncoder)
                     {
                     {
                         arg.Append("-vaapi_device ")
                         arg.Append("-vaapi_device ")
-                            .Append(encodingOptions.VaapiDevice)
+                            .Append(options.VaapiDevice)
                             .Append(' ');
                             .Append(' ');
                     }
                     }
 
 
@@ -557,7 +586,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 }
 
 
                 if (state.IsVideoRequest
                 if (state.IsVideoRequest
-                    && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+                    && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
                     var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
 
 
@@ -593,9 +622,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                         else if (isVaapiDecoder && isVppTonemappingSupported)
                         else if (isVaapiDecoder && isVppTonemappingSupported)
                         {
                         {
                             arg.Append("-init_hw_device vaapi=va:")
                             arg.Append("-init_hw_device vaapi=va:")
-                                .Append(encodingOptions.VaapiDevice)
-                                .Append(' ')
-                                .Append("-init_hw_device qsv@va ")
+                                .Append(options.VaapiDevice)
+                                .Append(" -init_hw_device qsv@va ")
                                 .Append("-hwaccel_output_format vaapi ");
                                 .Append("-hwaccel_output_format vaapi ");
                         }
                         }
 
 
@@ -604,7 +632,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 }
 
 
                 if (state.IsVideoRequest
                 if (state.IsVideoRequest
-                    && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
+                    && string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
                     && isNvdecDecoder)
                     && isNvdecDecoder)
                 {
                 {
                     // Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562
                     // Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562
@@ -612,22 +640,31 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 }
 
 
                 if (state.IsVideoRequest
                 if (state.IsVideoRequest
-                    && ((string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
-                         && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder))
-                        || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
-                            && (isD3d11vaDecoder || isSwDecoder))))
+                    && ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
+                         && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder))))
                 {
                 {
-                    if (isTonemappingSupported)
+                    if (!isCudaTonemappingSupported && isOpenclTonemappingSupported)
                     {
                     {
                         arg.Append("-init_hw_device opencl=ocl:")
                         arg.Append("-init_hw_device opencl=ocl:")
-                            .Append(encodingOptions.OpenclDevice)
-                            .Append(' ')
-                            .Append("-filter_hw_device ocl ");
+                            .Append(options.OpenclDevice)
+                            .Append(" -filter_hw_device ocl ");
                     }
                     }
                 }
                 }
 
 
                 if (state.IsVideoRequest
                 if (state.IsVideoRequest
-                    && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+                    && string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
+                    && (isD3d11vaDecoder || isSwDecoder))
+                {
+                    if (isOpenclTonemappingSupported)
+                    {
+                        arg.Append("-init_hw_device opencl=ocl:")
+                            .Append(options.OpenclDevice)
+                            .Append(" -filter_hw_device ocl ");
+                    }
+                }
+
+                if (state.IsVideoRequest
+                    && string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     arg.Append("-hwaccel videotoolbox ");
                     arg.Append("-hwaccel videotoolbox ");
                 }
                 }
@@ -2012,14 +2049,18 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isQsvHevcEncoder = outputVideoCodec.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
             var isQsvHevcEncoder = outputVideoCodec.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
             var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
             var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
             var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
             var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
-            var isTonemappingSupported = IsTonemappingSupported(state, options);
-            var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
             var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
             var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
             var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
             var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
+            var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+            var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
+
+            var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
+            var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay;
+            var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
 
 
             // Tonemapping and burn-in graphical subtitles requires overlay_vaapi.
             // Tonemapping and burn-in graphical subtitles requires overlay_vaapi.
             // But it's still in ffmpeg mailing list. Disable it for now.
             // But it's still in ffmpeg mailing list. Disable it for now.
-            if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported)
+            if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported)
             {
             {
                 return GetOutputSizeParam(state, options, outputVideoCodec);
                 return GetOutputSizeParam(state, options, outputVideoCodec);
             }
             }
@@ -2045,13 +2086,22 @@ namespace MediaBrowser.Controller.MediaEncoding
                 if (!string.IsNullOrEmpty(videoSizeParam)
                 if (!string.IsNullOrEmpty(videoSizeParam)
                     && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported))
                     && !(isTonemappingSupportedOnQsv && isVppTonemappingSupported))
                 {
                 {
-                    // For QSV, feed it into hardware encoder now
+                    // upload graphical subtitle to QSV
                     if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
                     if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
                         || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
                         || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
                     {
                     {
                         videoSizeParam += ",hwupload=extra_hw_frames=64";
                         videoSizeParam += ",hwupload=extra_hw_frames=64";
                     }
                     }
                 }
                 }
+
+                if (!string.IsNullOrEmpty(videoSizeParam))
+                {
+                    // upload graphical subtitle to cuda
+                    if (isNvdecDecoder && isNvencEncoder && isCudaOverlaySupported && isCudaFormatConversionSupported)
+                    {
+                        videoSizeParam += ",hwupload_cuda";
+                    }
+                }
             }
             }
 
 
             var mapPrefix = state.SubtitleStream.IsExternal ?
             var mapPrefix = state.SubtitleStream.IsExternal ?
@@ -2064,9 +2114,9 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
             // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
             // Always put the scaler before the overlay for better performance
             // Always put the scaler before the overlay for better performance
-            var retStr = !outputSizeParam.IsEmpty
-                ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""
-                : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
+            var retStr = outputSizeParam.IsEmpty
+                ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""
+                : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
 
 
             // When the input may or may not be hardware VAAPI decodable
             // When the input may or may not be hardware VAAPI decodable
             if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
             if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
@@ -2077,9 +2127,9 @@ namespace MediaBrowser.Controller.MediaEncoding
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [base][sub]: SW overlay
                     [base][sub]: SW overlay
                 */
                 */
-                retStr = !outputSizeParam.IsEmpty
-                    ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""
-                    : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
+                retStr = outputSizeParam.IsEmpty
+                    ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""
+                    : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
             }
             }
 
 
             // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
             // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
@@ -2092,9 +2142,9 @@ namespace MediaBrowser.Controller.MediaEncoding
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [base][sub]: SW overlay
                     [base][sub]: SW overlay
                 */
                 */
-                retStr = !outputSizeParam.IsEmpty
-                    ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""
-                    : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
+                retStr = outputSizeParam.IsEmpty
+                    ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""
+                    : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
             }
             }
             else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
             else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
                      || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
                      || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
@@ -2111,16 +2161,25 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 }
                 else if (isLinux)
                 else if (isLinux)
                 {
                 {
-                    retStr = !outputSizeParam.IsEmpty
-                        ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\""
-                        : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
+                    retStr = outputSizeParam.IsEmpty
+                        ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""
+                        : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"";
                 }
                 }
             }
             }
             else if (isNvdecDecoder && isNvencEncoder)
             else if (isNvdecDecoder && isNvencEncoder)
             {
             {
-                retStr = !outputSizeParam.IsEmpty
-                    ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""
-                    : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"";
+                if (isCudaOverlaySupported && isCudaFormatConversionSupported)
+                {
+                    retStr = outputSizeParam.IsEmpty
+                        ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]scale_cuda=format=yuv420p[base];[base][sub]overlay_cuda\""
+                        : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_cuda\"";
+                }
+                else
+                {
+                    retStr = outputSizeParam.IsEmpty
+                        ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""
+                        : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"";
+                }
             }
             }
 
 
             return string.Format(
             return string.Format(
@@ -2217,11 +2276,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var isVaapiHevcEncoder = videoEncoder.Contains("hevc_vaapi", StringComparison.OrdinalIgnoreCase);
                 var isVaapiHevcEncoder = videoEncoder.Contains("hevc_vaapi", StringComparison.OrdinalIgnoreCase);
                 var isQsvH264Encoder = videoEncoder.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase);
                 var isQsvH264Encoder = videoEncoder.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase);
                 var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
                 var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
-                var isTonemappingSupported = IsTonemappingSupported(state, options);
+                var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
                 var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
                 var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
                 var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
                 var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
                 var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
                 var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
-                var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported))
+                var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported))
                     || (isTonemappingSupportedOnQsv && isVppTonemappingSupported);
                     || (isTonemappingSupportedOnQsv && isVppTonemappingSupported);
 
 
                 var outputPixFmt = "format=nv12";
                 var outputPixFmt = "format=nv12";
@@ -2272,15 +2331,23 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var outputWidth = width.Value;
                 var outputWidth = width.Value;
                 var outputHeight = height.Value;
                 var outputHeight = height.Value;
 
 
-                var isTonemappingSupported = IsTonemappingSupported(state, options);
+                var isNvencEncoder = videoEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
+                var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+                var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
                 var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase);
                 var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase);
-                var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")");
+                var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
+                var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay;
+                var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
+                var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
 
 
                 var outputPixFmt = string.Empty;
                 var outputPixFmt = string.Empty;
                 if (isCudaFormatConversionSupported)
                 if (isCudaFormatConversionSupported)
                 {
                 {
-                    outputPixFmt = "format=nv12";
-                    if (isTonemappingSupported && isTonemappingSupportedOnNvenc)
+                    outputPixFmt = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder)
+                        ? "format=yuv420p"
+                        : "format=nv12";
+                    if ((isOpenclTonemappingSupported || isCudaTonemappingSupported)
+                        && isTonemappingSupportedOnNvenc)
                     {
                     {
                         outputPixFmt = "format=p010";
                         outputPixFmt = "format=p010";
                     }
                     }
@@ -2558,16 +2625,21 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
             var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
             var isCuvidH264Decoder = videoDecoder.Contains("h264_cuvid", StringComparison.OrdinalIgnoreCase);
             var isCuvidH264Decoder = videoDecoder.Contains("h264_cuvid", StringComparison.OrdinalIgnoreCase);
             var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
             var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
+            var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase);
             var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
             var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
             var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
             var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
             var isLinux = OperatingSystem.IsLinux();
             var isLinux = OperatingSystem.IsLinux();
             var isColorDepth10 = IsColorDepth10(state);
             var isColorDepth10 = IsColorDepth10(state);
-            var isTonemappingSupported = IsTonemappingSupported(state, options);
-            var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
-            var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder);
+
+            var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder);
             var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder);
             var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder);
             var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
             var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
             var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
             var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
+            var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+            var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
+            var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
+            var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
+            var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= minVersionForCudaOverlay;
 
 
             var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
             var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
             var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
             var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
@@ -2579,19 +2651,25 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isScalingInAdvance = false;
             var isScalingInAdvance = false;
             var isCudaDeintInAdvance = false;
             var isCudaDeintInAdvance = false;
             var isHwuploadCudaRequired = false;
             var isHwuploadCudaRequired = false;
+            var isNoTonemapFilterApplied = true;
             var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
             var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
             var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
             var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
 
 
             // Add OpenCL tonemapping filter for NVENC/AMF/VAAPI.
             // Add OpenCL tonemapping filter for NVENC/AMF/VAAPI.
-            if (isTonemappingSupportedOnNvenc || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported))
+            if ((isTonemappingSupportedOnNvenc && !isCudaTonemappingSupported) || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported))
             {
             {
-                // Currently only with the use of NVENC decoder can we get a decent performance.
-                // Currently only the HEVC/H265 format is supported with NVDEC decoder.
                 // NVIDIA Pascal and Turing or higher are recommended.
                 // NVIDIA Pascal and Turing or higher are recommended.
                 // AMD Polaris and Vega or higher are recommended.
                 // AMD Polaris and Vega or higher are recommended.
                 // Intel Kaby Lake or newer is required.
                 // Intel Kaby Lake or newer is required.
-                if (isTonemappingSupported)
+                if (isOpenclTonemappingSupported)
                 {
                 {
+                    isNoTonemapFilterApplied = false;
+                    var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer);
+                    if (!string.IsNullOrEmpty(inputHdrParams))
+                    {
+                        filters.Add(inputHdrParams);
+                    }
+
                     var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
                     var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
 
 
                     if (options.TonemappingParam != 0)
                     if (options.TonemappingParam != 0)
@@ -2663,7 +2741,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                         filters.Add("hwdownload,format=p010");
                         filters.Add("hwdownload,format=p010");
                     }
                     }
 
 
-                    if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder)
+                    if (isNvdecDecoder
+                        || isCuvidHevcDecoder
+                        || isCuvidVp9Decoder
+                        || isSwDecoder
+                        || isD3d11vaDecoder)
                     {
                     {
                         // Upload the HDR10 or HLG data to the OpenCL device,
                         // Upload the HDR10 or HLG data to the OpenCL device,
                         // use tonemap_opencl filter for tone mapping,
                         // use tonemap_opencl filter for tone mapping,
@@ -2671,6 +2753,14 @@ namespace MediaBrowser.Controller.MediaEncoding
                         filters.Add("hwupload");
                         filters.Add("hwupload");
                     }
                     }
 
 
+                    // Fallback to hable if bt2390 is chosen but not supported in tonemap_opencl.
+                    var isBt2390SupportedInOpenclTonemap = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapOpenclBt2390);
+                    if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase)
+                        && !isBt2390SupportedInOpenclTonemap)
+                    {
+                        options.TonemappingAlgorithm = "hable";
+                    }
+
                     filters.Add(
                     filters.Add(
                         string.Format(
                         string.Format(
                             CultureInfo.InvariantCulture,
                             CultureInfo.InvariantCulture,
@@ -2682,7 +2772,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                             options.TonemappingParam,
                             options.TonemappingParam,
                             options.TonemappingRange));
                             options.TonemappingRange));
 
 
-                    if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder)
+                    if (isNvdecDecoder
+                        || isCuvidHevcDecoder
+                        || isCuvidVp9Decoder
+                        || isSwDecoder
+                        || isD3d11vaDecoder)
                     {
                     {
                         filters.Add("hwdownload");
                         filters.Add("hwdownload");
                         filters.Add("format=nv12");
                         filters.Add("format=nv12");
@@ -2698,12 +2792,18 @@ namespace MediaBrowser.Controller.MediaEncoding
                         // Reverse the data route from opencl to vaapi.
                         // Reverse the data route from opencl to vaapi.
                         filters.Add("hwmap=derive_device=vaapi:reverse=1");
                         filters.Add("hwmap=derive_device=vaapi:reverse=1");
                     }
                     }
+
+                    var outputSdrParams = GetOutputSdrParams(options.TonemappingRange);
+                    if (!string.IsNullOrEmpty(outputSdrParams))
+                    {
+                        filters.Add(outputSdrParams);
+                    }
                 }
                 }
             }
             }
 
 
             // When the input may or may not be hardware VAAPI decodable.
             // When the input may or may not be hardware VAAPI decodable.
             if ((isVaapiH264Encoder || isVaapiHevcEncoder)
             if ((isVaapiH264Encoder || isVaapiHevcEncoder)
-                && !(isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported)))
+                && !(isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported)))
             {
             {
                 filters.Add("format=nv12|vaapi");
                 filters.Add("format=nv12|vaapi");
                 filters.Add("hwupload");
                 filters.Add("hwupload");
@@ -2811,6 +2911,61 @@ namespace MediaBrowser.Controller.MediaEncoding
                         request.MaxHeight));
                         request.MaxHeight));
             }
             }
 
 
+            // Add Cuda tonemapping filter.
+            if (isNvdecDecoder && isCudaTonemappingSupported)
+            {
+                isNoTonemapFilterApplied = false;
+                var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer);
+                if (!string.IsNullOrEmpty(inputHdrParams))
+                {
+                    filters.Add(inputHdrParams);
+                }
+
+                var parameters = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder)
+                    ? "tonemap_cuda=format=yuv420p:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}"
+                    : "tonemap_cuda=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}";
+
+                if (options.TonemappingParam != 0)
+                {
+                    parameters += ":param={3}";
+                }
+
+                if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+                {
+                    parameters += ":range={4}";
+                }
+
+                filters.Add(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        parameters,
+                        options.TonemappingAlgorithm,
+                        options.TonemappingPeak,
+                        options.TonemappingDesat,
+                        options.TonemappingParam,
+                        options.TonemappingRange));
+
+                if (isLibX264Encoder
+                    || isLibX265Encoder
+                    || hasTextSubs
+                    || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder))
+                {
+                    if (isNvencEncoder)
+                    {
+                        isHwuploadCudaRequired = true;
+                    }
+
+                    filters.Add("hwdownload");
+                    filters.Add("format=nv12");
+                }
+
+                var outputSdrParams = GetOutputSdrParams(options.TonemappingRange);
+                if (!string.IsNullOrEmpty(outputSdrParams))
+                {
+                    filters.Add(outputSdrParams);
+                }
+            }
+
             // Add VPP tonemapping filter for VAAPI.
             // Add VPP tonemapping filter for VAAPI.
             // Full hardware based video post processing, faster than OpenCL but lacks fine tuning options.
             // Full hardware based video post processing, faster than OpenCL but lacks fine tuning options.
             if ((isTonemappingSupportedOnVaapi || isTonemappingSupportedOnQsv)
             if ((isTonemappingSupportedOnVaapi || isTonemappingSupportedOnQsv)
@@ -2820,10 +2975,10 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
             }
 
 
             // Another case is when using Nvenc decoder.
             // Another case is when using Nvenc decoder.
-            if (isNvdecDecoder && !isTonemappingSupported)
+            if (isNvdecDecoder && !isOpenclTonemappingSupported && !isCudaTonemappingSupported)
             {
             {
                 var codec = videoStream.Codec;
                 var codec = videoStream.Codec;
-                var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")");
+                var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
 
 
                 // Assert 10-bit hardware decodable
                 // Assert 10-bit hardware decodable
                 if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
                 if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2832,7 +2987,10 @@ namespace MediaBrowser.Controller.MediaEncoding
                 {
                 {
                     if (isCudaFormatConversionSupported)
                     if (isCudaFormatConversionSupported)
                     {
                     {
-                        if (isLibX264Encoder || isLibX265Encoder || hasSubs)
+                        if (isLibX264Encoder
+                            || isLibX265Encoder
+                            || hasTextSubs
+                            || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder))
                         {
                         {
                             if (isNvencEncoder)
                             if (isNvencEncoder)
                             {
                             {
@@ -2859,7 +3017,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 }
 
 
                 // Assert 8-bit hardware decodable
                 // Assert 8-bit hardware decodable
-                else if (!isColorDepth10 && (isLibX264Encoder || isLibX265Encoder || hasSubs))
+                else if (!isColorDepth10
+                         && (isLibX264Encoder
+                             || isLibX265Encoder
+                             || hasTextSubs
+                             || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder)))
                 {
                 {
                     if (isNvencEncoder)
                     if (isNvencEncoder)
                     {
                     {
@@ -2880,7 +3042,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 {
                 {
                     // Convert hw context from ocl to va.
                     // Convert hw context from ocl to va.
                     // For tonemapping and text subs burn-in.
                     // For tonemapping and text subs burn-in.
-                    if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported)
+                    if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported)
                     {
                     {
                         filters.Add("scale_vaapi");
                         filters.Add("scale_vaapi");
                     }
                     }
@@ -2926,6 +3088,17 @@ namespace MediaBrowser.Controller.MediaEncoding
                 filters.Add("hwupload_cuda");
                 filters.Add("hwupload_cuda");
             }
             }
 
 
+            // If no tonemap filter is applied,
+            // tag the video range as SDR to prevent the encoder from encoding HDR video.
+            if (isNoTonemapFilterApplied)
+            {
+                var outputSdrParams = GetOutputSdrParams(null);
+                if (!string.IsNullOrEmpty(outputSdrParams))
+                {
+                    filters.Add(outputSdrParams);
+                }
+            }
+
             var output = string.Empty;
             var output = string.Empty;
             if (filters.Count > 0)
             if (filters.Count > 0)
             {
             {
@@ -2938,6 +3111,36 @@ namespace MediaBrowser.Controller.MediaEncoding
             return output;
             return output;
         }
         }
 
 
+        public static string GetInputHdrParams(string colorTransfer)
+        {
+            if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+            {
+                // HLG
+                return "setparams=color_primaries=bt2020:color_trc=arib-std-b67:colorspace=bt2020nc";
+            }
+            else
+            {
+                // HDR10
+                return "setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc";
+            }
+        }
+
+        public static string GetOutputSdrParams(string tonemappingRange)
+        {
+            // SDR
+            if (string.Equals(tonemappingRange, "tv", StringComparison.OrdinalIgnoreCase))
+            {
+                return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=tv";
+            }
+
+            if (string.Equals(tonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
+            {
+                return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=pc";
+            }
+
+            return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709";
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the number of threads.
         /// Gets the number of threads.
         /// </summary>
         /// </summary>
@@ -3408,8 +3611,13 @@ namespace MediaBrowser.Controller.MediaEncoding
                 if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)
                 if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)
                     && IsVppTonemappingSupported(state, encodingOptions))
                     && IsVppTonemappingSupported(state, encodingOptions))
                 {
                 {
-                    // Since tonemap_vaapi only support HEVC for now, no need to check the codec again.
-                    return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10);
+                    var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+                    var isQsvEncoder = outputVideoCodec.Contains("qsv", StringComparison.OrdinalIgnoreCase);
+                    if (isQsvEncoder)
+                    {
+                        // Since tonemap_vaapi only support HEVC for now, no need to check the codec again.
+                        return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10);
+                    }
                 }
                 }
 
 
                 if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
@@ -3942,6 +4150,11 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             if (videoStream != null)
             if (videoStream != null)
             {
             {
+                if (videoStream.BitDepth.HasValue)
+                {
+                    return videoStream.BitDepth.Value == 10;
+                }
+
                 if (!string.IsNullOrEmpty(videoStream.PixelFormat))
                 if (!string.IsNullOrEmpty(videoStream.PixelFormat))
                 {
                 {
                     result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase);
                     result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase);
@@ -3961,12 +4174,6 @@ namespace MediaBrowser.Controller.MediaEncoding
                         return true;
                         return true;
                     }
                     }
                 }
                 }
-
-                result = (videoStream.BitDepth ?? 8) == 10;
-                if (result)
-                {
-                    return true;
-                }
             }
             }
 
 
             return result;
             return result;

+ 23 - 0
MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs

@@ -0,0 +1,23 @@
+namespace MediaBrowser.Controller.MediaEncoding
+{
+    /// <summary>
+    /// Enum FilterOptionType.
+    /// </summary>
+    public enum FilterOptionType
+    {
+        /// <summary>
+        /// The scale_cuda_format.
+        /// </summary>
+        ScaleCudaFormat = 0,
+
+        /// <summary>
+        /// The tonemap_cuda_name.
+        /// </summary>
+        TonemapCudaName = 1,
+
+        /// <summary>
+        /// The tonemap_opencl_bt2390.
+        /// </summary>
+        TonemapOpenclBt2390 = 2
+    }
+}

+ 13 - 7
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -10,7 +10,6 @@ using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.System;
 
 
 namespace MediaBrowser.Controller.MediaEncoding
 namespace MediaBrowser.Controller.MediaEncoding
 {
 {
@@ -19,11 +18,6 @@ namespace MediaBrowser.Controller.MediaEncoding
     /// </summary>
     /// </summary>
     public interface IMediaEncoder : ITranscoderSupport
     public interface IMediaEncoder : ITranscoderSupport
     {
     {
-        /// <summary>
-        /// Gets location of the discovered FFmpeg tool.
-        /// </summary>
-        FFmpegLocation EncoderLocation { get; }
-
         /// <summary>
         /// <summary>
         /// Gets the encoder path.
         /// Gets the encoder path.
         /// </summary>
         /// </summary>
@@ -55,9 +49,21 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// Whether given filter is supported.
         /// Whether given filter is supported.
         /// </summary>
         /// </summary>
         /// <param name="filter">The filter.</param>
         /// <param name="filter">The filter.</param>
+        /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
+        bool SupportsFilter(string filter);
+
+        /// <summary>
+        /// Whether filter is supported with the given option.
+        /// </summary>
         /// <param name="option">The option.</param>
         /// <param name="option">The option.</param>
         /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
         /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
-        bool SupportsFilter(string filter, string option);
+        bool SupportsFilterWithOption(FilterOptionType option);
+
+        /// <summary>
+        /// Get the version of media encoder.
+        /// </summary>
+        /// <returns>The version of media encoder.</returns>
+        Version GetMediaEncoderVersion();
 
 
         /// <summary>
         /// <summary>
         /// Extracts the audio image.
         /// Extracts the audio image.

+ 94 - 12
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -12,8 +12,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
 {
 {
     public class EncoderValidator
     public class EncoderValidator
     {
     {
-        private const string DefaultEncoderPath = "ffmpeg";
-
         private static readonly string[] _requiredDecoders = new[]
         private static readonly string[] _requiredDecoders = new[]
         {
         {
             "h264",
             "h264",
@@ -89,6 +87,24 @@ namespace MediaBrowser.MediaEncoding.Encoder
             "hevc_videotoolbox"
             "hevc_videotoolbox"
         };
         };
 
 
+        private static readonly string[] _requiredFilters = new[]
+        {
+            "scale_cuda",
+            "yadif_cuda",
+            "hwupload_cuda",
+            "overlay_cuda",
+            "tonemap_cuda",
+            "tonemap_opencl",
+            "tonemap_vaapi",
+        };
+
+        private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
+        {
+            { 0, new string[] { "scale_cuda", "Output format (default \"same\")" } },
+            { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
+            { 2, new string[] { "tonemap_opencl", "bt2390" } }
+        };
+
         // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
         // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
         private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
         private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
         {
         {
@@ -106,7 +122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
         private readonly string _encoderPath;
         private readonly string _encoderPath;
 
 
-        public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath)
+        public EncoderValidator(ILogger logger, string encoderPath)
         {
         {
             _logger = logger;
             _logger = logger;
             _encoderPath = encoderPath;
             _encoderPath = encoderPath;
@@ -156,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
             }
 
 
             // Work out what the version under test is
             // Work out what the version under test is
-            var version = GetFFmpegVersion(versionOutput);
+            var version = GetFFmpegVersionInternal(versionOutput);
 
 
             _logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown");
             _logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown");
 
 
@@ -200,6 +216,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
         public IEnumerable<string> GetHwaccels() => GetHwaccelTypes();
         public IEnumerable<string> GetHwaccels() => GetHwaccelTypes();
 
 
+        public IEnumerable<string> GetFilters() => GetFFmpegFilters();
+
+        public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
+
+        public Version? GetFFmpegVersion()
+        {
+            string output;
+            try
+            {
+                output = GetProcessOutput(_encoderPath, "-version");
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error validating encoder");
+                return null;
+            }
+
+            if (string.IsNullOrWhiteSpace(output))
+            {
+                _logger.LogError("FFmpeg validation: The process returned no result");
+                return null;
+            }
+
+            _logger.LogDebug("ffmpeg output: {Output}", output);
+
+            return GetFFmpegVersionInternal(output);
+        }
+
         /// <summary>
         /// <summary>
         /// Using the output from "ffmpeg -version" work out the FFmpeg version.
         /// Using the output from "ffmpeg -version" work out the FFmpeg version.
         /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
         /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
@@ -208,7 +252,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// </summary>
         /// </summary>
         /// <param name="output">The output from "ffmpeg -version".</param>
         /// <param name="output">The output from "ffmpeg -version".</param>
         /// <returns>The FFmpeg version.</returns>
         /// <returns>The FFmpeg version.</returns>
-        internal Version? GetFFmpegVersion(string output)
+        internal Version? GetFFmpegVersionInternal(string output)
         {
         {
             // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
             // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
             var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
             var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
@@ -297,9 +341,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return found;
             return found;
         }
         }
 
 
-        public bool CheckFilter(string filter, string option)
+        public bool CheckFilterWithOption(string filter, string option)
         {
         {
-            if (string.IsNullOrEmpty(filter))
+            if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
             {
             {
                 return false;
                 return false;
             }
             }
@@ -317,11 +361,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
             if (output.Contains("Filter " + filter, StringComparison.Ordinal))
             if (output.Contains("Filter " + filter, StringComparison.Ordinal))
             {
             {
-                if (string.IsNullOrEmpty(option))
-                {
-                    return true;
-                }
-
                 return output.Contains(option, StringComparison.Ordinal);
                 return output.Contains(option, StringComparison.Ordinal);
             }
             }
 
 
@@ -362,6 +401,49 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return found;
             return found;
         }
         }
 
 
+        private IEnumerable<string> GetFFmpegFilters()
+        {
+            string output;
+            try
+            {
+                output = GetProcessOutput(_encoderPath, "-filters");
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error detecting available filters");
+                return Enumerable.Empty<string>();
+            }
+
+            if (string.IsNullOrWhiteSpace(output))
+            {
+                return Enumerable.Empty<string>();
+            }
+
+            var found = Regex
+                .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline)
+                .Cast<Match>()
+                .Select(x => x.Groups["filter"].Value)
+                .Where(x => _requiredFilters.Contains(x));
+
+            _logger.LogInformation("Available filters: {Filters}", found);
+
+            return found;
+        }
+
+        private IDictionary<int, bool> GetFFmpegFiltersWithOption()
+        {
+            IDictionary<int, bool> dict = new Dictionary<int, bool>();
+            for (int i = 0; i < _filterOptionsDict.Count; i++)
+            {
+                if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2)
+                {
+                    dict.Add(i, CheckFilterWithOption(val[0], val[1]));
+                }
+            }
+
+            return dict;
+        }
+
         private string GetProcessOutput(string path, string arguments)
         private string GetProcessOutput(string path, string arguments)
         {
         {
             using (var process = new Process()
             using (var process = new Process()

+ 69 - 89
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -23,7 +23,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.System;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -66,10 +65,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
         private List<string> _encoders = new List<string>();
         private List<string> _encoders = new List<string>();
         private List<string> _decoders = new List<string>();
         private List<string> _decoders = new List<string>();
         private List<string> _hwaccels = new List<string>();
         private List<string> _hwaccels = new List<string>();
+        private List<string> _filters = new List<string>();
+        private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
 
 
+        private Version _ffmpegVersion = null;
         private string _ffmpegPath = string.Empty;
         private string _ffmpegPath = string.Empty;
         private string _ffprobePath;
         private string _ffprobePath;
-        private int threads;
+        private int _threads;
 
 
         public MediaEncoder(
         public MediaEncoder(
             ILogger<MediaEncoder> logger,
             ILogger<MediaEncoder> logger,
@@ -89,9 +91,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <inheritdoc />
         /// <inheritdoc />
         public string EncoderPath => _ffmpegPath;
         public string EncoderPath => _ffmpegPath;
 
 
-        /// <inheritdoc />
-        public FFmpegLocation EncoderLocation { get; private set; }
-
         /// <summary>
         /// <summary>
         /// Run at startup or if the user removes a Custom path from transcode page.
         /// Run at startup or if the user removes a Custom path from transcode page.
         /// Sets global variables FFmpegPath.
         /// Sets global variables FFmpegPath.
@@ -100,20 +99,23 @@ namespace MediaBrowser.MediaEncoding.Encoder
         public void SetFFmpegPath()
         public void SetFFmpegPath()
         {
         {
             // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
             // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
-            if (!ValidatePath(_configurationManager.GetEncodingOptions().EncoderAppPath, FFmpegLocation.Custom))
+            var ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
+            if (string.IsNullOrEmpty(ffmpegPath))
             {
             {
                 // 2) Check if the --ffmpeg CLI switch has been given
                 // 2) Check if the --ffmpeg CLI switch has been given
-                if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument))
+                ffmpegPath = _startupOptionFFmpegPath;
+                if (string.IsNullOrEmpty(ffmpegPath))
                 {
                 {
-                    // 3) Search system $PATH environment variable for valid FFmpeg
-                    if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System))
-                    {
-                        EncoderLocation = FFmpegLocation.NotFound;
-                        _ffmpegPath = null;
-                    }
+                    // 3) Check "ffmpeg"
+                    ffmpegPath = "ffmpeg";
                 }
                 }
             }
             }
 
 
+            if (!ValidatePath(ffmpegPath))
+            {
+                _ffmpegPath = null;
+            }
+
             // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
             // Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
             var config = _configurationManager.GetEncodingOptions();
             var config = _configurationManager.GetEncodingOptions();
             config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
             config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
@@ -130,11 +132,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
                 SetAvailableDecoders(validator.GetDecoders());
                 SetAvailableDecoders(validator.GetDecoders());
                 SetAvailableEncoders(validator.GetEncoders());
                 SetAvailableEncoders(validator.GetEncoders());
+                SetAvailableFilters(validator.GetFilters());
+                SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
                 SetAvailableHwaccels(validator.GetHwaccels());
                 SetAvailableHwaccels(validator.GetHwaccels());
-                threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
+                SetMediaEncoderVersion(validator);
+
+                _threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
             }
             }
 
 
-            _logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty);
+            _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -153,15 +159,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
             {
             {
                 throw new ArgumentException("Unexpected pathType value");
                 throw new ArgumentException("Unexpected pathType value");
             }
             }
-            else if (string.IsNullOrWhiteSpace(path))
+
+            if (string.IsNullOrWhiteSpace(path))
             {
             {
                 // User had cleared the custom path in UI
                 // User had cleared the custom path in UI
                 newPath = string.Empty;
                 newPath = string.Empty;
             }
             }
-            else if (File.Exists(path))
-            {
-                newPath = path;
-            }
             else if (Directory.Exists(path))
             else if (Directory.Exists(path))
             {
             {
                 // Given path is directory, so resolve down to filename
                 // Given path is directory, so resolve down to filename
@@ -169,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
             }
             else
             else
             {
             {
-                throw new ResourceNotFoundException();
+                newPath = path;
             }
             }
 
 
             // Write the new ffmpeg path to the xml as <EncoderAppPath>
             // Write the new ffmpeg path to the xml as <EncoderAppPath>
@@ -184,37 +187,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
         /// <summary>
         /// <summary>
         /// Validates the supplied FQPN to ensure it is a ffmpeg utility.
         /// Validates the supplied FQPN to ensure it is a ffmpeg utility.
-        /// If checks pass, global variable FFmpegPath and EncoderLocation are updated.
+        /// If checks pass, global variable FFmpegPath is updated.
         /// </summary>
         /// </summary>
         /// <param name="path">FQPN to test.</param>
         /// <param name="path">FQPN to test.</param>
-        /// <param name="location">Location (External, Custom, System) of tool.</param>
         /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
-        private bool ValidatePath(string path, FFmpegLocation location)
+        private bool ValidatePath(string path)
         {
         {
-            bool rc = false;
-
-            if (!string.IsNullOrEmpty(path))
+            if (string.IsNullOrEmpty(path))
             {
             {
-                if (File.Exists(path))
-                {
-                    rc = new EncoderValidator(_logger, path).ValidateVersion();
-
-                    if (!rc)
-                    {
-                        _logger.LogWarning("FFmpeg: {Location}: Failed version check: {Path}", location, path);
-                    }
+                return false;
+            }
 
 
-                    _ffmpegPath = path;
-                    EncoderLocation = location;
-                    return true;
-                }
-                else
-                {
-                    _logger.LogWarning("FFmpeg: {Location}: File not found: {Path}", location, path);
-                }
+            bool rc = new EncoderValidator(_logger, path).ValidateVersion();
+            if (!rc)
+            {
+                _logger.LogWarning("FFmpeg: Failed version check: {Path}", path);
+                return false;
             }
             }
 
 
-            return rc;
+            _ffmpegPath = path;
+            return true;
         }
         }
 
 
         private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
         private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
@@ -235,34 +227,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Search the system $PATH environment variable looking for given filename.
-        /// </summary>
-        /// <param name="fileName">The filename.</param>
-        /// <returns>The full path to the file.</returns>
-        private string ExistsOnSystemPath(string fileName)
-        {
-            var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true);
-            if (!string.IsNullOrEmpty(inJellyfinPath))
-            {
-                return inJellyfinPath;
-            }
-
-            var values = Environment.GetEnvironmentVariable("PATH");
-
-            foreach (var path in values.Split(Path.PathSeparator))
-            {
-                var candidatePath = GetEncoderPathFromDirectory(path, fileName);
-
-                if (!string.IsNullOrEmpty(candidatePath))
-                {
-                    return candidatePath;
-                }
-            }
-
-            return null;
-        }
-
         public void SetAvailableEncoders(IEnumerable<string> list)
         public void SetAvailableEncoders(IEnumerable<string> list)
         {
         {
             _encoders = list.ToList();
             _encoders = list.ToList();
@@ -278,6 +242,21 @@ namespace MediaBrowser.MediaEncoding.Encoder
             _hwaccels = list.ToList();
             _hwaccels = list.ToList();
         }
         }
 
 
+        public void SetAvailableFilters(IEnumerable<string> list)
+        {
+            _filters = list.ToList();
+        }
+
+        public void SetAvailableFiltersWithOption(IDictionary<int, bool> dict)
+        {
+            _filtersWithOption = dict;
+        }
+
+        public void SetMediaEncoderVersion(EncoderValidator validator)
+        {
+            _ffmpegVersion = validator.GetFFmpegVersion();
+        }
+
         public bool SupportsEncoder(string encoder)
         public bool SupportsEncoder(string encoder)
         {
         {
             return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
             return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
@@ -293,17 +272,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
             return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
         }
         }
 
 
-        public bool SupportsFilter(string filter, string option)
+        public bool SupportsFilter(string filter)
         {
         {
-            if (_ffmpegPath != null)
+            return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
+        }
+
+        public bool SupportsFilterWithOption(FilterOptionType option)
+        {
+            if (_filtersWithOption.TryGetValue((int)option, out var val))
             {
             {
-                var validator = new EncoderValidator(_logger, _ffmpegPath);
-                return validator.CheckFilter(filter, option);
+                return val;
             }
             }
 
 
             return false;
             return false;
         }
         }
 
 
+        public Version GetMediaEncoderVersion()
+        {
+            return _ffmpegVersion;
+        }
+
         public bool CanEncodeToAudioCodec(string codec)
         public bool CanEncodeToAudioCodec(string codec)
         {
         {
             if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
@@ -394,7 +382,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var args = extractChapters
             var args = extractChapters
                 ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
                 ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
                 : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
                 : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
-            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim();
+            args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
 
 
             var process = new Process
             var process = new Process
             {
             {
@@ -503,15 +491,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         {
         {
             var inputArgument = GetInputArgument(inputFile, mediaSource);
             var inputArgument = GetInputArgument(inputFile, mediaSource);
 
 
-            if (isAudio)
-            {
-                if (imageStreamIndex.HasValue && imageStreamIndex.Value > 0)
-                {
-                    // It seems for audio files we need to subtract 1 (for the audio stream??)
-                    imageStreamIndex = imageStreamIndex.Value - 1;
-                }
-            }
-            else
+            if (!isAudio)
             {
             {
                 // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter.
                 // The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter.
                 try
                 try
@@ -582,7 +562,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 _ => string.Empty
                 _ => string.Empty
             };
             };
 
 
-            var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
+            var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
 
 
             var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase);
             var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase);
             if (enableHdrExtraction)
             if (enableHdrExtraction)
@@ -615,7 +595,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 }
                 }
             }
             }
 
 
-            var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads);
+            var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads);
 
 
             if (offset.HasValue)
             if (offset.HasValue)
             {
             {
@@ -728,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             Directory.CreateDirectory(targetDirectory);
             Directory.CreateDirectory(targetDirectory);
             var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
             var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
 
 
-            var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads);
+            var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, _threads);
 
 
             if (!string.IsNullOrWhiteSpace(container))
             if (!string.IsNullOrWhiteSpace(container))
             {
             {

+ 17 - 0
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -740,6 +740,23 @@ namespace MediaBrowser.MediaEncoding.Probing
                     stream.BitDepth = streamInfo.BitsPerRawSample;
                     stream.BitDepth = streamInfo.BitsPerRawSample;
                 }
                 }
 
 
+                if (!stream.BitDepth.HasValue)
+                {
+                    if (!string.IsNullOrEmpty(streamInfo.PixelFormat)
+                        && streamInfo.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase))
+                    {
+                        stream.BitDepth = 10;
+                    }
+
+                    if (!string.IsNullOrEmpty(streamInfo.Profile)
+                        && (streamInfo.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
+                            || streamInfo.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)
+                            || streamInfo.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase)))
+                    {
+                        stream.BitDepth = 10;
+                    }
+                }
+
                 // stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
                 // stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
                 //    string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
                 //    string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
                 //    string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
                 //    string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);

+ 6 - 2
MediaBrowser.Model/Configuration/MediaPathInfo.cs

@@ -1,12 +1,16 @@
-#nullable disable
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 namespace MediaBrowser.Model.Configuration
 namespace MediaBrowser.Model.Configuration
 {
 {
     public class MediaPathInfo
     public class MediaPathInfo
     {
     {
+        public MediaPathInfo(string path)
+        {
+            Path = path;
+        }
+
         public string Path { get; set; }
         public string Path { get; set; }
 
 
-        public string NetworkPath { get; set; }
+        public string? NetworkPath { get; set; }
     }
     }
 }
 }

+ 5 - 0
MediaBrowser.Model/Entities/MediaStream.cs

@@ -255,6 +255,11 @@ namespace MediaBrowser.Model.Entities
                             attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced);
                             attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced);
                         }
                         }
 
 
+                        if (!string.IsNullOrEmpty(Codec))
+                        {
+                            attributes.Add(Codec.ToUpperInvariant());
+                        }
+
                         if (!string.IsNullOrEmpty(Title))
                         if (!string.IsNullOrEmpty(Title))
                         {
                         {
                             var result = new StringBuilder(Title);
                             var result = new StringBuilder(Title);

+ 1 - 0
MediaBrowser.Model/System/SystemInfo.cs

@@ -133,6 +133,7 @@ namespace MediaBrowser.Model.System
         [Obsolete("This should be handled by the package manager")]
         [Obsolete("This should be handled by the package manager")]
         public bool HasUpdateAvailable { get; set; }
         public bool HasUpdateAvailable { get; set; }
 
 
+        [Obsolete("This isn't set correctly anymore")]
         public FFmpegLocation EncoderLocation { get; set; }
         public FFmpegLocation EncoderLocation { get; set; }
 
 
         public Architecture SystemArchitecture { get; set; }
         public Architecture SystemArchitecture { get; set; }

+ 1 - 17
MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs

@@ -88,22 +88,6 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
             if (imageStream != null)
             if (imageStream != null)
             {
             {
-                // Instead of using the raw stream index, we need to use nth video/embedded image stream
-                var videoIndex = -1;
-                foreach (var mediaStream in mediaStreams)
-                {
-                    if (mediaStream.Type == MediaStreamType.Video ||
-                        mediaStream.Type == MediaStreamType.EmbeddedImage)
-                    {
-                        videoIndex++;
-                    }
-
-                    if (mediaStream == imageStream)
-                    {
-                        break;
-                    }
-                }
-
                 MediaSourceInfo mediaSource = new MediaSourceInfo
                 MediaSourceInfo mediaSource = new MediaSourceInfo
                 {
                 {
                     VideoType = item.VideoType,
                     VideoType = item.VideoType,
@@ -111,7 +95,7 @@ namespace MediaBrowser.Providers.MediaInfo
                     Protocol = item.PathProtocol.Value,
                     Protocol = item.PathProtocol.Value,
                 };
                 };
 
 
-                extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false);
+                extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false);
             }
             }
             else
             else
             {
             {

+ 2 - 2
MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs

@@ -15,8 +15,8 @@ namespace MediaBrowser.XbmcMetadata.Configuration
             {
             {
                 new ConfigurationStore
                 new ConfigurationStore
                 {
                 {
-                     ConfigurationType = typeof(XbmcMetadataOptions),
-                     Key = "xbmcmetadata"
+                    ConfigurationType = typeof(XbmcMetadataOptions),
+                    Key = "xbmcmetadata"
                 }
                 }
             };
             };
         }
         }

+ 70 - 52
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

@@ -779,59 +779,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 
 
                 case "thumb":
                 case "thumb":
                     {
                     {
-                        var artType = reader.GetAttribute("aspect");
-                        var val = reader.ReadElementContentAsString();
-
-                        // skip:
-                        // - empty aspect tag
-                        // - empty uri
-                        // - tag containing '.' because we can't set images for seasons, episodes or movie sets within series or movies
-                        if (string.IsNullOrEmpty(artType) || string.IsNullOrEmpty(val) || artType.Contains('.', StringComparison.Ordinal))
-                        {
-                            break;
-                        }
-
-                        ImageType imageType = GetImageType(artType);
-
-                        if (!Uri.TryCreate(val, UriKind.Absolute, out var uri))
-                        {
-                            Logger.LogError("Image location {Path} specified in nfo file for {ItemName} is not a valid URL or file path.", val, item.Name);
-                            break;
-                        }
-
-                        if (uri.IsFile)
-                        {
-                            // only allow one item of each type
-                            if (itemResult.Images.Any(x => x.Type == imageType))
-                            {
-                                break;
-                            }
-
-                            var fileSystemMetadata = _directoryService.GetFile(val);
-                            // non existing file returns null
-                            if (fileSystemMetadata == null || !fileSystemMetadata.Exists)
-                            {
-                                Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, item.Name);
-                                break;
-                            }
-
-                            itemResult.Images.Add(new LocalImageInfo()
-                            {
-                                FileInfo = fileSystemMetadata,
-                                Type = imageType
-                            });
-                        }
-                        else
-                        {
-                            // only allow one item of each type
-                            if (itemResult.RemoteImages.Any(x => x.type == imageType))
-                            {
-                                break;
-                            }
-
-                            itemResult.RemoteImages.Add((uri.ToString(), imageType));
-                        }
+                        FetchThumbNode(reader, itemResult);
+                        break;
+                    }
 
 
+                case "fanart":
+                    {
+                        var subtree = reader.ReadSubtree();
+                        subtree.ReadToDescendant("thumb");
+                        FetchThumbNode(subtree, itemResult);
                         break;
                         break;
                     }
                     }
 
 
@@ -854,6 +810,68 @@ namespace MediaBrowser.XbmcMetadata.Parsers
             }
             }
         }
         }
 
 
+        private void FetchThumbNode(XmlReader reader, MetadataResult<T> itemResult)
+        {
+            var artType = reader.GetAttribute("aspect");
+            var val = reader.ReadElementContentAsString();
+
+            // artType is null if the thumb node is a child of the fanart tag
+            // -> set image type to fanart
+            if (string.IsNullOrWhiteSpace(artType))
+            {
+                artType = "fanart";
+            }
+
+            // skip:
+            // - empty uri
+            // - tag containing '.' because we can't set images for seasons, episodes or movie sets within series or movies
+            if (string.IsNullOrEmpty(val) || artType.Contains('.', StringComparison.Ordinal))
+            {
+                return;
+            }
+
+            ImageType imageType = GetImageType(artType);
+
+            if (!Uri.TryCreate(val, UriKind.Absolute, out var uri))
+            {
+                Logger.LogError("Image location {Path} specified in nfo file for {ItemName} is not a valid URL or file path.", val, itemResult.Item.Name);
+                return;
+            }
+
+            if (uri.IsFile)
+            {
+                // only allow one item of each type
+                if (itemResult.Images.Any(x => x.Type == imageType))
+                {
+                    return;
+                }
+
+                var fileSystemMetadata = _directoryService.GetFile(val);
+                // non existing file returns null
+                if (fileSystemMetadata == null || !fileSystemMetadata.Exists)
+                {
+                    Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, itemResult.Item.Name);
+                    return;
+                }
+
+                itemResult.Images.Add(new LocalImageInfo()
+                {
+                    FileInfo = fileSystemMetadata,
+                    Type = imageType
+                });
+            }
+            else
+            {
+                // only allow one item of each type
+                if (itemResult.RemoteImages.Any(x => x.type == imageType))
+                {
+                    return;
+                }
+
+                itemResult.RemoteImages.Add((uri.ToString(), imageType));
+            }
+        }
+
         private void FetchFromFileInfoNode(XmlReader reader, T item)
         private void FetchFromFileInfoNode(XmlReader reader, T item)
         {
         {
             reader.MoveToContent();
             reader.MoveToContent();

+ 7 - 0
fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj

@@ -12,6 +12,13 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="AutoFixture" Version="4.17.0" />
+    <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+    <PackageReference Include="Moq" Version="4.16.1" />
     <PackageReference Include="SharpFuzz" Version="1.6.2" />
     <PackageReference Include="SharpFuzz" Version="1.6.2" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 30 - 0
fuzz/Emby.Server.Implementations.Fuzz/Program.cs

@@ -1,5 +1,12 @@
 using System;
 using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using Moq;
 using SharpFuzz;
 using SharpFuzz;
 
 
 namespace Emby.Server.Implementations.Fuzz
 namespace Emby.Server.Implementations.Fuzz
@@ -11,6 +18,7 @@ namespace Emby.Server.Implementations.Fuzz
             switch (args[0])
             switch (args[0])
             {
             {
                 case "PathExtensions.TryReplaceSubPath": Run(PathExtensions_TryReplaceSubPath); return;
                 case "PathExtensions.TryReplaceSubPath": Run(PathExtensions_TryReplaceSubPath); return;
+                case "SqliteItemRepository.ItemImageInfoFromValueString": Run(SqliteItemRepository_ItemImageInfoFromValueString); return;
                 default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}");
                 default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}");
             }
             }
         }
         }
@@ -28,5 +36,27 @@ namespace Emby.Server.Implementations.Fuzz
 
 
             _ = PathExtensions.TryReplaceSubPath(parts[0], parts[1], parts[2], out _);
             _ = PathExtensions.TryReplaceSubPath(parts[0], parts[1], parts[2], out _);
         }
         }
+
+        private static void SqliteItemRepository_ItemImageInfoFromValueString(string data)
+        {
+            var sqliteItemRepository = MockSqliteItemRepository();
+            sqliteItemRepository.ItemImageInfoFromValueString(data);
+        }
+
+        private static SqliteItemRepository MockSqliteItemRepository()
+        {
+            const string VirtualMetaDataPath = "%MetadataPath%";
+            const string MetaDataPath = "/meta/data/path";
+
+            var appHost = new Mock<IServerApplicationHost>();
+            appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>()))
+                .Returns((string x) => x.Replace(VirtualMetaDataPath, MetaDataPath, StringComparison.Ordinal));
+            appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
+                .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal));
+
+            IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+            fixture.Inject(appHost);
+            return fixture.Create<SqliteItemRepository>();
+        }
     }
     }
 }
 }

+ 1 - 0
fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt

@@ -0,0 +1 @@
+/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN

+ 1 - 1
tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj

@@ -16,7 +16,7 @@
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="coverlet.collector" Version="3.1.0" />
     <PackageReference Include="coverlet.collector" Version="3.1.0" />
-    <PackageReference Include="FsCheck.Xunit" Version="2.16.0" />
+    <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code Analyzers -->
   <!-- Code Analyzers -->

+ 1 - 1
tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj

@@ -17,7 +17,7 @@
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>
     </PackageReference>
-    <PackageReference Include="FsCheck.Xunit" Version="2.16.0" />
+    <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code Analyzers -->
   <!-- Code Analyzers -->

+ 8 - 4
tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs

@@ -9,15 +9,18 @@ namespace Jellyfin.MediaEncoding.Tests
 {
 {
     public class EncoderValidatorTests
     public class EncoderValidatorTests
     {
     {
+        private readonly EncoderValidator _encoderValidator = new EncoderValidator(new NullLogger<EncoderValidatorTests>(), "ffmpeg");
+
         [Theory]
         [Theory]
         [ClassData(typeof(GetFFmpegVersionTestData))]
         [ClassData(typeof(GetFFmpegVersionTestData))]
         public void GetFFmpegVersionTest(string versionOutput, Version? version)
         public void GetFFmpegVersionTest(string versionOutput, Version? version)
         {
         {
-            var val = new EncoderValidator(new NullLogger<EncoderValidatorTests>());
-            Assert.Equal(version, val.GetFFmpegVersion(versionOutput));
+            Assert.Equal(version, _encoderValidator.GetFFmpegVersionInternal(versionOutput));
         }
         }
 
 
         [Theory]
         [Theory]
+        [InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)]
+        [InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
@@ -28,14 +31,15 @@ namespace Jellyfin.MediaEncoding.Tests
         [InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)]
         [InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)]
         public void ValidateVersionInternalTest(string versionOutput, bool valid)
         public void ValidateVersionInternalTest(string versionOutput, bool valid)
         {
         {
-            var val = new EncoderValidator(new NullLogger<EncoderValidatorTests>());
-            Assert.Equal(valid, val.ValidateVersionInternal(versionOutput));
+            Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput));
         }
         }
 
 
         private class GetFFmpegVersionTestData : IEnumerable<object?[]>
         private class GetFFmpegVersionTestData : IEnumerable<object?[]>
         {
         {
             public IEnumerator<object?[]> GetEnumerator()
             public IEnumerator<object?[]> GetEnumerator()
             {
             {
+                yield return new object?[] { EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4) };
+                yield return new object?[] { EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };

+ 24 - 0
tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs

@@ -2,6 +2,30 @@ namespace Jellyfin.MediaEncoding.Tests
 {
 {
     internal static class EncoderValidatorTestsData
     internal static class EncoderValidatorTestsData
     {
     {
+        public const string FFmpegV44Output = @"ffmpeg version 4.4-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers
+built with gcc 10.3.0 (Rev5, Built by MSYS2 project)
+configuration:  --disable-static --enable-shared --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls
+libavutil      56. 70.100 / 56. 70.100
+libavcodec     58.134.100 / 58.134.100
+libavformat    58. 76.100 / 58. 76.100
+libavdevice    58. 13.100 / 58. 13.100
+libavfilter     7.110.100 /  7.110.100
+libswscale      5.  9.100 /  5.  9.100
+libswresample   3.  9.100 /  3.  9.100
+libpostproc    55.  9.100 / 55.  9.100";
+
+        public const string FFmpegV432Output = @"ffmpeg version n4.3.2-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers
+built with gcc 10.2.0 (Rev9, Built by MSYS2 project)
+configuration:  --disable-static --enable-shared --cc='ccache gcc' --cxx='ccache g++' --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-lto --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls
+libavutil      56. 51.100 / 56. 51.100
+libavcodec     58. 91.100 / 58. 91.100
+libavformat    58. 45.100 / 58. 45.100
+libavdevice    58. 10.100 / 58. 10.100
+libavfilter     7. 85.100 /  7. 85.100
+libswscale      5.  7.100 /  5.  7.100
+libswresample   3.  7.100 /  3.  7.100
+libpostproc    55.  7.100 / 55.  7.100";
+
         public const string FFmpegV431Output = @"ffmpeg version n4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
         public const string FFmpegV431Output = @"ffmpeg version n4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
 built with gcc 10.1.0 (GCC)
 built with gcc 10.1.0 (GCC)
 configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-avisynth --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmfx --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-librav1e --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3
 configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-avisynth --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmfx --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-librav1e --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3

+ 80 - 0
tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs

@@ -1,3 +1,4 @@
+using System.Collections.Generic;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using Xunit;
 using Xunit;
 
 
@@ -5,6 +6,85 @@ namespace Jellyfin.Model.Tests.Entities
 {
 {
     public class MediaStreamTests
     public class MediaStreamTests
     {
     {
+        public static IEnumerable<object[]> Get_DisplayTitle_TestData()
+        {
+            return new List<object[]>
+            {
+                new object[]
+                {
+                    new MediaStream
+                    {
+                        Type = MediaStreamType.Subtitle,
+                        Title = "English",
+                        Language = string.Empty,
+                        IsForced = false,
+                        IsDefault = false,
+                        Codec = "ASS"
+                    },
+                    "English - Und - ASS"
+                },
+                new object[]
+                {
+                    new MediaStream
+                    {
+                        Type = MediaStreamType.Subtitle,
+                        Title = "English",
+                        Language = string.Empty,
+                        IsForced = false,
+                        IsDefault = false,
+                        Codec = string.Empty
+                    },
+                    "English - Und"
+                },
+                new object[]
+                {
+                    new MediaStream
+                    {
+                        Type = MediaStreamType.Subtitle,
+                        Title = "English",
+                        Language = "EN",
+                        IsForced = false,
+                        IsDefault = false,
+                        Codec = string.Empty
+                    },
+                    "English"
+                },
+                new object[]
+                {
+                    new MediaStream
+                    {
+                        Type = MediaStreamType.Subtitle,
+                        Title = "English",
+                        Language = "EN",
+                        IsForced = true,
+                        IsDefault = true,
+                        Codec = "SRT"
+                    },
+                    "English - Default - Forced - SRT"
+                },
+                new object[]
+                {
+                    new MediaStream
+                    {
+                        Type = MediaStreamType.Subtitle,
+                        Title = null,
+                        Language = null,
+                        IsForced = false,
+                        IsDefault = false,
+                        Codec = null
+                    },
+                    "Und"
+                }
+            };
+        }
+
+        [Theory]
+        [MemberData(nameof(Get_DisplayTitle_TestData))]
+        public void Get_DisplayTitle_should_return_valid_title(MediaStream mediaStream, string expected)
+        {
+            Assert.Equal(expected, mediaStream.DisplayTitle);
+        }
+
         [Theory]
         [Theory]
         [InlineData(null, null, false, null)]
         [InlineData(null, null, false, null)]
         [InlineData(null, 0, false, null)]
         [InlineData(null, 0, false, null)]

+ 1 - 1
tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj

@@ -11,7 +11,7 @@
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="3.1.0" />
     <PackageReference Include="coverlet.collector" Version="3.1.0" />
-    <PackageReference Include="FsCheck.Xunit" Version="2.16.0" />
+    <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code Analyzers -->
   <!-- Code Analyzers -->

+ 1 - 1
tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj

@@ -16,7 +16,7 @@
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="3.1.0" />
     <PackageReference Include="coverlet.collector" Version="3.1.0" />
-    <PackageReference Include="FsCheck.Xunit" Version="2.16.0" />
+    <PackageReference Include="FsCheck.Xunit" Version="2.16.1" />
     <PackageReference Include="Moq" Version="4.16.1" />
     <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 3 - 0
tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs

@@ -109,6 +109,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
         [InlineData("")]
         [InlineData("")]
         [InlineData("*")]
         [InlineData("*")]
         [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")]
         [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")]
+        [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*6374520964785129080*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid modified date
+        [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*-637452096478512963*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Negative modified date
+        [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Invalid*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid type
         public void ItemImageInfoFromValueString_Invalid_Null(string value)
         public void ItemImageInfoFromValueString_Invalid_Null(string value)
         {
         {
             Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
             Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));

+ 122 - 0
tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs

@@ -0,0 +1,122 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.LibraryStructureDto;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Configuration;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+    public sealed class MediaStructureControllerTests : IClassFixture<JellyfinApplicationFactory>
+    {
+        private readonly JellyfinApplicationFactory _factory;
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+        private static string? _accessToken;
+
+        public MediaStructureControllerTests(JellyfinApplicationFactory factory)
+        {
+            _factory = factory;
+        }
+
+        [Fact]
+        public async Task RenameVirtualFolder_WhiteSpaceName_ReturnsBadRequest()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            using var postContent = new ByteArrayContent(Array.Empty<byte>());
+            var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent).ConfigureAwait(false);
+
+            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+        }
+
+        [Fact]
+        public async Task RenameVirtualFolder_WhiteSpaceNewName_ReturnsBadRequest()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            using var postContent = new ByteArrayContent(Array.Empty<byte>());
+            var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent).ConfigureAwait(false);
+
+            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+        }
+
+        [Fact]
+        public async Task RenameVirtualFolder_NameDoesntExist_ReturnsNotFound()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            using var postContent = new ByteArrayContent(Array.Empty<byte>());
+            var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent).ConfigureAwait(false);
+
+            Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+        }
+
+        [Fact]
+        public async Task AddMediaPath_PathDoesntExist_ReturnsNotFound()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            var data = new MediaPathDto()
+            {
+                Name = "Test",
+                Path = "/this/path/doesnt/exist"
+            };
+
+            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions));
+            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+            var response = await client.PostAsync("Library/VirtualFolders/Paths", postContent).ConfigureAwait(false);
+
+            Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+        }
+
+        [Fact]
+        public async Task UpdateMediaPath_WhiteSpaceName_ReturnsBadRequest()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            var data = new UpdateMediaPathRequestDto()
+            {
+                Name = " ",
+                PathInfo = new MediaPathInfo("test")
+            };
+
+            using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions));
+            postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+            var response = await client.PostAsync("Library/VirtualFolders/Paths/Update", postContent).ConfigureAwait(false);
+
+            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+        }
+
+        [Fact]
+        public async Task RemoveMediaPath_WhiteSpaceName_ReturnsBadRequest()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+").ConfigureAwait(false);
+
+            Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+        }
+
+        [Fact]
+        public async Task RemoveMediaPath_PathDoesntExist_ReturnsNotFound()
+        {
+            var client = _factory.CreateClient();
+            client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+            var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist").ConfigureAwait(false);
+
+            Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+        }
+    }
+}

+ 14 - 0
tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs

@@ -207,6 +207,20 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
             Assert.Equal(id, item.ProviderIds[provider]);
             Assert.Equal(id, item.ProviderIds[provider]);
         }
         }
 
 
+        [Fact]
+        public void Parse_GivenFileWithFanartTag_Success()
+        {
+            var result = new MetadataResult<Video>()
+            {
+                Item = new Movie()
+            };
+
+            _parser.Fetch(result, "Test Data/Fanart.nfo", CancellationToken.None);
+
+            Assert.Single(result.RemoteImages.Where(x => x.type == ImageType.Backdrop));
+            Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg", result.RemoteImages.First(x => x.type == ImageType.Backdrop).url);
+        }
+
         [Fact]
         [Fact]
         public void Parse_RadarrUrlFile_Success()
         public void Parse_RadarrUrlFile_Success()
         {
         {

+ 33 - 0
tests/Jellyfin.XbmcMetadata.Tests/Test Data/Fanart.nfo

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<movie>
+    <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png</thumb>
+    <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png</thumb>
+    <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57b476a831d74.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57b476a831d74.png</thumb>
+    <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57947e28cf10b.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57947e28cf10b.png</thumb>
+    <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5863d5c0cf0c9.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5863d5c0cf0c9.png</thumb>
+    <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5a801747e5545.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5a801747e5545.png</thumb>
+    <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5cd75683df92b.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5cd75683df92b.png</thumb>
+    <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-586017e95adbd.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-586017e95adbd.jpg</thumb>
+    <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-5934d45bc6592.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-5934d45bc6592.jpg</thumb>
+    <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-5aa9289a379fa.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-5aa9289a379fa.jpg</thumb>
+    <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-585fb155c3743.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585fb155c3743.jpg</thumb>
+    <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-585edbda91d82.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585edbda91d82.jpg</thumb>
+    <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-5b86588882c12.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-5b86588882c12.jpg</thumb>
+    <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-5bbb9babe600c.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-5bbb9babe600c.jpg</thumb>
+    <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/movies/141052/hdmovieclearart/justice-league-5865c23193041.png">https://assets.fanart.tv/fanart/movies/141052/hdmovieclearart/justice-league-5865c23193041.png</thumb>
+    <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a3af26360617.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a3af26360617.png</thumb>
+    <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-58690967b9765.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-58690967b9765.png</thumb>
+    <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a953ca4db6a6.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a953ca4db6a6.png</thumb>
+    <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a0b913c233be.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a0b913c233be.png</thumb>
+    <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png</thumb>
+    <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-59dc595362ef1.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-59dc595362ef1.png</thumb>
+    <fanart>
+        <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg</thumb>
+        <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg</thumb>
+        <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg</thumb>
+        <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-58fa1f1932897.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-58fa1f1932897.jpg</thumb>
+        <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a14f5fd8dd16.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a14f5fd8dd16.jpg</thumb>
+        <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a119394ea362.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a119394ea362.jpg</thumb>
+    </fanart>
+    <thumb aspect="fanart">This-should-not-be-saved-as-a-fanart-image.jpg</thumb>
+</movie>