瀏覽代碼

Merge remote-tracking branch 'upstream/master' into http-client-migrate

crobibero 4 年之前
父節點
當前提交
2a8653b309
共有 69 個文件被更改,包括 586 次插入460 次删除
  1. 1 0
      CONTRIBUTORS.md
  2. 1 1
      Dockerfile
  3. 1 1
      Dockerfile.arm
  4. 1 1
      Dockerfile.arm64
  5. 1 1
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  6. 2 2
      Emby.Dlna/Didl/DidlBuilder.cs
  7. 1 1
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  8. 8 4
      Emby.Server.Implementations/ApplicationHost.cs
  9. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  10. 1 1
      Emby.Server.Implementations/Dto/DtoService.cs
  11. 4 3
      Emby.Server.Implementations/IO/FileRefresher.cs
  12. 1 1
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  13. 0 32
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  14. 1 31
      Emby.Server.Implementations/IO/StreamHelper.cs
  15. 8 4
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  16. 1 21
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  17. 10 33
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  18. 7 6
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  19. 2 2
      Emby.Server.Implementations/Localization/Core/nb.json
  20. 60 3
      Emby.Server.Implementations/Localization/Core/nn.json
  21. 16 16
      Emby.Server.Implementations/Localization/Core/ta.json
  22. 28 31
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  23. 21 21
      Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  24. 6 6
      Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
  25. 7 7
      Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
  26. 6 9
      Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
  27. 7 10
      Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
  28. 5 1
      Jellyfin.Api/BaseJellyfinApiController.cs
  29. 1 3
      Jellyfin.Api/Controllers/DisplayPreferencesController.cs
  30. 11 9
      Jellyfin.Api/Controllers/DlnaServerController.cs
  31. 6 1
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  32. 11 25
      Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
  33. 4 6
      Jellyfin.Server/CoreAppHost.cs
  34. 2 0
      Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
  35. 4 0
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  36. 1 1
      Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
  37. 3 2
      Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
  38. 2 1
      Jellyfin.Server/Formatters/XmlOutputFormatter.cs
  39. 0 36
      Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs
  40. 10 0
      Jellyfin.Server/Jellyfin.Server.csproj
  41. 3 2
      Jellyfin.Server/Startup.cs
  42. 0 0
      Jellyfin.Server/wwwroot/api-docs/redoc/custom.css
  43. 0 0
      Jellyfin.Server/wwwroot/api-docs/swagger/custom.css
  44. 10 0
      MediaBrowser.Common/Json/JsonDefaults.cs
  45. 51 8
      MediaBrowser.Controller/Entities/BaseItem.cs
  46. 2 9
      MediaBrowser.Controller/IDisplayPreferencesManager.cs
  47. 1 0
      MediaBrowser.Controller/LiveTv/ChannelInfo.cs
  48. 154 76
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  49. 14 0
      MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
  50. 10 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  51. 34 1
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  52. 12 6
      MediaBrowser.Model/Entities/MediaStream.cs
  53. 1 1
      MediaBrowser.Model/IO/FileSystemMetadata.cs
  54. 2 2
      MediaBrowser.Model/IO/IFileSystem.cs
  55. 0 1
      MediaBrowser.Model/IO/IShortcutHandler.cs
  56. 0 2
      MediaBrowser.Model/IO/IStreamHelper.cs
  57. 1 0
      MediaBrowser.Model/IO/IZipClient.cs
  58. 5 3
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
  59. 13 5
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
  60. 1 1
      debian/rules
  61. 1 1
      deployment/Dockerfile.docker.amd64
  62. 1 1
      deployment/Dockerfile.docker.arm64
  63. 1 1
      deployment/Dockerfile.docker.armhf
  64. 1 1
      deployment/build.linux.amd64
  65. 1 1
      deployment/build.macos
  66. 1 1
      deployment/build.portable
  67. 1 1
      deployment/build.windows.amd64
  68. 1 1
      fedora/jellyfin.spec
  69. 1 1
      windows/build-jellyfin.ps1

+ 1 - 0
CONTRIBUTORS.md

@@ -78,6 +78,7 @@
  - [nvllsvm](https://github.com/nvllsvm)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [oddstr13](https://github.com/oddstr13)
+ - [orryverducci](https://github.com/orryverducci)
  - [petermcneil](https://github.com/petermcneil)
  - [Phlogi](https://github.com/Phlogi)
  - [pjeanjean](https://github.com/pjeanjean)

+ 1 - 1
Dockerfile

@@ -14,7 +14,7 @@ COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
 
 FROM debian:buster-slim
 

+ 1 - 1
Dockerfile.arm

@@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu

+ 1 - 1
Dockerfile.arm64

@@ -21,7 +21,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM arm64v8/debian:buster-slim

+ 1 - 1
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -1363,7 +1363,7 @@ namespace Emby.Dlna.ContentDirectory
                 };
             }
 
-            Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id);
+            Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
 
             return new ServerItem(_libraryManager.GetUserRootFolder());
         }

+ 2 - 2
Emby.Dlna/Didl/DidlBuilder.cs

@@ -948,7 +948,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", name);
+                _logger.LogError(ex, "Error adding xml value: {Value}", name);
             }
         }
 
@@ -960,7 +960,7 @@ namespace Emby.Dlna.Didl
             }
             catch (XmlException ex)
             {
-                _logger.LogError(ex, "Error adding xml value: {value}", value);
+                _logger.LogError(ex, "Error adding xml value: {Value}", value);
             }
         }
 

+ 1 - 1
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error loading configuration file: {path}", path);
+                Logger.LogError(ex, "Error loading configuration file: {Path}", path);
 
                 return Activator.CreateInstance(configurationType);
             }

+ 8 - 4
Emby.Server.Implementations/ApplicationHost.cs

@@ -280,6 +280,10 @@ namespace Emby.Server.Implementations
                 Password = ServerConfigurationManager.Configuration.CertificatePassword
             };
             Certificate = GetCertificate(CertificateInfo);
+
+            ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
+            ApplicationVersionString = ApplicationVersion.ToString(3);
+            ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
         }
 
         public string ExpandVirtualPath(string path)
@@ -309,16 +313,16 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc />
-        public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version;
+        public Version ApplicationVersion { get; }
 
         /// <inheritdoc />
-        public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3);
+        public string ApplicationVersionString { get; }
 
         /// <summary>
         /// Gets the current application user agent.
         /// </summary>
         /// <value>The application user agent.</value>
-        public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+        public string ApplicationUserAgent { get; }
 
         /// <summary>
         /// Gets the email address for use within a comment section of a user agent field.
@@ -1392,7 +1396,7 @@ namespace Emby.Server.Implementations
 
             foreach (var assembly in assemblies)
             {
-                Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName);
+                Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName);
                 yield return assembly;
             }
         }

+ 1 - 1
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -890,7 +890,7 @@ namespace Emby.Server.Implementations.Channels
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error writing to channel cache file: {path}", path);
+                _logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
             }
         }
 

+ 1 - 1
Emby.Server.Implementations/Dto/DtoService.cs

@@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.Dto
                 catch (Exception ex)
                 {
                     // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
-                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name);
+                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
                 }
             }
 

+ 4 - 3
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO
                     continue;
                 }
 
-                _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+                _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
 
                 try
                 {
@@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO
                     // For now swallow and log.
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Should we remove it from it's parent?
-                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {Name}", item.Name);
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {Name}", item.Name);
                 }
             }
         }
@@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
             _disposed = true;

+ 1 - 1
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path);
+                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
                 }
             }
         }

+ 0 - 32
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        public virtual void SetReadOnly(string path, bool isReadOnly)
-        {
-            if (OperatingSystem.Id != OperatingSystemId.Windows)
-            {
-                return;
-            }
-
-            var info = GetExtendedFileSystemInfo(path);
-
-            if (info.Exists && info.IsReadOnly != isReadOnly)
-            {
-                if (isReadOnly)
-                {
-                    File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly);
-                }
-                else
-                {
-                    var attributes = File.GetAttributes(path);
-                    attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
-                    File.SetAttributes(path, attributes);
-                }
-            }
-        }
-
         public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
         {
             if (OperatingSystem.Id != OperatingSystemId.Windows)
@@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO
             return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
         }
 
-        public virtual void SetExecutable(string path)
-        {
-            if (OperatingSystem.Id == OperatingSystemId.Darwin)
-            {
-                RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path));
-            }
-        }
-
         private static void RunProcess(string path, string args, string workingDirectory)
         {
             using (var process = Process.Start(new ProcessStartInfo

+ 1 - 31
Emby.Server.Implementations/IO/StreamHelper.cs

@@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO
 {
     public class StreamHelper : IStreamHelper
     {
-        private const int StreamCopyToBufferSize = 81920;
-
         public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
         {
             byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
@@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken)
-        {
-            byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
-            try
-            {
-                int totalBytesRead = 0;
-
-                int bytesRead;
-                while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
-                {
-                    var bytesToWrite = bytesRead;
-
-                    if (bytesToWrite > 0)
-                    {
-                        await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
-                        totalBytesRead += bytesRead;
-                    }
-                }
-
-                return totalBytesRead;
-            }
-            finally
-            {
-                ArrayPool<byte>.Shared.Return(buffer);
-            }
-        }
-
         public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
         {
-            byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
+            byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
             try
             {
                 int bytesRead;

+ 8 - 4
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 _logger.LogInformation("Copying recording stream to file {0}", targetFile);
 
                 // The media source is infinite so we need to handle stopping ourselves
-                var durationToken = new CancellationTokenSource(duration);
-                cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+                using var durationToken = new CancellationTokenSource(duration);
+                using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
 
-                await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
+                await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
             }
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
@@ -80,7 +80,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             var durationToken = new CancellationTokenSource(duration);
             cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
 
-            await _streamHelper.CopyUntilCancelled(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), output, 81920, cancellationToken).ConfigureAwait(false);
+            await _streamHelper.CopyUntilCancelled(
+                await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+                output,
+                81920,
+                cancellationToken).ConfigureAwait(false);
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
         }

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

@@ -605,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return Task.CompletedTask;
         }
 
-        public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
-        {
-            return Task.CompletedTask;
-        }
-
         public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();
@@ -809,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             return null;
         }
 
-        public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings()
-        {
-            return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested);
-        }
-
         public ActiveRecordingInfo GetActiveRecordingInfo(string path)
         {
             if (string.IsNullOrWhiteSpace(path))
@@ -1016,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             throw new Exception("Tuner not found.");
         }
 
-        private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
-        {
-            var json = _jsonSerializer.SerializeToString(mediaSource);
-            mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
-
-            mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id;
-
-            return mediaSource;
-        }
-
         public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(channelId))
@@ -1655,7 +1635,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
             {
-                return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
+                return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
             }
 
             return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);

+ 10 - 33
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -8,12 +8,9 @@ using System.IO;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
@@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly ILogger _logger;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerApplicationPaths _appPaths;
+        private readonly IJsonSerializer _json;
+        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
+
         private bool _hasExited;
         private Stream _logFileStream;
         private string _targetPath;
         private Process _process;
-        private readonly IJsonSerializer _json;
-        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
-        private readonly IServerConfigurationManager _config;
 
         public EncodedRecorder(
             ILogger logger,
             IMediaEncoder mediaEncoder,
             IServerApplicationPaths appPaths,
-            IJsonSerializer json,
-            IServerConfigurationManager config)
+            IJsonSerializer json)
         {
             _logger = logger;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _json = json;
-            _config = config;
         }
 
         private static bool CopySubtitles => false;
@@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
             // The media source is infinite so we need to handle stopping ourselves
-            var durationToken = new CancellationTokenSource(duration);
-            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+            using var durationToken = new CancellationTokenSource(duration);
+            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
 
-            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false);
+            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false);
 
             _logger.LogInformation("Recording completed to file {0}", targetFile);
         }
 
-        private EncodingOptions GetEncodingOptions()
-        {
-            return _config.GetConfiguration<EncodingOptions>("encoding");
-        }
-
         private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
         {
             _targetPath = targetFile;
@@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 StartInfo = processStartInfo,
                 EnableRaisingEvents = true
             };
-            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
+            _process.Exited += (sender, args) => OnFfMpegProcessExited(_process);
 
             _process.Start();
 
@@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         }
 
         protected string GetOutputSizeParam()
-        {
-            var filters = new List<string>();
-
-            filters.Add("yadif=0:-1:0");
-
-            var output = string.Empty;
-
-            if (filters.Count > 0)
-            {
-                output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray()));
-            }
-
-            return output;
-        }
+            => "-vf \"yadif=0:-1:0\"";
 
         private void Stop()
         {
@@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         /// <summary>
         /// Processes the exited.
         /// </summary>
-        private void OnFfMpegProcessExited(Process process, string inputFile)
+        private void OnFfMpegProcessExited(Process process)
         {
             using (process)
             {

+ 7 - 6
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -26,14 +26,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 {
     public class SchedulesDirect : IListingsProvider
     {
+        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+
         private readonly ILogger<SchedulesDirect> _logger;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
         private readonly IApplicationHost _appHost;
 
-        private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
-
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
             IJsonSerializer jsonSerializer,
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             while (start <= end)
             {
-                dates.Add(start.ToString("yyyy-MM-dd"));
+                dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
                 start = start.AddDays(1);
             }
 
@@ -352,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             if (!string.IsNullOrWhiteSpace(details.originalAirDate))
             {
-                info.OriginalAirDate = DateTime.Parse(details.originalAirDate);
+                info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
                 info.ProductionYear = info.OriginalAirDate.Value.Year;
             }
 
             if (details.movie != null)
             {
-                if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year))
+                if (!string.IsNullOrEmpty(details.movie.year)
+                    && int.TryParse(details.movie.year, out int year))
                 {
                     info.ProductionYear = year;
                 }
@@ -557,7 +558,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return null;
             }
 
-            NameValuePair savedToken = null;
+            NameValuePair savedToken;
             if (!_tokens.TryGetValue(username, out savedToken))
             {
                 savedToken = new NameValuePair();

+ 2 - 2
Emby.Server.Implementations/Localization/Core/nb.json

@@ -71,7 +71,7 @@
     "ScheduledTaskFailedWithName": "{0} mislykkes",
     "ScheduledTaskStartedWithName": "{0} startet",
     "ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
-    "Shows": "Programmer",
+    "Shows": "Program",
     "Songs": "Sanger",
     "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
     "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for  {0}",
@@ -88,7 +88,7 @@
     "UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
     "UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
     "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}",
-    "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}",
+    "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}",
     "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling  {1}",
     "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
     "ValueSpecialEpisodeName": "Spesialepisode - {0}",

+ 60 - 3
Emby.Server.Implementations/Localization/Core/nn.json

@@ -35,7 +35,7 @@
     "AuthenticationSucceededWithUserName": "{0} Har logga inn",
     "Artists": "Artistar",
     "Application": "Program",
-    "AppDeviceValues": "App: {0}, Einheit: {1}",
+    "AppDeviceValues": "App: {0}, Eining: {1}",
     "Albums": "Album",
     "NotificationOptionServerRestartRequired": "Tenaren krev omstart",
     "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
@@ -43,7 +43,7 @@
     "NotificationOptionPluginInstalled": "Tilleggsprogram installert",
     "NotificationOptionPluginError": "Tilleggsprogram feila",
     "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
-    "NotificationOptionInstallationFailed": "Installasjonen feila",
+    "NotificationOptionInstallationFailed": "Installasjonsfeil",
     "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa",
     "NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
@@ -56,5 +56,62 @@
     "MusicVideos": "Musikkvideoar",
     "Music": "Musikk",
     "Movies": "Filmar",
-    "MixedContent": "Blanda innhald"
+    "MixedContent": "Blanda innhald",
+    "Sync": "Synkronisera",
+    "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
+    "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
+    "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
+    "TaskRefreshChannels": "Oppdater kanalar",
+    "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.",
+    "TaskCleanTranscode": "Reins transkodemappe",
+    "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.",
+    "TaskUpdatePlugins": "Oppdaterer programtillegg",
+    "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.",
+    "TaskRefreshPeople": "Oppdater personar",
+    "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.",
+    "TaskCleanLogs": "Reins loggmappe",
+    "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.",
+    "TaskRefreshLibrary": "Skann mediebibliotek",
+    "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.",
+    "TaskRefreshChapterImages": "Trekk ut kapittelbilete",
+    "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.",
+    "TaskCleanCache": "Rens mappe for hurtiglager",
+    "TasksChannelsCategory": "Internettkanalar",
+    "TasksApplicationCategory": "Applikasjon",
+    "TasksLibraryCategory": "Bibliotek",
+    "TasksMaintenanceCategory": "Vedlikehald",
+    "VersionNumber": "Versjon {0}",
+    "ValueSpecialEpisodeName": "Spesialepisode - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
+    "UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}",
+    "UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}",
+    "UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}",
+    "UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
+    "UserOnlineFromDevice": "{0} er direktekopla frå {1}",
+    "UserOfflineFromDevice": "{0} har kopla frå {1}",
+    "UserLockedOutWithName": "Brukar {0} har blitt utestengd",
+    "UserDownloadingItemWithValues": "{0} lastar ned {1}",
+    "UserDeletedWithName": "Brukar {0} er sletta",
+    "UserCreatedWithName": "Brukar {0} er oppretta",
+    "User": "Brukar",
+    "TvShows": "TV-seriar",
+    "System": "System",
+    "SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}",
+    "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.",
+    "Songs": "Songar",
+    "Shows": "Program",
+    "ServerNameNeedsToBeRestarted": "{0} må omstartast",
+    "ScheduledTaskStartedWithName": "{0} starta",
+    "ScheduledTaskFailedWithName": "{0} feila",
+    "ProviderValue": "Leverandør: {0}",
+    "PluginUpdatedWithName": "{0} blei oppdatert",
+    "PluginUninstalledWithName": "{0} blei avinstallert",
+    "PluginInstalledWithName": "{0} blei installert",
+    "Plugin": "Programtillegg",
+    "Playlists": "Speleliste",
+    "Photos": "Foto",
+    "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
+    "NotificationOptionVideoPlayback": "Videoavspeling starta",
+    "NotificationOptionUserLockedOut": "Brukar er utestengd",
+    "NotificationOptionTaskFailed": "Planlagt oppgåve feila"
 }

+ 16 - 16
Emby.Server.Implementations/Localization/Core/ta.json

@@ -18,7 +18,7 @@
     "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
     "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
     "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
-    "Inherit": "மரபரிமையாகப் பெறு",
+    "Inherit": "மரபரிமையாகப் பெறு",
     "HeaderRecordingGroups": "பதிவு குழுக்கள்",
     "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
     "Folders": "கோப்புறைகள்",
@@ -31,7 +31,7 @@
     "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
     "TaskRefreshChannels": "சேனல்களை புதுப்பி",
     "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
-    "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்",
+    "TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்",
     "TasksChannelsCategory": "இணைய சேனல்கள்",
     "TasksApplicationCategory": "செயலி",
     "TasksLibraryCategory": "நூலகம்",
@@ -46,7 +46,7 @@
     "Sync": "ஒத்திசைவு",
     "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
     "Songs": "பாடல்கள்",
-    "Shows": "தொடர்கள்",
+    "Shows": "நிகழ்ச்சிகள்",
     "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
     "ScheduledTaskStartedWithName": "{0} துவங்கியது",
     "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
@@ -67,20 +67,20 @@
     "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
     "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
     "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
-    "NameSeasonUnknown": "பருவம் அறியப்படாதவை",
+    "NameSeasonUnknown": "அறியப்படாத பருவம்",
     "NameSeasonNumber": "பருவம் {0}",
     "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
     "MusicVideos": "இசைப்படங்கள்",
     "Music": "இசை",
     "Movies": "திரைப்படங்கள்",
-    "Latest": "புதிய",
+    "Latest": "புதியவை",
     "LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
     "LabelIpAddressValue": "ஐபி முகவரி: {0}",
     "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
     "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
-    "HeaderNextUp": "அடுத்ததாக",
+    "HeaderNextUp": "அடுத்தத",
     "HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
-    "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்",
+    "HeaderFavoriteSongs": "பிடித்த பாட்கள்",
     "HeaderFavoriteShows": "பிடித்த தொடர்கள்",
     "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
     "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
@@ -93,25 +93,25 @@
     "Channels": "சேனல்கள்",
     "Books": "புத்தகங்கள்",
     "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
-    "Artists": "கலைஞர்",
+    "Artists": "கலைஞர்கள்",
     "Application": "செயலி",
     "Albums": "ஆல்பங்கள்",
     "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
-    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது",
+    "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது",
     "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
     "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
-    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
-    "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+    "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
+    "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
     "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
-    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
-    "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.",
+    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+    "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
     "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
-    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
-    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.",
+    "TaskCleanLogs": "பதிவு அடைவ சுத்தம் செய்யுங்கள்",
+    "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.",
     "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
     "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
-    "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது",
+    "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
     "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
 }

+ 28 - 31
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -5,10 +5,10 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
 
 namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 {
@@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// Gets or sets the application paths.
         /// </summary>
         /// <value>The application paths.</value>
-        private IApplicationPaths ApplicationPaths { get; set; }
-
+        private readonly IApplicationPaths _applicationPaths;
         private readonly ILogger<DeleteCacheFileTask> _logger;
-
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
 
@@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             IFileSystem fileSystem,
             ILocalizationManager localization)
         {
-            ApplicationPaths = appPaths;
+            _applicationPaths = appPaths;
             _logger = logger;
             _fileSystem = fileSystem;
             _localization = localization;
         }
 
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskCleanCache");
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+        /// <inheritdoc />
+        public string Key => "DeleteCacheFiles";
+
+        /// <inheritdoc />
+        public bool IsHidden => false;
+
+        /// <inheritdoc />
+        public bool IsEnabled => true;
+
+        /// <inheritdoc />
+        public bool IsLogged => true;
+
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// </summary>
         /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         {
-            return new[] {
-
+            return new[]
+            {
                 // Every so often
                 new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
             };
@@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
             try
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress);
             }
             catch (DirectoryNotFoundException)
             {
@@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
             try
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress);
             }
             catch (DirectoryNotFoundException)
             {
@@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             return Task.CompletedTask;
         }
 
-
         /// <summary>
         /// Deletes the cache files from directory with a last write time less than a given date.
         /// </summary>
@@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
                 _logger.LogError(ex, "Error deleting file {path}", path);
             }
         }
-
-        /// <inheritdoc />
-        public string Name => _localization.GetLocalizedString("TaskCleanCache");
-
-        /// <inheritdoc />
-        public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
-
-        /// <inheritdoc />
-        public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
-        /// <inheritdoc />
-        public string Key => "DeleteCacheFiles";
-
-        /// <inheritdoc />
-        public bool IsHidden => false;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
     }
 }

+ 21 - 21
Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs

@@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks
             _localization = localization;
         }
 
+        /// <inheritdoc />
+        public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
+
+        /// <inheritdoc />
+        public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
+
+        /// <inheritdoc />
+        public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
+
+        /// <inheritdoc />
+        public string Key => "PluginUpdates";
+
+        /// <inheritdoc />
+        public bool IsHidden => false;
+
+        /// <inheritdoc />
+        public bool IsEnabled => true;
+
+        /// <inheritdoc />
+        public bool IsLogged => true;
+
         /// <summary>
         /// Creates the triggers that define when the task will run.
         /// </summary>
@@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
             progress.Report(100);
         }
-
-        /// <inheritdoc />
-        public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
-
-        /// <inheritdoc />
-        public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
-
-        /// <inheritdoc />
-        public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
-
-        /// <inheritdoc />
-        public string Key => "PluginUpdates";
-
-        /// <inheritdoc />
-        public bool IsHidden => false;
-
-        /// <inheritdoc />
-        public bool IsEnabled => true;
-
-        /// <inheritdoc />
-        public bool IsLogged => true;
     }
 }

+ 6 - 6
Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs

@@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
     public class DailyTrigger : ITaskTrigger
     {
         /// <summary>
-        /// Get the time of day to trigger the task to run.
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
+        /// <summary>
+        /// Gets or sets the time of day to trigger the task to run.
         /// </summary>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
@@ -69,11 +74,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>

+ 7 - 7
Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs

@@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     public class IntervalTrigger : ITaskTrigger
     {
+        private DateTime _lastStartDate;
+
+        /// <summary>
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
         /// <summary>
         /// Gets or sets the interval.
         /// </summary>
@@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <value>The timer.</value>
         private Timer Timer { get; set; }
 
-        private DateTime _lastStartDate;
-
         /// <summary>
         /// Stars waiting for the trigger action.
         /// </summary>
@@ -88,11 +93,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>

+ 6 - 9
Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs

@@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     public class StartupTrigger : ITaskTrigger
     {
+        /// <summary>
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
         public int DelayMs { get; set; }
 
         /// <summary>
@@ -48,20 +53,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
         {
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>
         private void OnTriggered()
         {
-            if (Triggered != null)
-            {
-                Triggered(this, EventArgs.Empty);
-            }
+            Triggered?.Invoke(this, EventArgs.Empty);
         }
     }
 }

+ 7 - 10
Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs

@@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
     public class WeeklyTrigger : ITaskTrigger
     {
         /// <summary>
-        /// Get the time of day to trigger the task to run.
+        /// Occurs when [triggered].
+        /// </summary>
+        public event EventHandler<EventArgs> Triggered;
+
+        /// <summary>
+        /// Gets or sets the time of day to trigger the task to run.
         /// </summary>
         /// <value>The time of day.</value>
         public TimeSpan TimeOfDay { get; set; }
@@ -95,20 +100,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
             }
         }
 
-        /// <summary>
-        /// Occurs when [triggered].
-        /// </summary>
-        public event EventHandler<EventArgs> Triggered;
-
         /// <summary>
         /// Called when [triggered].
         /// </summary>
         private void OnTriggered()
         {
-            if (Triggered != null)
-            {
-                Triggered(this, EventArgs.Empty);
-            }
+            Triggered?.Invoke(this, EventArgs.Empty);
         }
     }
 }

+ 5 - 1
Jellyfin.Api/BaseJellyfinApiController.cs

@@ -1,4 +1,5 @@
 using System.Net.Mime;
+using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Mvc;
 
 namespace Jellyfin.Api
@@ -8,7 +9,10 @@ namespace Jellyfin.Api
     /// </summary>
     [ApiController]
     [Route("[controller]")]
-    [Produces(MediaTypeNames.Application.Json)]
+    [Produces(
+        MediaTypeNames.Application.Json,
+        JsonDefaults.CamelCaseMediaType,
+        JsonDefaults.PascalCaseMediaType)]
     public class BaseJellyfinApiController : ControllerBase
     {
     }

+ 1 - 3
Jellyfin.Api/Controllers/DisplayPreferencesController.cs

@@ -153,7 +153,6 @@ namespace Jellyfin.Api.Controllers
             {
                 var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
                 itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
-                _displayPreferencesManager.SaveChanges(itemPreferences);
             }
 
             var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
@@ -167,8 +166,7 @@ namespace Jellyfin.Api.Controllers
                 itemPrefs.ViewType = viewType;
             }
 
-            _displayPreferencesManager.SaveChanges(existingDisplayPreferences);
-            _displayPreferencesManager.SaveChanges(itemPrefs);
+            _displayPreferencesManager.SaveChanges();
 
             return NoContent();
         }

+ 11 - 9
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using System.Net.Mime;
 using System.Threading.Tasks;
 using Emby.Dlna;
 using Emby.Dlna.Main;
@@ -17,8 +18,6 @@ namespace Jellyfin.Api.Controllers
     [Route("Dlna")]
     public class DlnaServerController : BaseJellyfinApiController
     {
-        private const string XMLContentType = "text/xml; charset=UTF-8";
-
         private readonly IDlnaManager _dlnaManager;
         private readonly IContentDirectory _contentDirectory;
         private readonly IConnectionManager _connectionManager;
@@ -44,7 +43,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
         [HttpGet("{serverId}/description")]
         [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
-        [Produces(XMLContentType)]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult GetDescriptionXml([FromRoute] string serverId)
         {
@@ -61,8 +60,9 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Dlna content directory returned.</response>
         /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
         [HttpGet("{serverId}/ContentDirectory")]
-        [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")]
-        [Produces(XMLContentType)]
+        [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
+        [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
         public ActionResult GetContentDirectory([FromRoute] string serverId)
@@ -76,8 +76,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
         [HttpGet("{serverId}/MediaReceiverRegistrar")]
-        [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
-        [Produces(XMLContentType)]
+        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
+        [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
         public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
@@ -91,8 +92,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="serverId">Server UUID.</param>
         /// <returns>Dlna media receiver registrar xml.</returns>
         [HttpGet("{serverId}/ConnectionManager")]
-        [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")]
-        [Produces(XMLContentType)]
+        [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
+        [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
+        [Produces(MediaTypeNames.Text.Xml)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
         public ActionResult GetConnectionManager([FromRoute] string serverId)

+ 6 - 1
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -1354,15 +1354,20 @@ namespace Jellyfin.Api.Controllers
                 segmentFormat = "mpegts";
             }
 
+            var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
+                ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+                : "128";
+
             return string.Format(
                 CultureInfo.InvariantCulture,
-                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
+                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
                 inputModifier,
                 _encodingHelper.GetInputArgument(state, encodingOptions),
                 threads,
                 mapArgs,
                 GetVideoArguments(state, encodingOptions, startNumber),
                 GetAudioArguments(state, encodingOptions),
+                maxMuxingQueueSize,
                 state.SegmentLength.ToString(CultureInfo.InvariantCulture),
                 segmentFormat,
                 startNumberParam,

+ 11 - 25
Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs

@@ -14,22 +14,21 @@ namespace Jellyfin.Server.Implementations.Users
     /// </summary>
     public class DisplayPreferencesManager : IDisplayPreferencesManager
     {
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly JellyfinDb _dbContext;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
         /// </summary>
-        /// <param name="dbProvider">The Jellyfin db provider.</param>
-        public DisplayPreferencesManager(JellyfinDbProvider dbProvider)
+        /// <param name="dbContext">The database context.</param>
+        public DisplayPreferencesManager(JellyfinDb dbContext)
         {
-            _dbProvider = dbProvider;
+            _dbContext = dbContext;
         }
 
         /// <inheritdoc />
         public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
         {
-            using var dbContext = _dbProvider.CreateContext();
-            var prefs = dbContext.DisplayPreferences
+            var prefs = _dbContext.DisplayPreferences
                 .Include(pref => pref.HomeSections)
                 .FirstOrDefault(pref =>
                     pref.UserId == userId && string.Equals(pref.Client, client));
@@ -37,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Users
             if (prefs == null)
             {
                 prefs = new DisplayPreferences(userId, client);
-                dbContext.DisplayPreferences.Add(prefs);
+                _dbContext.DisplayPreferences.Add(prefs);
             }
 
             return prefs;
@@ -46,14 +45,13 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc />
         public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
         {
-            using var dbContext = _dbProvider.CreateContext();
-            var prefs = dbContext.ItemDisplayPreferences
+            var prefs = _dbContext.ItemDisplayPreferences
                 .FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client));
 
             if (prefs == null)
             {
                 prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
-                dbContext.ItemDisplayPreferences.Add(prefs);
+                _dbContext.ItemDisplayPreferences.Add(prefs);
             }
 
             return prefs;
@@ -62,27 +60,15 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc />
         public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
         {
-            using var dbContext = _dbProvider.CreateContext();
-
-            return dbContext.ItemDisplayPreferences
+            return _dbContext.ItemDisplayPreferences
                 .Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
                 .ToList();
         }
 
         /// <inheritdoc />
-        public void SaveChanges(DisplayPreferences preferences)
-        {
-            using var dbContext = _dbProvider.CreateContext();
-            dbContext.Update(preferences);
-            dbContext.SaveChanges();
-        }
-
-        /// <inheritdoc />
-        public void SaveChanges(ItemDisplayPreferences preferences)
+        public void SaveChanges()
         {
-            using var dbContext = _dbProvider.CreateContext();
-            dbContext.Update(preferences);
-            dbContext.SaveChanges();
+            _dbContext.SaveChanges();
         }
     }
 }

+ 4 - 6
Jellyfin.Server/CoreAppHost.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Reflection;
 using Emby.Drawing;
 using Emby.Server.Implementations;
@@ -15,6 +16,7 @@ using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
@@ -67,12 +69,8 @@ namespace Jellyfin.Server
                 Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
             }
 
-            // TODO: Set up scoping and use AddDbContextPool,
-            // can't register as Transient since tracking transient in GC is funky
-            // serviceCollection.AddDbContext<JellyfinDb>(
-            //     options => options
-            //         .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
-            //     ServiceLifetime.Transient);
+            ServiceCollection.AddDbContextPool<JellyfinDb>(
+                 options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
 
             ServiceCollection.AddEventServices();
             ServiceCollection.AddSingleton<IEventManager, EventManager>();

+ 2 - 0
Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs

@@ -39,12 +39,14 @@ namespace Jellyfin.Server.Extensions
                     c.DocumentTitle = "Jellyfin API";
                     c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API");
                     c.RoutePrefix = $"{baseUrl}api-docs/swagger";
+                    c.InjectStylesheet($"/{baseUrl}api-docs/swagger/custom.css");
                 })
                 .UseReDoc(c =>
                 {
                     c.DocumentTitle = "Jellyfin API";
                     c.SpecUrl($"/{baseUrl}api-docs/openapi.json");
                     c.RoutePrefix = $"{baseUrl}api-docs/redoc";
+                    c.InjectStylesheet($"/{baseUrl}api-docs/redoc/custom.css");
                 });
         }
 

+ 4 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -152,6 +152,10 @@ namespace Jellyfin.Server.Extensions
                 .AddMvc(opts =>
                 {
                     opts.UseGeneralRoutePrefix(baseUrl);
+
+                    // Allow requester to change between camelCase and PascalCase
+                    opts.RespectBrowserAcceptHeader = true;
+
                     opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
                     opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 

+ 1 - 1
Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Formatters
         public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions())
         {
             SupportedMediaTypes.Clear();
-            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
         }
     }
 }

+ 3 - 2
Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs

@@ -1,3 +1,4 @@
+using System.Net.Mime;
 using MediaBrowser.Common.Json;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Net.Http.Headers;
@@ -16,8 +17,8 @@ namespace Jellyfin.Server.Formatters
         {
             SupportedMediaTypes.Clear();
             // Add application/json for default formatter
-            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
-            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\""));
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json));
+            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType));
         }
     }
 }

+ 2 - 1
Jellyfin.Server/Formatters/XmlOutputFormatter.cs

@@ -16,8 +16,9 @@ namespace Jellyfin.Server.Formatters
         /// </summary>
         public XmlOutputFormatter()
         {
+            SupportedMediaTypes.Clear();
             SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
-            SupportedMediaTypes.Add("text/xml;charset=UTF-8");
+
             SupportedEncodings.Add(Encoding.UTF8);
             SupportedEncodings.Add(Encoding.Unicode);
         }

+ 0 - 36
Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs

@@ -1,36 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Server.Implementations;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
-
-namespace Jellyfin.Server.HealthChecks
-{
-    /// <summary>
-    /// Checks connectivity to the database.
-    /// </summary>
-    public class JellyfinDbHealthCheck : IHealthCheck
-    {
-        private readonly JellyfinDbProvider _dbProvider;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JellyfinDbHealthCheck"/> class.
-        /// </summary>
-        /// <param name="dbProvider">The jellyfin db provider.</param>
-        public JellyfinDbHealthCheck(JellyfinDbProvider dbProvider)
-        {
-            _dbProvider = dbProvider;
-        }
-
-        /// <inheritdoc />
-        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
-        {
-            await using var jellyfinDb = _dbProvider.CreateContext();
-            if (await jellyfinDb.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false))
-            {
-                return HealthCheckResult.Healthy("Database connection successful.");
-            }
-
-            return HealthCheckResult.Unhealthy("Unable to connect to the database.");
-        }
-    }
-}

+ 10 - 0
Jellyfin.Server/Jellyfin.Server.csproj

@@ -44,6 +44,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.7" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.7" />
     <PackageReference Include="prometheus-net" Version="3.6.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
@@ -64,4 +65,13 @@
     <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <None Update="wwwroot\api-docs\swagger\custom.css">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+    <None Update="wwwroot\api-docs\redoc\custom.css">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
 </Project>

+ 3 - 2
Jellyfin.Server/Startup.cs

@@ -3,7 +3,7 @@ using System.ComponentModel;
 using System.Net.Http.Headers;
 using Jellyfin.Api.TypeConverters;
 using Jellyfin.Server.Extensions;
-using Jellyfin.Server.HealthChecks;
+using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Models;
 using MediaBrowser.Common.Net;
@@ -79,7 +79,7 @@ namespace Jellyfin.Server
                 .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
 
             services.AddHealthChecks()
-                .AddCheck<JellyfinDbHealthCheck>("JellyfinDb");
+                .AddDbContextCheck<JellyfinDb>();
         }
 
         /// <summary>
@@ -112,6 +112,7 @@ namespace Jellyfin.Server
                 app.UseHttpsRedirection();
             }
 
+            app.UseStaticFiles();
             app.UseAuthentication();
             app.UseJellyfinApiSwagger(_serverConfigurationManager);
             app.UseRouting();

+ 0 - 0
Jellyfin.Server/wwwroot/api-docs/redoc/custom.css


+ 0 - 0
Jellyfin.Server/wwwroot/api-docs/swagger/custom.css


+ 10 - 0
MediaBrowser.Common/Json/JsonDefaults.cs

@@ -9,6 +9,16 @@ namespace MediaBrowser.Common.Json
     /// </summary>
     public static class JsonDefaults
     {
+        /// <summary>
+        /// Pascal case json profile media type.
+        /// </summary>
+        public const string PascalCaseMediaType = "application/json; profile=\"PascalCase\"";
+
+        /// <summary>
+        /// Camel case json profile media type.
+        /// </summary>
+        public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
+
         /// <summary>
         /// Gets the default <see cref="JsonSerializerOptions" /> options.
         /// </summary>

+ 51 - 8
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -60,8 +60,6 @@ namespace MediaBrowser.Controller.Entities
 
         protected BaseItem()
         {
-            ThemeSongIds = Array.Empty<Guid>();
-            ThemeVideoIds = Array.Empty<Guid>();
             Tags = Array.Empty<string>();
             Genres = Array.Empty<string>();
             Studios = Array.Empty<string>();
@@ -100,12 +98,52 @@ namespace MediaBrowser.Controller.Entities
         };
 
         [JsonIgnore]
-        public Guid[] ThemeSongIds { get; set; }
+        public Guid[] ThemeSongIds
+        {
+            get
+            {
+                if (_themeSongIds == null)
+                {
+                    _themeSongIds = GetExtras()
+                        .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
+                        .Select(song => song.Id)
+                        .ToArray();
+                }
+
+                return _themeSongIds;
+            }
+
+            private set
+            {
+                _themeSongIds = value;
+            }
+        }
+
         [JsonIgnore]
-        public Guid[] ThemeVideoIds { get; set; }
+        public Guid[] ThemeVideoIds
+        {
+            get
+            {
+                if (_themeVideoIds == null)
+                {
+                    _themeVideoIds = GetExtras()
+                        .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
+                        .Select(song => song.Id)
+                        .ToArray();
+                }
+
+                return _themeVideoIds;
+            }
+
+            private set
+            {
+                _themeVideoIds = value;
+            }
+        }
 
         [JsonIgnore]
         public string PreferredMetadataCountryCode { get; set; }
+
         [JsonIgnore]
         public string PreferredMetadataLanguage { get; set; }
 
@@ -635,6 +673,9 @@ namespace MediaBrowser.Controller.Entities
         }
 
         private string _sortName;
+        private Guid[] _themeSongIds;
+        private Guid[] _themeVideoIds;
+
         /// <summary>
         /// Gets the name of the sort.
         /// </summary>
@@ -1582,7 +1623,8 @@ namespace MediaBrowser.Controller.Entities
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
 
-            item.ThemeVideoIds = newThemeVideoIds;
+            // They are expected to be sorted by SortName
+            item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
 
             return themeVideosChanged;
         }
@@ -1619,7 +1661,8 @@ namespace MediaBrowser.Controller.Entities
 
             await Task.WhenAll(tasks).ConfigureAwait(false);
 
-            item.ThemeSongIds = newThemeSongIds;
+            // They are expected to be sorted by SortName
+            item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
 
             return themeSongsChanged;
         }
@@ -2910,12 +2953,12 @@ namespace MediaBrowser.Controller.Entities
 
         public IEnumerable<BaseItem> GetThemeSongs()
         {
-            return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName);
+            return ThemeSongIds.Select(LibraryManager.GetItemById);
         }
 
         public IEnumerable<BaseItem> GetThemeVideos()
         {
-            return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName);
+            return ThemeVideoIds.Select(LibraryManager.GetItemById);
         }
 
         /// <summary>

+ 2 - 9
MediaBrowser.Controller/IDisplayPreferencesManager.cs

@@ -35,15 +35,8 @@ namespace MediaBrowser.Controller
         IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
 
         /// <summary>
-        /// Saves changes to the provided display preferences.
+        /// Saves changes made to the database.
         /// </summary>
-        /// <param name="preferences">The display preferences to save.</param>
-        void SaveChanges(DisplayPreferences preferences);
-
-        /// <summary>
-        /// Saves changes to the provided item display preferences.
-        /// </summary>
-        /// <param name="preferences">The item display preferences to save.</param>
-        void SaveChanges(ItemDisplayPreferences preferences);
+        void SaveChanges();
     }
 }

+ 1 - 0
MediaBrowser.Controller/LiveTv/ChannelInfo.cs

@@ -62,6 +62,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// </summary>
         /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
         public bool? HasImage { get; set; }
+
         /// <summary>
         /// Gets or sets a value indicating whether this instance is favorite.
         /// </summary>

+ 154 - 76
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -456,6 +456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
+            var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
             var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
             var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
@@ -515,6 +516,24 @@ namespace MediaBrowser.Controller.MediaEncoding
                     }
                 }
 
+                if (state.IsVideoRequest
+                    && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+                {
+                    var isColorDepth10 = IsColorDepth10(state);
+
+                    if (isNvencHevcDecoder && isColorDepth10
+                        && _mediaEncoder.SupportsHwaccel("opencl")
+                        && encodingOptions.EnableTonemapping
+                        && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+                        && state.VideoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+                    {
+                        arg.Append("-init_hw_device opencl=ocl:")
+                            .Append(encodingOptions.OpenclDevice)
+                            .Append(' ')
+                            .Append("-filter_hw_device ocl ");
+                    }
+                }
+
                 if (state.IsVideoRequest
                     && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
                 {
@@ -1003,11 +1022,33 @@ namespace MediaBrowser.Controller.MediaEncoding
             if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+                && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
             {
                 param = "-pix_fmt yuv420p " + param;
             }
 
+            if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+            {
+                var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
+                var videoStream = state.VideoStream;
+                var isColorDepth10 = IsColorDepth10(state);
+
+                if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1
+                    && isColorDepth10
+                    && _mediaEncoder.SupportsHwaccel("opencl")
+                    && encodingOptions.EnableTonemapping
+                    && !string.IsNullOrEmpty(videoStream.VideoRange)
+                    && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+                {
+                    param = "-pix_fmt nv12 " + param;
+                }
+                else
+                {
+                    param = "-pix_fmt yuv420p " + param;
+                }
+            }
+
             if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
             {
                 param = "-pix_fmt nv21 " + param;
@@ -1611,64 +1652,45 @@ namespace MediaBrowser.Controller.MediaEncoding
             var outputSizeParam = ReadOnlySpan<char>.Empty;
             var request = state.BaseRequest;
 
-            // Add resolution params, if specified
-            if (request.Width.HasValue
-                || request.Height.HasValue
-                || request.MaxHeight.HasValue
-                || request.MaxWidth.HasValue)
+            outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
+
+            // All possible beginning of video filters
+            // Don't break the order
+            string[] beginOfOutputSizeParam = new[]
             {
-                outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
+                // for tonemap_opencl
+                "hwupload,tonemap_opencl",
 
                 // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
-                var index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
+                "hwupload=extra_hw_frames",
+
+                // vpp_qsv
+                "vpp",
+
+                // hwdownload,format=p010le (hardware decode + software encode for vaapi)
+                "hwdownload",
+
+                // format=nv12|vaapi,hwupload,scale_vaapi
+                "format",
+
+                // bwdif,scale=expr
+                "bwdif",
+
+                // yadif,scale=expr
+                "yadif",
+
+                // scale=expr
+                "scale"
+            };
+
+            var index = -1;
+            foreach (var param in beginOfOutputSizeParam)
+            {
+                index = outputSizeParam.IndexOf(param, StringComparison.OrdinalIgnoreCase);
                 if (index != -1)
                 {
                     outputSizeParam = outputSizeParam.Slice(index);
-                }
-                else
-                {
-                    // vpp_qsv
-                    index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
-                    if (index != -1)
-                    {
-                        outputSizeParam = outputSizeParam.Slice(index);
-                    }
-                    else
-                    {
-                        // hwdownload,format=p010le (hardware decode + software encode for vaapi)
-                        index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
-                        if (index != -1)
-                        {
-                            outputSizeParam = outputSizeParam.Slice(index);
-                        }
-                        else
-                        {
-                            // format=nv12|vaapi,hwupload,scale_vaapi
-                            index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
-                            if (index != -1)
-                            {
-                                outputSizeParam = outputSizeParam.Slice(index);
-                            }
-                            else
-                            {
-                                // yadif,scale=expr
-                                index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
-                                if (index != -1)
-                                {
-                                    outputSizeParam = outputSizeParam.Slice(index);
-                                }
-                                else
-                                {
-                                    // scale=expr
-                                    index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
-                                    if (index != -1)
-                                    {
-                                        outputSizeParam = outputSizeParam.Slice(index);
-                                    }
-                                }
-                            }
-                        }
-                    }
+                    break;
                 }
             }
 
@@ -1747,9 +1769,9 @@ namespace MediaBrowser.Controller.MediaEncoding
                 */
                 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}]{3}[base];[base][sub]overlay_qsv\""
+                        : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
                 }
             }
 
@@ -2084,12 +2106,61 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+            var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
             var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+            var isColorDepth10 = IsColorDepth10(state);
 
             var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
 
+            // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
+            var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30;
+
+            // Currently only with the use of NVENC decoder can we get a decent performance.
+            // Currently only the HEVC/H265 format is supported.
+            // NVIDIA Pascal and Turing or higher are recommended.
+            if (isNvdecHevcDecoder && isColorDepth10
+                && _mediaEncoder.SupportsHwaccel("opencl")
+                && options.EnableTonemapping
+                && !string.IsNullOrEmpty(videoStream.VideoRange)
+                && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+            {
+                var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
+
+                if (options.TonemappingParam != 0)
+                {
+                    parameters += ":param={4}";
+                }
+
+                if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+                {
+                    parameters += ":range={5}";
+                }
+
+                // Upload the HDR10 or HLG data to the OpenCL device,
+                // use tonemap_opencl filter for tone mapping,
+                // and then download the SDR data to memory.
+                filters.Add("hwupload");
+                filters.Add(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        parameters,
+                        options.TonemappingAlgorithm,
+                        options.TonemappingDesat,
+                        options.TonemappingThreshold,
+                        options.TonemappingPeak,
+                        options.TonemappingParam,
+                        options.TonemappingRange));
+                filters.Add("hwdownload");
+
+                if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)
+                    || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+                {
+                    filters.Add("format=nv12");
+                }
+            }
+
             // When the input may or may not be hardware VAAPI decodable
             if (isVaapiH264Encoder)
             {
@@ -2107,7 +2178,6 @@ namespace MediaBrowser.Controller.MediaEncoding
             else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
             {
                 var codec = videoStream.Codec.ToLowerInvariant();
-                var isColorDepth10 = IsColorDepth10(state);
 
                 // Assert 10-bit hardware VAAPI decodable
                 if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2136,35 +2206,38 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 if (isVaapiH264Encoder)
                 {
-                    filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
+                    filters.Add(
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            "deinterlace_vaapi=rate={0}",
+                            doubleRateDeinterlace ? "field" : "frame"));
                 }
             }
 
             // Add software deinterlace filter before scaling filter
-            if (state.DeInterlace("h264", true)
-                || state.DeInterlace("avc", true)
-                || state.DeInterlace("h265", true)
-                || state.DeInterlace("hevc", true))
+            if ((state.DeInterlace("h264", true)
+                 || state.DeInterlace("avc", true)
+                 || state.DeInterlace("h265", true)
+                 || state.DeInterlace("hevc", true))
+                && !isVaapiH264Encoder
+                && !isQsvH264Encoder
+                && !isNvdecH264Decoder)
             {
-                string deintParam;
-                var inputFramerate = videoStream?.RealFrameRate;
-
-                // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
-                if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
+                if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase))
                 {
-                    deintParam = "yadif=1:-1:0";
+                    filters.Add(
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            "bwdif={0}:-1:0",
+                            doubleRateDeinterlace ? "1" : "0"));
                 }
                 else
                 {
-                    deintParam = "yadif=0:-1:0";
-                }
-
-                if (!string.IsNullOrEmpty(deintParam))
-                {
-                    if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
-                    {
-                        filters.Add(deintParam);
-                    }
+                    filters.Add(
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            "yadif={0}:-1:0",
+                            doubleRateDeinterlace ? "1" : "0"));
                 }
             }
 
@@ -2397,6 +2470,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                         if (state.DeInterlace("h264", true))
                         {
                             inputModifier += " -deint 1";
+
+                            if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.RealFrameRate ?? 60) > 30)
+                            {
+                                inputModifier += " -drop_second_field 1";
+                            }
                         }
                     }
                 }

+ 14 - 0
MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs

@@ -282,6 +282,20 @@ namespace MediaBrowser.MediaEncoding.Probing
         [JsonPropertyName("disposition")]
         public IReadOnlyDictionary<string, int> Disposition { get; set; }
 
+        /// <summary>
+        /// Gets or sets the color range.
+        /// </summary>
+        /// <value>The color range.</value>
+        [JsonPropertyName("color_range")]
+        public string ColorRange { get; set; }
+
+        /// <summary>
+        /// Gets or sets the color space.
+        /// </summary>
+        /// <value>The color space.</value>
+        [JsonPropertyName("color_space")]
+        public string ColorSpace { get; set; }
+
         /// <summary>
         /// Gets or sets the color transfer.
         /// </summary>

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

@@ -714,6 +714,16 @@ namespace MediaBrowser.MediaEncoding.Probing
                     stream.RefFrames = streamInfo.Refs;
                 }
 
+                if (!string.IsNullOrEmpty(streamInfo.ColorRange))
+                {
+                    stream.ColorRange = streamInfo.ColorRange;
+                }
+
+                if (!string.IsNullOrEmpty(streamInfo.ColorSpace))
+                {
+                    stream.ColorSpace = streamInfo.ColorSpace;
+                }
+
                 if (!string.IsNullOrEmpty(streamInfo.ColorTransfer))
                 {
                     stream.ColorTransfer = streamInfo.ColorTransfer;

+ 34 - 1
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Configuration
 
         public double DownMixAudioBoost { get; set; }
 
+        public int MaxMuxingQueueSize { get; set; }
+
         public bool EnableThrottling { get; set; }
 
         public int ThrottleDelaySeconds { get; set; }
@@ -29,12 +31,30 @@ namespace MediaBrowser.Model.Configuration
 
         public string VaapiDevice { get; set; }
 
+        public string OpenclDevice { get; set; }
+
+        public bool EnableTonemapping { get; set; }
+
+        public string TonemappingAlgorithm { get; set; }
+
+        public string TonemappingRange { get; set; }
+
+        public double TonemappingDesat { get; set; }
+
+        public double TonemappingThreshold { get; set; }
+
+        public double TonemappingPeak { get; set; }
+
+        public double TonemappingParam { get; set; }
+
         public int H264Crf { get; set; }
 
         public int H265Crf { get; set; }
 
         public string EncoderPreset { get; set; }
 
+        public bool DeinterlaceDoubleRate { get; set; }
+
         public string DeinterlaceMethod { get; set; }
 
         public bool EnableDecodingColorDepth10Hevc { get; set; }
@@ -50,13 +70,26 @@ namespace MediaBrowser.Model.Configuration
         public EncodingOptions()
         {
             DownMixAudioBoost = 2;
+            MaxMuxingQueueSize = 2048;
             EnableThrottling = false;
             ThrottleDelaySeconds = 180;
             EncodingThreadCount = -1;
-            // This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything
+            // This is a DRM device that is almost guaranteed to be there on every intel platform,
+            // plus it's the default one in ffmpeg if you don't specify anything
             VaapiDevice = "/dev/dri/renderD128";
+            // This is the OpenCL device that is used for tonemapping.
+            // The left side of the dot is the platform number, and the right side is the device number on the platform.
+            OpenclDevice = "0.0";
+            EnableTonemapping = false;
+            TonemappingAlgorithm = "reinhard";
+            TonemappingRange = "auto";
+            TonemappingDesat = 0;
+            TonemappingThreshold = 0.8;
+            TonemappingPeak = 0;
+            TonemappingParam = 0;
             H264Crf = 23;
             H265Crf = 28;
+            DeinterlaceDoubleRate = false;
             DeinterlaceMethod = "yadif";
             EnableDecodingColorDepth10Hevc = true;
             EnableDecodingColorDepth10Vp9 = true;

+ 12 - 6
MediaBrowser.Model/Entities/MediaStream.cs

@@ -35,6 +35,18 @@ namespace MediaBrowser.Model.Entities
         /// <value>The language.</value>
         public string Language { get; set; }
 
+        /// <summary>
+        /// Gets or sets the color range.
+        /// </summary>
+        /// <value>The color range.</value>
+        public string ColorRange { get; set; }
+
+        /// <summary>
+        /// Gets or sets the color space.
+        /// </summary>
+        /// <value>The color space.</value>
+        public string ColorSpace { get; set; }
+
         /// <summary>
         /// Gets or sets the color transfer.
         /// </summary>
@@ -47,12 +59,6 @@ namespace MediaBrowser.Model.Entities
         /// <value>The color primaries.</value>
         public string ColorPrimaries { get; set; }
 
-        /// <summary>
-        /// Gets or sets the color space.
-        /// </summary>
-        /// <value>The color space.</value>
-        public string ColorSpace { get; set; }
-
         /// <summary>
         /// Gets or sets the comment.
         /// </summary>

+ 1 - 1
MediaBrowser.Model/IO/FileSystemMetadata.cs

@@ -56,7 +56,7 @@ namespace MediaBrowser.Model.IO
         public DateTime CreationTimeUtc { get; set; }
 
         /// <summary>
-        /// Gets a value indicating whether this instance is directory.
+        /// Gets or sets a value indicating whether this instance is directory.
         /// </summary>
         /// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value>
         public bool IsDirectory { get; set; }

+ 2 - 2
MediaBrowser.Model/IO/IFileSystem.cs

@@ -201,9 +201,9 @@ namespace MediaBrowser.Model.IO
         IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false);
 
         void SetHidden(string path, bool isHidden);
-        void SetReadOnly(string path, bool readOnly);
+
         void SetAttributes(string path, bool isHidden, bool readOnly);
+
         List<FileSystemMetadata> GetDrives();
-        void SetExecutable(string path);
     }
 }

+ 0 - 1
MediaBrowser.Model/IO/IShortcutHandler.cs

@@ -22,7 +22,6 @@ namespace MediaBrowser.Model.IO
         /// </summary>
         /// <param name="shortcutPath">The shortcut path.</param>
         /// <param name="targetPath">The target path.</param>
-        /// <returns>System.String.</returns>
         void Create(string shortcutPath, string targetPath);
     }
 }

+ 0 - 2
MediaBrowser.Model/IO/IStreamHelper.cs

@@ -13,8 +13,6 @@ namespace MediaBrowser.Model.IO
 
         Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken);
 
-        Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken);
-
         Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken);
 
         Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken);

+ 1 - 0
MediaBrowser.Model/IO/IZipClient.cs

@@ -26,6 +26,7 @@ namespace MediaBrowser.Model.IO
         void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles);
 
         void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles);
+
         void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName);
 
         /// <summary>

+ 5 - 3
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs

@@ -32,9 +32,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
             _httpClientFactory = httpClientFactory;
         }
 
+        public static string ProviderName => TmdbUtils.ProviderName;
+
+        /// <inheritdoc />
         public string Name => ProviderName;
 
-        public static string ProviderName => TmdbUtils.ProviderName;
+        /// <inheritdoc />
+        public int Order => 0;
 
         public bool Supports(BaseItem item)
         {
@@ -126,8 +130,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
             return profile.Iso_639_1?.ToString();
         }
 
-        public int Order => 0;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);

+ 13 - 5
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs

@@ -33,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
     {
         const string DataFileName = "info.json";
 
-        internal static TmdbPersonProvider Current { get; private set; }
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
@@ -56,6 +56,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
             Current = this;
         }
 
+        internal static TmdbPersonProvider Current { get; private set; }
+
         public string Name => TmdbUtils.ProviderName;
 
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
@@ -96,7 +98,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
                 return new List<RemoteSearchResult>();
             }
 
-            var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey);
+            var url = string.Format(
+                CultureInfo.InvariantCulture,
+                TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}",
+                WebUtility.UrlEncode(searchInfo.Name),
+                TmdbUtils.ApiKey);
 
             using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
             foreach (var header in TmdbUtils.AcceptHeaders)
@@ -201,8 +207,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
             return result;
         }
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
         /// <summary>
         /// Gets the TMDB id.
         /// </summary>
@@ -227,7 +231,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
                 return;
             }
 
-            var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id);
+            var url = string.Format(
+                CultureInfo.InvariantCulture,
+                TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids",
+                TmdbUtils.ApiKey,
+                id);
 
             using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
             foreach (var header in TmdbUtils.AcceptHeaders)

+ 1 - 1
debian/rules

@@ -40,7 +40,7 @@ override_dh_clistrip:
 
 override_dh_auto_build:
 	dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
-		"-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
+		"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
 
 override_dh_auto_clean:
 	dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true

+ 1 - 1
deployment/Dockerfile.docker.amd64

@@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"

+ 1 - 1
deployment/Dockerfile.docker.arm64

@@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"

+ 1 - 1
deployment/Dockerfile.docker.armhf

@@ -12,4 +12,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 
 # because of changes in docker and systemd we need to not build in parallel at the moment
 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"

+ 1 - 1
deployment/build.linux.amd64

@@ -16,7 +16,7 @@ else
 fi
 
 # Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version}
 rm -rf dist/jellyfin-server_${version}
 

+ 1 - 1
deployment/build.macos

@@ -16,7 +16,7 @@ else
 fi
 
 # Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version}
 rm -rf dist/jellyfin-server_${version}
 

+ 1 - 1
deployment/build.portable

@@ -16,7 +16,7 @@ else
 fi
 
 # Build archives
-dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version}
 rm -rf dist/jellyfin-server_${version}
 

+ 1 - 1
deployment/build.windows.amd64

@@ -23,7 +23,7 @@ fi
 output_dir="dist/jellyfin-server_${version}"
 
 # Build binary
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
 
 # Prepare addins
 addin_build_dir="$( mktemp -d )"

+ 1 - 1
fedora/jellyfin.spec

@@ -54,7 +54,7 @@ The Jellyfin media server backend.
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
 dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
-    "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
+    "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
 %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
 %{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
 %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json

+ 1 - 1
windows/build-jellyfin.ps1

@@ -40,7 +40,7 @@ function Build-JellyFin {
     Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture"
     Write-Verbose "InstallLocation: $ResolvedInstallLocation"
     Write-Verbose "DotNetVerbosity: $DotNetVerbosity"
-    dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server
+    dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=true -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server
 }
 
 function Install-FFMPEG {