Selaa lähdekoodia

Merge branch 'master' into output-formatters

Claus Vium 4 vuotta sitten
vanhempi
sitoutus
81c764e87f
59 muutettua tiedostoa jossa 330 lisäystä ja 385 poistoa
  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. 13 8
      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. 14 9
      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. 1 3
      Jellyfin.Api/Controllers/DisplayPreferencesController.cs
  29. 6 3
      Jellyfin.Api/Controllers/DlnaServerController.cs
  30. 6 1
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  31. 11 25
      Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
  32. 4 6
      Jellyfin.Server/CoreAppHost.cs
  33. 2 0
      Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
  34. 0 36
      Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs
  35. 10 0
      Jellyfin.Server/Jellyfin.Server.csproj
  36. 3 2
      Jellyfin.Server/Startup.cs
  37. 0 0
      Jellyfin.Server/wwwroot/api-docs/redoc/custom.css
  38. 0 0
      Jellyfin.Server/wwwroot/api-docs/swagger/custom.css
  39. 2 9
      MediaBrowser.Controller/IDisplayPreferencesManager.cs
  40. 1 0
      MediaBrowser.Controller/LiveTv/ChannelInfo.cs
  41. 31 20
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  42. 6 0
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  43. 1 1
      MediaBrowser.Model/IO/FileSystemMetadata.cs
  44. 2 2
      MediaBrowser.Model/IO/IFileSystem.cs
  45. 0 1
      MediaBrowser.Model/IO/IShortcutHandler.cs
  46. 0 2
      MediaBrowser.Model/IO/IStreamHelper.cs
  47. 1 0
      MediaBrowser.Model/IO/IZipClient.cs
  48. 5 3
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
  49. 13 5
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
  50. 1 1
      debian/rules
  51. 1 1
      deployment/Dockerfile.docker.amd64
  52. 1 1
      deployment/Dockerfile.docker.arm64
  53. 1 1
      deployment/Dockerfile.docker.armhf
  54. 1 1
      deployment/build.linux.amd64
  55. 1 1
      deployment/build.macos
  56. 1 1
      deployment/build.portable
  57. 1 1
      deployment/build.windows.amd64
  58. 1 1
      fedora/jellyfin.spec
  59. 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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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

@@ -279,6 +279,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)
@@ -308,16 +312,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.
@@ -1401,7 +1405,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;

+ 13 - 8
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);
@@ -72,7 +72,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 UserAgent = "Emby/3.0",
 
                 // Shouldn't matter but may cause issues
-                DecompressionMethod = CompressionMethods.None
+                DecompressionMethod = CompressionMethods.None,
+                CancellationToken = cancellationToken
             };
 
             using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false))
@@ -88,10 +89,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     _logger.LogInformation("Copying recording stream to file {0}", targetFile);
 
                     // The media source if infinite so we need to handle stopping ourselves
-                    var durationToken = new CancellationTokenSource(duration);
-                    cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
-
-                    await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false);
+                    using var durationToken = new CancellationTokenSource(duration);
+                    using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
+
+                    await _streamHelper.CopyUntilCancelled(
+                        response.Content,
+                        output,
+                        IODefaults.CopyToBufferSize,
+                        cancellationTokenSource.Token).ConfigureAwait(false);
                 }
             }
 

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

@@ -604,11 +604,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();
@@ -808,11 +803,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))
@@ -1015,16 +1005,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))
@@ -1654,7 +1634,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, _httpClient, _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)
             {

+ 14 - 9
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -24,14 +24,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 IHttpClient _httpClient;
         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,
@@ -61,7 +61,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);
             }
 
@@ -367,13 +367,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;
                 }
@@ -587,7 +588,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return null;
             }
 
-            NameValuePair savedToken = null;
+            NameValuePair savedToken;
             if (!_tokens.TryGetValue(username, out savedToken))
             {
                 savedToken = new NameValuePair();
@@ -633,7 +634,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
         }
 
-        private async Task<HttpResponseInfo> Post(HttpRequestOptions options,
+        private async Task<HttpResponseInfo> Post(
+            HttpRequestOptions options,
             bool enableRetry,
             ListingsProviderInfo providerInfo)
         {
@@ -663,7 +665,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return await Post(options, false, providerInfo).ConfigureAwait(false);
         }
 
-        private async Task<HttpResponseInfo> Get(HttpRequestOptions options,
+        private async Task<HttpResponseInfo> Get(
+            HttpRequestOptions options,
             bool enableRetry,
             ListingsProviderInfo providerInfo)
         {
@@ -693,7 +696,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return await Get(options, false, providerInfo).ConfigureAwait(false);
         }
 
-        private async Task<string> GetTokenInternal(string username, string password,
+        private async Task<string> GetTokenInternal(
+            string username,
+            string password,
             CancellationToken cancellationToken)
         {
             var httpOptions = new HttpRequestOptions()

+ 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);
         }
     }
 }

+ 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();
         }

+ 6 - 3
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -60,7 +60,8 @@ 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")]
+        [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")]
@@ -75,7 +76,8 @@ 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")]
+        [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")]
@@ -90,7 +92,8 @@ 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")]
+        [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")]

+ 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");
                 });
         }
 

+ 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


+ 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>

+ 31 - 20
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -2090,6 +2090,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             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;
+
             // When the input may or may not be hardware VAAPI decodable
             if (isVaapiH264Encoder)
             {
@@ -2136,35 +2139,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 +2403,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";
+                            }
                         }
                     }
                 }

+ 6 - 0
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; }
@@ -35,6 +37,8 @@ namespace MediaBrowser.Model.Configuration
 
         public string EncoderPreset { get; set; }
 
+        public bool DeinterlaceDoubleRate { get; set; }
+
         public string DeinterlaceMethod { get; set; }
 
         public bool EnableDecodingColorDepth10Hevc { get; set; }
@@ -50,6 +54,7 @@ namespace MediaBrowser.Model.Configuration
         public EncodingOptions()
         {
             DownMixAudioBoost = 2;
+            MaxMuxingQueueSize = 2048;
             EnableThrottling = false;
             ThrottleDelaySeconds = 180;
             EncodingThreadCount = -1;
@@ -57,6 +62,7 @@ namespace MediaBrowser.Model.Configuration
             VaapiDevice = "/dev/dri/renderD128";
             H264Crf = 23;
             H265Crf = 28;
+            DeinterlaceDoubleRate = false;
             DeinterlaceMethod = "yadif";
             EnableDecodingColorDepth10Hevc = true;
             EnableDecodingColorDepth10Vp9 = true;

+ 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

@@ -31,9 +31,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)
         {
@@ -125,8 +129,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().GetAsync(url, cancellationToken);

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

@@ -32,7 +32,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;
@@ -55,6 +55,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)
@@ -95,7 +97,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)
@@ -200,8 +206,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
             return result;
         }
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
         /// <summary>
         /// Gets the TMDB id.
         /// </summary>
@@ -226,7 +230,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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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:GenerateDocumentationFile=true;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 {