소스 검색

Merge branch 'master' into NetworkPR2

BaronGreenback 4 년 전
부모
커밋
89e67b2e7f
82개의 변경된 파일591개의 추가작업 그리고 349개의 파일을 삭제
  1. 1 0
      CONTRIBUTORS.md
  2. 8 8
      Emby.Dlna/DlnaManager.cs
  3. BIN
      Emby.Dlna/Images/logo240.jpg
  4. BIN
      Emby.Dlna/Images/people48.png
  5. 41 46
      Emby.Dlna/PlayTo/PlayToController.cs
  6. 2 7
      Emby.Naming/AudioBook/AudioBookFilePathParser.cs
  7. 2 1
      Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
  8. 119 1
      Emby.Server.Implementations/ApplicationHost.cs
  9. 3 1
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  10. 0 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  11. 1 1
      Emby.Server.Implementations/Localization/Core/fr.json
  12. 9 1
      Emby.Server.Implementations/Localization/Core/gl.json
  13. 2 2
      Emby.Server.Implementations/Localization/Core/ko.json
  14. 1 1
      Emby.Server.Implementations/Localization/Core/nb.json
  15. 1 1
      Emby.Server.Implementations/Localization/Core/ta.json
  16. 10 10
      Emby.Server.Implementations/Localization/Core/vi.json
  17. 1 1
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  18. 1 0
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  19. 60 0
      Emby.Server.Implementations/Plugins/PluginManifest.cs
  20. 2 2
      Emby.Server.Implementations/Session/SessionManager.cs
  21. 26 8
      Emby.Server.Implementations/Updates/InstallationManager.cs
  22. 22 34
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  23. 1 1
      Jellyfin.Api/Controllers/LiveTvController.cs
  24. 24 23
      Jellyfin.Api/Controllers/SessionController.cs
  25. 5 3
      Jellyfin.Api/Controllers/SubtitleController.cs
  26. 2 2
      Jellyfin.Api/Controllers/VideosController.cs
  27. 1 1
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  28. 5 0
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  29. 2 2
      Jellyfin.Api/Jellyfin.Api.csproj
  30. 2 2
      Jellyfin.Data/Jellyfin.Data.csproj
  31. 2 2
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  32. 2 2
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  33. 4 4
      Jellyfin.Server/Jellyfin.Server.csproj
  34. 3 3
      MediaBrowser.Common/MediaBrowser.Common.csproj
  35. 1 5
      MediaBrowser.Controller/Extensions/StringExtensions.cs
  36. 2 1
      MediaBrowser.Controller/Library/NameExtensions.cs
  37. 2 2
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  38. 4 1
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  39. 1 0
      MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
  40. 1 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  41. 1 1
      MediaBrowser.Model/Session/GeneralCommand.cs
  42. 4 15
      MediaBrowser.Providers/Manager/MetadataService.cs
  43. 3 3
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  44. 1 1
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
  45. 1 1
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
  46. 1 1
      MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
  47. 3 2
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
  48. 3 8
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
  49. 15 13
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
  50. 6 1
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
  51. 9 0
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
  52. 2 2
      MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
  53. 4 7
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
  54. 14 9
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
  55. 8 7
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
  56. 9 4
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
  57. 3 2
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
  58. 16 7
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
  59. 11 17
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
  60. 16 21
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
  61. 4 4
      MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
  62. 8 8
      MediaBrowser.Providers/Studios/StudiosImageProvider.cs
  63. 1 1
      MediaBrowser.Providers/Subtitles/SubtitleManager.cs
  64. 11 4
      MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
  65. 3 3
      MediaBrowser.Providers/TV/SeasonMetadataService.cs
  66. 1 1
      README.md
  67. 2 2
      RSSDP/DisposableManagedObjectBase.cs
  68. 1 1
      deployment/Dockerfile.debian.amd64
  69. 1 1
      deployment/Dockerfile.debian.arm64
  70. 1 1
      deployment/Dockerfile.debian.armhf
  71. 1 1
      deployment/Dockerfile.linux.amd64
  72. 1 1
      deployment/Dockerfile.macos
  73. 1 1
      deployment/Dockerfile.portable
  74. 1 1
      deployment/Dockerfile.ubuntu.amd64
  75. 1 1
      deployment/Dockerfile.ubuntu.arm64
  76. 1 1
      deployment/Dockerfile.ubuntu.armhf
  77. 1 1
      deployment/Dockerfile.windows.amd64
  78. 4 0
      fedora/jellyfin.spec
  79. 2 2
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  80. 30 0
      tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs
  81. 8 8
      tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
  82. 1 0
      tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs

+ 1 - 0
CONTRIBUTORS.md

@@ -135,6 +135,7 @@
  - [YouKnowBlom](https://github.com/YouKnowBlom)
  - [KristupasSavickas](https://github.com/KristupasSavickas)
  - [Pusta](https://github.com/pusta)
+ - [nielsvanvelzen](https://github.com/nielsvanvelzen)
 
 # Emby Contributors
 

+ 8 - 8
Emby.Dlna/DlnaManager.cs

@@ -126,14 +126,14 @@ namespace Emby.Dlna
             var builder = new StringBuilder();
 
             builder.AppendLine("No matching device profile found. The default will need to be used.");
-            builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
-            builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
+            builder.Append("FriendlyName:").AppendLine(profile.FriendlyName);
+            builder.Append("Manufacturer:").AppendLine(profile.Manufacturer);
+            builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl);
+            builder.Append("ModelDescription:").AppendLine(profile.ModelDescription);
+            builder.Append("ModelName:").AppendLine(profile.ModelName);
+            builder.Append("ModelNumber:").AppendLine(profile.ModelNumber);
+            builder.Append("ModelUrl:").AppendLine(profile.ModelUrl);
+            builder.Append("SerialNumber:").AppendLine(profile.SerialNumber);
 
             _logger.LogInformation(builder.ToString());
         }

BIN
Emby.Dlna/Images/logo240.jpg


BIN
Emby.Dlna/Images/people48.png


+ 41 - 46
Emby.Dlna/PlayTo/PlayToController.cs

@@ -669,62 +669,57 @@ namespace Emby.Dlna.PlayTo
 
         private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
         {
-            if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType))
-            {
-                switch (commandType)
-                {
-                    case GeneralCommandType.VolumeDown:
-                        return _device.VolumeDown(cancellationToken);
-                    case GeneralCommandType.VolumeUp:
-                        return _device.VolumeUp(cancellationToken);
-                    case GeneralCommandType.Mute:
-                        return _device.Mute(cancellationToken);
-                    case GeneralCommandType.Unmute:
-                        return _device.Unmute(cancellationToken);
-                    case GeneralCommandType.ToggleMute:
-                        return _device.ToggleMute(cancellationToken);
-                    case GeneralCommandType.SetAudioStreamIndex:
-                        if (command.Arguments.TryGetValue("Index", out string index))
+            switch (command.Name)
+            {
+                case GeneralCommandType.VolumeDown:
+                    return _device.VolumeDown(cancellationToken);
+                case GeneralCommandType.VolumeUp:
+                    return _device.VolumeUp(cancellationToken);
+                case GeneralCommandType.Mute:
+                    return _device.Mute(cancellationToken);
+                case GeneralCommandType.Unmute:
+                    return _device.Unmute(cancellationToken);
+                case GeneralCommandType.ToggleMute:
+                    return _device.ToggleMute(cancellationToken);
+                case GeneralCommandType.SetAudioStreamIndex:
+                    if (command.Arguments.TryGetValue("Index", out string index))
+                    {
+                        if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
                         {
-                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
-                            {
-                                return SetAudioStreamIndex(val);
-                            }
-
-                            throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+                            return SetAudioStreamIndex(val);
                         }
 
-                        throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
-                    case GeneralCommandType.SetSubtitleStreamIndex:
-                        if (command.Arguments.TryGetValue("Index", out index))
-                        {
-                            if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
-                            {
-                                return SetSubtitleStreamIndex(val);
-                            }
+                        throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
+                    }
 
-                            throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+                    throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
+                case GeneralCommandType.SetSubtitleStreamIndex:
+                    if (command.Arguments.TryGetValue("Index", out index))
+                    {
+                        if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+                        {
+                            return SetSubtitleStreamIndex(val);
                         }
 
-                        throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
-                    case GeneralCommandType.SetVolume:
-                        if (command.Arguments.TryGetValue("Volume", out string vol))
-                        {
-                            if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
-                            {
-                                return _device.SetVolume(volume, cancellationToken);
-                            }
+                        throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
+                    }
 
-                            throw new ArgumentException("Unsupported volume value supplied.");
+                    throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
+                case GeneralCommandType.SetVolume:
+                    if (command.Arguments.TryGetValue("Volume", out string vol))
+                    {
+                        if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+                        {
+                            return _device.SetVolume(volume, cancellationToken);
                         }
 
-                        throw new ArgumentException("Volume argument cannot be null");
-                    default:
-                        return Task.CompletedTask;
-                }
-            }
+                        throw new ArgumentException("Unsupported volume value supplied.");
+                    }
 
-            return Task.CompletedTask;
+                    throw new ArgumentException("Volume argument cannot be null");
+                default:
+                    return Task.CompletedTask;
+            }
         }
 
         private async Task SetAudioStreamIndex(int? newIndex)

+ 2 - 7
Emby.Naming/AudioBook/AudioBookFilePathParser.cs

@@ -1,6 +1,6 @@
+#nullable enable
 #pragma warning disable CS1591
 
-using System;
 using System.Globalization;
 using System.IO;
 using System.Text.RegularExpressions;
@@ -19,12 +19,7 @@ namespace Emby.Naming.AudioBook
 
         public AudioBookFilePathParserResult Parse(string path)
         {
-            if (path == null)
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
-            var result = new AudioBookFilePathParserResult();
+            AudioBookFilePathParserResult result = default;
             var fileName = Path.GetFileNameWithoutExtension(path);
             foreach (var expression in _options.AudioBookPartsExpressions)
             {

+ 2 - 1
Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs

@@ -1,8 +1,9 @@
+#nullable enable
 #pragma warning disable CS1591
 
 namespace Emby.Naming.AudioBook
 {
-    public class AudioBookFilePathParserResult
+    public struct AudioBookFilePathParserResult
     {
         public int? PartNumber { get; set; }
 

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
@@ -37,6 +38,7 @@ using Emby.Server.Implementations.LiveTv;
 using Emby.Server.Implementations.Localization;
 using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.Plugins;
 using Emby.Server.Implementations.QuickConnect;
 using Emby.Server.Implementations.ScheduledTasks;
 using Emby.Server.Implementations.Security;
@@ -120,6 +122,7 @@ namespace Emby.Server.Implementations
 
         private readonly IFileSystem _fileSystemManager;
         private readonly IXmlSerializer _xmlSerializer;
+        private readonly IJsonSerializer _jsonSerializer;
         private readonly IStartupOptions _startupOptions;
 
         private IMediaEncoder _mediaEncoder;
@@ -259,6 +262,8 @@ namespace Emby.Server.Implementations
             IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
+            _jsonSerializer = new JsonSerializer();            
+            
             ServiceCollection = serviceCollection;
 
             ApplicationPaths = applicationPaths;
@@ -1012,6 +1017,119 @@ namespace Emby.Server.Implementations
 
         protected abstract void RestartInternal();
 
+        /// <summary>
+        /// Comparison function used in <see cref="GetPlugins" />.
+        /// </summary>
+        /// <param name="a">Item to compare.</param>
+        /// <param name="b">Item to compare with.</param>
+        /// <returns>Boolean result of the operation.</returns>
+        private static int VersionCompare(
+            (Version PluginVersion, string Name, string Path) a,
+            (Version PluginVersion, string Name, string Path) b)
+        {
+            int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+            if (compare == 0)
+            {
+                return a.PluginVersion.CompareTo(b.PluginVersion);
+            }
+
+            return compare;
+        }
+
+        /// <summary>
+        /// Returns a list of plugins to install.
+        /// </summary>
+        /// <param name="path">Path to check.</param>
+        /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
+        /// <returns>Enumerable list of dlls to load.</returns>
+        private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+        {
+            var dllList = new List<string>();
+            var versions = new List<(Version PluginVersion, string Name, string Path)>();
+            var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
+            string metafile;
+
+            foreach (var dir in directories)
+            {
+                try
+                {
+                    metafile = Path.Combine(dir, "meta.json");
+                    if (File.Exists(metafile))
+                    {
+                        var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
+
+                        if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+                        {
+                            targetAbi = new Version(0, 0, 0, 1);
+                        }
+
+                        if (!Version.TryParse(manifest.Version, out var version))
+                        {
+                            version = new Version(0, 0, 0, 1);
+                        }
+
+                        if (ApplicationVersion >= targetAbi)
+                        {
+                            // Only load Plugins if the plugin is built for this version or below.
+                            versions.Add((version, manifest.Name, dir));
+                        }
+                    }
+                    else
+                    {
+                        // No metafile, so lets see if the folder is versioned.
+                        metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
+                        
+                        int versionIndex = dir.LastIndexOf('_');
+                        if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+                        {
+                            // Versioned folder.
+                            versions.Add((ver, metafile, dir));
+                        }
+                        else
+                        {
+                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.                        
+                            versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+                        }   
+                    }
+                }
+                catch
+                {
+                    continue;
+                }
+            }
+
+            string lastName = string.Empty;
+            versions.Sort(VersionCompare);
+            // Traverse backwards through the list.
+            // The first item will be the latest version.
+            for (int x = versions.Count - 1; x >= 0; x--)
+            {
+                if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
+                {
+                    dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+                    lastName = versions[x].Name;
+                    continue;
+                }
+
+                if (!string.IsNullOrEmpty(lastName) && cleanup)
+                {
+                    // Attempt a cleanup of old folders.
+                    try
+                    {
+                        Logger.LogDebug("Deleting {Path}", versions[x].Path);
+                        Directory.Delete(versions[x].Path, true);
+                    }
+                    catch (Exception e)
+                    {
+                        Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
+                    }
+                }
+            }
+
+            return dllList;
+        }
+
         /// <summary>
         /// Gets the composable part assemblies.
         /// </summary>
@@ -1020,7 +1138,7 @@ namespace Emby.Server.Implementations
         {
             if (Directory.Exists(ApplicationPaths.PluginsPath))
             {
-                foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
+                foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
                 {
                     Assembly plugAss;
                     try

+ 3 - 1
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data
         {
             if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
             {
-                bindParam.Bind(value.ToByteArray());
+                Span<byte> byteValue = stackalloc byte[16];
+                value.TryWriteBytes(byteValue);
+                bindParam.Bind(byteValue);
             }
             else
             {

+ 0 - 4
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -32,10 +32,6 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
     <PackageReference Include="Mono.Nat" Version="3.0.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />

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

@@ -107,7 +107,7 @@
     "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
     "TaskCleanLogs": "Nettoyer le répertoire des journaux",
     "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
-    "TaskRefreshLibrary": "Scanner toute les Bibliothèques",
+    "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
     "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
     "TaskRefreshChapterImages": "Extraire les images de chapitre",
     "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

+ 9 - 1
Emby.Server.Implementations/Localization/Core/gl.json

@@ -1,3 +1,11 @@
 {
-    "Albums": "Álbumes"
+    "Albums": "Álbumes",
+    "Collections": "Colecións",
+    "ChapterNameValue": "Capítulos {0}",
+    "Channels": "Canles",
+    "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
+    "Books": "Libros",
+    "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
+    "Artists": "Artistas",
+    "Application": "Aplicativo"
 }

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

@@ -84,8 +84,8 @@
     "UserDeletedWithName": "사용자 {0} 삭제됨",
     "UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다",
     "UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다",
-    "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다",
-    "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다",
+    "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴",
+    "UserOnlineFromDevice": "{0}이 {1}으로 접속",
     "UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다",
     "UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다",
     "UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중",

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

@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "Lydavspilling startet",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
     "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
-    "NotificationOptionInstallationFailed": "Installasjonsfeil",
+    "NotificationOptionInstallationFailed": "Installasjonen feilet",
     "NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
     "NotificationOptionPluginError": "Pluginfeil",
     "NotificationOptionPluginInstalled": "Plugin installert",

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

@@ -26,7 +26,7 @@
     "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
     "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
     "Collections": "தொகுப்புகள்",
-    "CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
+    "CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
     "AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
     "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
     "TaskRefreshChannels": "சேனல்களை புதுப்பி",

+ 10 - 10
Emby.Server.Implementations/Localization/Core/vi.json

@@ -1,20 +1,20 @@
 {
     "Collections": "Bộ Sưu Tập",
-    "Favorites": "Sở Thích",
+    "Favorites": "Yêu Thích",
     "Folders": "Thư Mục",
     "Genres": "Thể Loại",
     "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
-    "HeaderContinueWatching": "Tiếp Tục Xem",
+    "HeaderContinueWatching": "Xem Tiếp",
     "HeaderLiveTV": "TV Trực Tiếp",
     "Movies": "Phim",
     "Photos": "Ảnh",
-    "Playlists": "Danh Sách Chơi",
-    "Shows": "Các Chương Trình",
+    "Playlists": "Danh sách phát",
+    "Shows": "Chương Trình TV",
     "Songs": "Các Bài Hát",
     "Sync": "Đồng Bộ",
     "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
-    "Albums": "Bộ Sưu Tập",
-    "Artists": "Nghệ Sĩ",
+    "Albums": "Albums",
+    "Artists": "Các Nghệ Sĩ",
     "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình thông tin chi tiết.",
     "TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu",
     "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
@@ -29,8 +29,8 @@
     "TaskCleanLogs": "Làm sạch nhật ký",
     "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
     "TaskRefreshLibrary": "Quét Thư viện Phương tiện",
-    "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho các video có chương.",
-    "TaskRefreshChapterImages": "Trích xuất hình ảnh chương",
+    "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
+    "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
     "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
     "TaskCleanCache": "Làm Sạch Thư Mục Cache",
     "TasksChannelsCategory": "Kênh Internet",
@@ -107,8 +107,8 @@
     "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
     "DeviceOnlineWithName": "{0} đã kết nối",
     "DeviceOfflineWithName": "{0} đã ngắt kết nối",
-    "ChapterNameValue": "Chương {0}",
-    "Channels": "Kênh",
+    "ChapterNameValue": "Phân Cảnh {0}",
+    "Channels": "Các Kênh",
     "CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
     "Books": "Sách",
     "AuthenticationSucceededWithUserName": "{0} xác thực thành công",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/zh-TW.json

@@ -96,7 +96,7 @@
     "TaskDownloadMissingSubtitles": "下載遺失的字幕",
     "TaskRefreshChannels": "重新整理頻道",
     "TaskUpdatePlugins": "更新外掛",
-    "TaskRefreshPeople": "重新整理人員",
+    "TaskRefreshPeople": "刷新用戶",
     "TaskCleanLogsDescription": "刪除超過 {0} 天的舊紀錄檔。",
     "TaskCleanLogs": "清空紀錄資料夾",
     "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。",

+ 1 - 0
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -413,6 +413,7 @@ namespace Emby.Server.Implementations.Localization
             yield return new LocalizationOption("Swedish", "sv");
             yield return new LocalizationOption("Swiss German", "gsw");
             yield return new LocalizationOption("Turkish", "tr");
+            yield return new LocalizationOption("Tiếng Việt", "vi");
         }
     }
 }

+ 60 - 0
Emby.Server.Implementations/Plugins/PluginManifest.cs

@@ -0,0 +1,60 @@
+using System;
+
+namespace Emby.Server.Implementations.Plugins
+{
+    /// <summary>
+    /// Defines a Plugin manifest file.
+    /// </summary>
+    public class PluginManifest
+    {
+        /// <summary>
+        /// Gets or sets the category of the plugin.
+        /// </summary>
+        public string Category { get; set; }
+
+        /// <summary>
+        /// Gets or sets the changelog information.
+        /// </summary>
+        public string Changelog { get; set; }
+
+        /// <summary>
+        /// Gets or sets the description of the plugin.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Global Unique Identifier for the plugin.
+        /// </summary>
+        public Guid Guid { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Name of the plugin.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets an overview of the plugin.
+        /// </summary>
+        public string Overview { get; set; }
+
+        /// <summary>
+        /// Gets or sets the owner of the plugin.
+        /// </summary>
+        public string Owner { get; set; }
+
+        /// <summary>
+        /// Gets or sets the compatibility version for the plugin.
+        /// </summary>
+        public string TargetAbi { get; set; }
+
+        /// <summary>
+        /// Gets or sets the timestamp of the plugin.
+        /// </summary>
+        public DateTime Timestamp { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Version number of the plugin.
+        /// </summary>
+        public string Version { get; set; }
+    }
+}

+ 2 - 2
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1037,7 +1037,7 @@ namespace Emby.Server.Implementations.Session
 
             var generalCommand = new GeneralCommand
             {
-                Name = GeneralCommandType.DisplayMessage.ToString()
+                Name = GeneralCommandType.DisplayMessage
             };
 
             generalCommand.Arguments["Header"] = command.Header;
@@ -1268,7 +1268,7 @@ namespace Emby.Server.Implementations.Session
         {
             var generalCommand = new GeneralCommand
             {
-                Name = GeneralCommandType.DisplayContent.ToString(),
+                Name = GeneralCommandType.DisplayContent,
                 Arguments =
                 {
                     ["ItemId"] = command.ItemId,

+ 26 - 8
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -15,12 +15,14 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
+using MediaBrowser.Common.System;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.Logging;
+using MediaBrowser.Model.System;
 
 namespace Emby.Server.Implementations.Updates
 {
@@ -377,11 +379,20 @@ namespace Emby.Server.Implementations.Updates
                 throw new InvalidDataException("The checksum of the received data doesn't match.");
             }
 
+            // Version folder as they cannot be overwritten in Windows.
+            targetDir += "_" + package.Version;
+
             if (Directory.Exists(targetDir))
             {
-                Directory.Delete(targetDir, true);
+                try
+                {
+                    Directory.Delete(targetDir, true);
+                }
+                catch
+                {
+                    // Ignore any exceptions.
+                }
             }
-
             stream.Position = 0;
             _zipClient.ExtractAllFromZip(stream, targetDir, true);
 
@@ -423,15 +434,22 @@ namespace Emby.Server.Implementations.Updates
                 path = file;
             }
 
-            if (isDirectory)
+            try
             {
-                _logger.LogInformation("Deleting plugin directory {0}", path);
-                Directory.Delete(path, true);
+                if (isDirectory)
+                {
+                    _logger.LogInformation("Deleting plugin directory {0}", path);
+                    Directory.Delete(path, true);
+                }
+                else
+                {
+                    _logger.LogInformation("Deleting plugin file {0}", path);
+                    _fileSystem.DeleteFile(path);
+                }
             }
-            else
+            catch
             {
-                _logger.LogInformation("Deleting plugin file {0}", path);
-                _fileSystem.DeleteFile(path);
+                // Ignore file errors.
             }
 
             var list = _config.Configuration.UninstalledPlugins.ToList();

+ 22 - 34
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Diagnostics.CodeAnalysis;
@@ -113,7 +113,6 @@ namespace Jellyfin.Api.Controllers
         /// Gets a video hls playlist stream.
         /// </summary>
         /// <param name="itemId">The item id.</param>
-        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
         /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
         /// <param name="params">The streaming parameters.</param>
         /// <param name="tag">The tag.</param>
@@ -170,7 +169,6 @@ namespace Jellyfin.Api.Controllers
         [ProducesPlaylistFile]
         public async Task<ActionResult> GetMasterHlsVideoPlaylist(
             [FromRoute, Required] Guid itemId,
-            [FromRoute, Required] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -223,7 +221,6 @@ namespace Jellyfin.Api.Controllers
             var streamingRequest = new HlsVideoRequestDto
             {
                 Id = itemId,
-                Container = container,
                 Static = @static ?? true,
                 Params = @params,
                 Tag = tag,
@@ -281,7 +278,6 @@ namespace Jellyfin.Api.Controllers
         /// Gets an audio hls playlist stream.
         /// </summary>
         /// <param name="itemId">The item id.</param>
-        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
         /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
         /// <param name="params">The streaming parameters.</param>
         /// <param name="tag">The tag.</param>
@@ -338,7 +334,6 @@ namespace Jellyfin.Api.Controllers
         [ProducesPlaylistFile]
         public async Task<ActionResult> GetMasterHlsAudioPlaylist(
             [FromRoute, Required] Guid itemId,
-            [FromQuery, Required] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -391,7 +386,6 @@ namespace Jellyfin.Api.Controllers
             var streamingRequest = new HlsAudioRequestDto
             {
                 Id = itemId,
-                Container = container,
                 Static = @static ?? true,
                 Params = @params,
                 Tag = tag,
@@ -449,7 +443,6 @@ namespace Jellyfin.Api.Controllers
         /// Gets a video stream using HTTP live streaming.
         /// </summary>
         /// <param name="itemId">The item id.</param>
-        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
         /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
         /// <param name="params">The streaming parameters.</param>
         /// <param name="tag">The tag.</param>
@@ -504,7 +497,6 @@ namespace Jellyfin.Api.Controllers
         [ProducesPlaylistFile]
         public async Task<ActionResult> GetVariantHlsVideoPlaylist(
             [FromRoute, Required] Guid itemId,
-            [FromQuery, Required] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -557,7 +549,6 @@ namespace Jellyfin.Api.Controllers
             var streamingRequest = new VideoRequestDto
             {
                 Id = itemId,
-                Container = container,
                 Static = @static ?? true,
                 Params = @params,
                 Tag = tag,
@@ -615,7 +606,6 @@ namespace Jellyfin.Api.Controllers
         /// Gets an audio stream using HTTP live streaming.
         /// </summary>
         /// <param name="itemId">The item id.</param>
-        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
         /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
         /// <param name="params">The streaming parameters.</param>
         /// <param name="tag">The tag.</param>
@@ -670,7 +660,6 @@ namespace Jellyfin.Api.Controllers
         [ProducesPlaylistFile]
         public async Task<ActionResult> GetVariantHlsAudioPlaylist(
             [FromRoute, Required] Guid itemId,
-            [FromQuery, Required] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -723,7 +712,6 @@ namespace Jellyfin.Api.Controllers
             var streamingRequest = new StreamingRequestDto
             {
                 Id = itemId,
-                Container = container,
                 Static = @static ?? true,
                 Params = @params,
                 Tag = tag,
@@ -841,7 +829,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] string playlistId,
             [FromRoute, Required] int segmentId,
-            [FromRoute, Required] string container,
+            [FromRoute] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -1011,7 +999,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] string playlistId,
             [FromRoute, Required] int segmentId,
-            [FromRoute, Required] string container,
+            [FromRoute] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -1144,30 +1132,30 @@ namespace Jellyfin.Api.Controllers
 
             var builder = new StringBuilder();
 
-            builder.AppendLine("#EXTM3U");
-            builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
-            builder.AppendLine("#EXT-X-VERSION:3");
-            builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
-            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+            builder.AppendLine("#EXTM3U")
+                .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+                .AppendLine("#EXT-X-VERSION:3")
+                .Append("#EXT-X-TARGETDURATION:")
+                .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
+                .AppendLine()
+                .AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
 
-            var queryString = Request.QueryString;
             var index = 0;
-
             var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
+            var queryString = Request.QueryString;
 
             foreach (var length in segmentLengths)
             {
-                builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
-                builder.AppendLine(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        "hls1/{0}/{1}{2}{3}",
-                        name,
-                        index.ToString(CultureInfo.InvariantCulture),
-                        segmentExtension,
-                        queryString));
-
-                index++;
+                builder.Append("#EXTINF:")
+                    .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
+                    .AppendLine(", nodesc")
+                    .Append("hls1/")
+                    .Append(name)
+                    .Append('/')
+                    .Append(index++)
+                    .Append(segmentExtension)
+                    .Append(queryString)
+                    .AppendLine();
             }
 
             builder.AppendLine("#EXT-X-ENDLIST");
@@ -1465,7 +1453,7 @@ namespace Jellyfin.Api.Controllers
 
             var args = "-codec:v:0 " + codec;
 
-            // if (state.EnableMpegtsM2TsMode)
+            // if  (state.EnableMpegtsM2TsMode)
             // {
             //     args += " -mpegts_m2ts_mode 1";
             // }

+ 1 - 1
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -1017,9 +1017,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool validateListings = false,
             [FromQuery] bool validateLogin = false)
         {
-            using var sha = SHA1.Create();
             if (!string.IsNullOrEmpty(pw))
             {
+                using var sha = SHA1.Create();
                 listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
             }
 

+ 24 - 23
Jellyfin.Api/Controllers/SessionController.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CA1801
-
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
@@ -150,25 +148,25 @@ namespace Jellyfin.Api.Controllers
         /// Instructs a session to play an item.
         /// </summary>
         /// <param name="sessionId">The session id.</param>
-        /// <param name="command">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
+        /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
         /// <param name="itemIds">The ids of the items to play, comma delimited.</param>
         /// <param name="startPositionTicks">The starting position of the first item.</param>
         /// <response code="204">Instruction sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("Sessions/{sessionId}/Playing/{command}")]
+        [HttpPost("Sessions/{sessionId}/Playing")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult Play(
             [FromRoute, Required] string sessionId,
-            [FromRoute, Required] PlayCommand command,
-            [FromQuery] Guid[] itemIds,
+            [FromQuery, Required] PlayCommand playCommand,
+            [FromQuery, Required] string itemIds,
             [FromQuery] long? startPositionTicks)
         {
             var playRequest = new PlayRequest
             {
-                ItemIds = itemIds,
+                ItemIds = RequestHelpers.GetGuids(itemIds),
                 StartPositionTicks = startPositionTicks,
-                PlayCommand = command
+                PlayCommand = playCommand
             };
 
             _sessionManager.SendPlayCommand(
@@ -184,20 +182,29 @@ namespace Jellyfin.Api.Controllers
         /// Issues a playstate command to a client.
         /// </summary>
         /// <param name="sessionId">The session id.</param>
-        /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param>
+        /// <param name="command">The <see cref="PlaystateCommand"/>.</param>
+        /// <param name="seekPositionTicks">The optional position ticks.</param>
+        /// <param name="controllingUserId">The optional controlling user id.</param>
         /// <response code="204">Playstate command sent to session.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpPost("Sessions/{sessionId}/Playing")]
+        [HttpPost("Sessions/{sessionId}/Playing/{command}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendPlaystateCommand(
             [FromRoute, Required] string sessionId,
-            [FromBody] PlaystateRequest playstateRequest)
+            [FromRoute, Required] PlaystateCommand command,
+            [FromQuery] long? seekPositionTicks,
+            [FromQuery] string? controllingUserId)
         {
             _sessionManager.SendPlaystateCommand(
                 RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
                 sessionId,
-                playstateRequest,
+                new PlaystateRequest()
+                {
+                    Command = command,
+                    ControllingUserId = controllingUserId,
+                    SeekPositionTicks = seekPositionTicks,
+                },
                 CancellationToken.None);
 
             return NoContent();
@@ -215,18 +222,12 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendSystemCommand(
             [FromRoute, Required] string sessionId,
-            [FromRoute, Required] string command)
+            [FromRoute, Required] GeneralCommandType command)
         {
-            var name = command;
-            if (Enum.TryParse(name, true, out GeneralCommandType commandType))
-            {
-                name = commandType.ToString();
-            }
-
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
             var generalCommand = new GeneralCommand
             {
-                Name = name,
+                Name = command,
                 ControllingUserId = currentSession.UserId
             };
 
@@ -247,7 +248,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SendGeneralCommand(
             [FromRoute, Required] string sessionId,
-            [FromRoute, Required] string command)
+            [FromRoute, Required] GeneralCommandType command)
         {
             var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
 
@@ -434,9 +435,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult ReportViewing(
             [FromQuery] string? sessionId,
-            [FromQuery] string? itemId)
+            [FromQuery, Required] string? itemId)
         {
-            string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+            string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
 
             _sessionManager.ReportNowViewingItem(session, itemId);
             return NoContent();

+ 5 - 3
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -281,7 +281,8 @@ namespace Jellyfin.Api.Controllers
             var builder = new StringBuilder();
             builder.AppendLine("#EXTM3U")
                 .Append("#EXT-X-TARGETDURATION:")
-                .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
+                .Append(segmentLength)
+                .AppendLine()
                 .AppendLine("#EXT-X-VERSION:3")
                 .AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
                 .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
@@ -296,8 +297,9 @@ namespace Jellyfin.Api.Controllers
                 var lengthTicks = Math.Min(remaining, segmentLengthTicks);
 
                 builder.Append("#EXTINF:")
-                    .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
-                    .AppendLine(",");
+                    .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
+                    .Append(',')
+                    .AppendLine();
 
                 var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
 

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

@@ -326,9 +326,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStream_2")]
+        [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
         [HttpGet("{itemId}/stream")]
-        [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
+        [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
         [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesVideoFile]

+ 1 - 1
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Helpers
 
             string? containerInternal = Path.GetExtension(state.RequestedUrl);
 
-            if (string.IsNullOrEmpty(streamingRequest.Container))
+            if (!string.IsNullOrEmpty(streamingRequest.Container))
             {
                 containerInternal = streamingRequest.Container;
             }

+ 5 - 0
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -504,6 +504,11 @@ namespace Jellyfin.Api.Helpers
                 }
             }
 
+            if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
+            {
+                throw new ArgumentException("FFMPEG path not set.");
+            }
+
             var process = new Process
             {
                 StartInfo = new ProcessStartInfo

+ 2 - 2
Jellyfin.Api/Jellyfin.Api.csproj

@@ -14,9 +14,9 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.7" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.8" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" />
   </ItemGroup>

+ 2 - 2
Jellyfin.Data/Jellyfin.Data.csproj

@@ -41,8 +41,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.7" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.8" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.8" />
   </ItemGroup>
 
   <ItemGroup>

+ 2 - 2
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -20,8 +20,8 @@
   <ItemGroup>
     <PackageReference Include="BlurHashSharp" Version="1.1.0" />
     <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
-    <PackageReference Include="SkiaSharp" Version="2.80.1" />
-    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
+    <PackageReference Include="SkiaSharp" Version="2.80.2" />
+    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" />
   </ItemGroup>
 
   <ItemGroup>

+ 2 - 2
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -24,11 +24,11 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.8">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.8">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 4 - 4
Jellyfin.Server/Jellyfin.Server.csproj

@@ -41,10 +41,10 @@
 
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
-    <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="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.8" />
     <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" />

+ 3 - 3
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -18,9 +18,9 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.7" />
-    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
     <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
     <PackageReference Include="NetworkCollection" Version="1.0.1" />
   </ItemGroup>

+ 1 - 5
MediaBrowser.Controller/Extensions/StringExtensions.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -15,11 +16,6 @@ namespace MediaBrowser.Controller.Extensions
     {
         public static string RemoveDiacritics(this string text)
         {
-            if (text == null)
-            {
-                throw new ArgumentNullException(nameof(text));
-            }
-
             var chars = Normalize(text, NormalizationForm.FormD)
                 .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark);
 

+ 2 - 1
MediaBrowser.Controller/Library/NameExtensions.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -9,7 +10,7 @@ namespace MediaBrowser.Controller.Library
 {
     public static class NameExtensions
     {
-        private static string RemoveDiacritics(string name)
+        private static string RemoveDiacritics(string? name)
         {
             if (name == null)
             {

+ 2 - 2
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -14,8 +14,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.8" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
   </ItemGroup>
 

+ 4 - 1
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -212,7 +212,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             if (match.Success)
             {
-                return new Version(match.Groups[1].Value);
+                if (Version.TryParse(match.Groups[1].Value, out var result))
+                {
+                    return result;
+                }
             }
 
             var versionMap = GetFFmpegLibraryVersions(output);

+ 1 - 0
MediaBrowser.Model/Dlna/ResolutionNormalizer.cs

@@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Dlna
                 new ResolutionConfiguration(720, 950000),
                 new ResolutionConfiguration(1280, 2500000),
                 new ResolutionConfiguration(1920, 4000000),
+                new ResolutionConfiguration(2560, 8000000),
                 new ResolutionConfiguration(3840, 35000000)
             };
 

+ 1 - 1
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -34,7 +34,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
     <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.8" />
     <PackageReference Include="System.Globalization" Version="4.3.0" />
     <PackageReference Include="System.Text.Json" Version="5.0.0-preview.8.20407.11" />
   </ItemGroup>

+ 1 - 1
MediaBrowser.Model/Session/GeneralCommand.cs

@@ -8,7 +8,7 @@ namespace MediaBrowser.Model.Session
 {
     public class GeneralCommand
     {
-        public string Name { get; set; }
+        public GeneralCommandType Name { get; set; }
 
         public Guid ControllingUserId { get; set; }
 

+ 4 - 15
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -297,7 +297,7 @@ namespace MediaBrowser.Providers.Manager
         }
 
         /// <summary>
-        /// Befores the save.
+        /// Before the save.
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
@@ -355,13 +355,12 @@ namespace MediaBrowser.Providers.Manager
 
         protected virtual IList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
         {
-            var folder = item as Folder;
-            if (folder != null)
+            if (item is Folder folder)
             {
                 return folder.GetRecursiveChildren();
             }
 
-            return new List<BaseItem>();
+            return Array.Empty<BaseItem>();
         }
 
         protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
@@ -814,7 +813,7 @@ namespace MediaBrowser.Providers.Manager
 
             try
             {
-                refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
+                refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
             }
             catch (OperationCanceledException)
             {
@@ -882,16 +881,6 @@ namespace MediaBrowser.Providers.Manager
             return refreshResult;
         }
 
-        private string NormalizeLanguage(string language)
-        {
-            if (string.IsNullOrWhiteSpace(language))
-            {
-                return "en";
-            }
-
-            return language;
-        }
-
         private void MergeNewData(TItemType source, TIdType lookupInfo)
         {
             // Copy new provider id's that may have been obtained

+ 3 - 3
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -16,9 +16,9 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.1.2" />
     <PackageReference Include="TvDbSharper" Version="3.2.1" />

+ 1 - 1
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs

@@ -6,6 +6,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
 {
     public class Videos
     {
-        public List<Video> Results { get; set; }
+        public IReadOnlyList<Video> Results { get; set; }
     }
 }

+ 1 - 1
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs

@@ -6,6 +6,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
 {
     public class Trailers
     {
-        public List<Youtube> Youtube { get; set; }
+        public IReadOnlyList<Youtube> Youtube { get; set; }
     }
 }

+ 1 - 1
MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs

@@ -7,6 +7,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
 {
     public class PersonImages
     {
-        public List<Profile> Profiles { get; set; }
+        public IReadOnlyList<Profile> Profiles { get; set; }
     }
 }

+ 3 - 2
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs

@@ -38,6 +38,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
         public static string ProviderName => TmdbUtils.ProviderName;
 
+        /// <inheritdoc />
+        public int Order => 0;
+
         public bool Supports(BaseItem item)
         {
             return item is Movie || item is MusicVideo || item is Trailer;
@@ -201,8 +204,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
             return null;
         }
 
-        public int Order => 0;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);

+ 3 - 8
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs → MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs

@@ -6,22 +6,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 {
     internal class TmdbImageSettings
     {
-        public List<string> backdrop_sizes { get; set; }
+        public IReadOnlyList<string> backdrop_sizes { get; set; }
 
         public string secure_base_url { get; set; }
 
-        public List<string> poster_sizes { get; set; }
+        public IReadOnlyList<string> poster_sizes { get; set; }
 
-        public List<string> profile_sizes { get; set; }
+        public IReadOnlyList<string> profile_sizes { get; set; }
 
         public string GetImageUrl(string image)
         {
             return secure_base_url + image;
         }
     }
-
-    internal class TmdbSettingsResult
-    {
-        public TmdbImageSettings images { get; set; }
-    }
 }

+ 15 - 13
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs

@@ -34,7 +34,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
         private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
 
-        internal static TmdbMovieProvider Current { get; private set; }
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IHttpClientFactory _httpClientFactory;
@@ -44,7 +44,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         private readonly ILibraryManager _libraryManager;
         private readonly IApplicationHost _appHost;
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        /// <summary>
+        /// The _TMDB settings task.
+        /// </summary>
+        private TmdbSettingsResult _tmdbSettings;
 
         public TmdbMovieProvider(
             IJsonSerializer jsonSerializer,
@@ -65,6 +68,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
             Current = this;
         }
 
+        internal static TmdbMovieProvider Current { get; private set; }
+
+        /// <inheritdoc />
+        public string Name => TmdbUtils.ProviderName;
+
+        /// <inheritdoc />
+        public int Order => 1;
+
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
         {
             return GetMovieSearchResults(searchInfo, cancellationToken);
@@ -131,13 +142,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
             return movieDb.GetMetadata(id, cancellationToken);
         }
 
-        public string Name => TmdbUtils.ProviderName;
-
-        /// <summary>
-        /// The _TMDB settings task.
-        /// </summary>
-        private TmdbSettingsResult _tmdbSettings;
-
         /// <summary>
         /// Gets the TMDB settings.
         /// </summary>
@@ -272,7 +276,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 languages.Add("en");
             }
 
-            return string.Join(",", languages);
+            return string.Join(',', languages);
         }
 
         public static string NormalizeLanguage(string language)
@@ -381,15 +385,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         /// <summary>
         /// Gets the movie db response.
         /// </summary>
+        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
         internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default)
         {
             message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent);
             return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken);
         }
 
-        /// <inheritdoc />
-        public int Order => 1;
-
         /// <inheritdoc />
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {

+ 6 - 1
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs

@@ -207,7 +207,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
             return results
                 .Select(i =>
                 {
-                    var remoteResult = new RemoteSearchResult {SearchProviderName = TmdbMovieProvider.Current.Name, Name = i.Title ?? i.Name ?? i.Original_Title, ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path};
+                    var remoteResult = new RemoteSearchResult
+                    {
+                        SearchProviderName = TmdbMovieProvider.Current.Name,
+                        Name = i.Title ?? i.Name ?? i.Original_Title,
+                        ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
+                    };
 
                     if (!string.IsNullOrWhiteSpace(i.Release_Date))
                     {

+ 9 - 0
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs

@@ -0,0 +1,9 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+    internal class TmdbSettingsResult
+    {
+        public TmdbImageSettings images { get; set; }
+    }
+}

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs

@@ -14,6 +14,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
 {
     public class TmdbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo>
     {
+        public string Name => TmdbMovieProvider.Current.Name;
+
         public Task<MetadataResult<MusicVideo>> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken)
         {
             return TmdbMovieProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken);
@@ -24,8 +26,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Music
             return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
         }
 
-        public string Name => TmdbMovieProvider.Current.Name;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();

+ 4 - 7
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs

@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
 {
     public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo>
     {
-        const string DataFileName = "info.json";
+        private const string DataFileName = "info.json";
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
@@ -39,20 +39,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IHttpClientFactory _httpClientFactory;
-        private readonly ILogger<TmdbPersonProvider> _logger;
 
         public TmdbPersonProvider(
             IFileSystem fileSystem,
             IServerConfigurationManager configurationManager,
             IJsonSerializer jsonSerializer,
-            IHttpClientFactory httpClientFactory,
-            ILogger<TmdbPersonProvider> logger)
+            IHttpClientFactory httpClientFactory)
         {
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _jsonSerializer = jsonSerializer;
             _httpClientFactory = httpClientFactory;
-            _logger = logger;
             Current = this;
         }
 
@@ -75,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
                 var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
                 var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
 
-                var images = (info.Images ?? new PersonImages()).Profiles ?? new List<Profile>();
+                IReadOnlyList<Profile> images = info.Images?.Profiles ?? Array.Empty<Profile>();
 
                 var result = new RemoteSearchResult
                 {
@@ -95,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
             if (searchInfo.IsAutomated)
             {
                 // Don't hammer moviedb searching by name
-                return new List<RemoteSearchResult>();
+                return Array.Empty<RemoteSearchResult>();
             }
 
             var url = string.Format(

+ 14 - 9
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs

@@ -28,7 +28,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     {
         public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
             : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
-        { }
+        {
+        }
+
+        public string Name => TmdbUtils.ProviderName;
+
+        // After TheTvDb
+        public int Order => 1;
 
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
         {
@@ -43,7 +49,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             var episode = (Controller.Entities.TV.Episode)item;
             var series = episode.Series;
 
-            var seriesId = series != null ? series.GetProviderId(MetadataProvider.Tmdb) : null;
+            var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
 
             var list = new List<RemoteImageInfo>();
 
@@ -62,8 +68,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             var language = item.GetPreferredMetadataLanguage();
 
-            var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value,
-                        language, cancellationToken).ConfigureAwait(false);
+            var response = await GetEpisodeInfo(
+                seriesId,
+                seasonNumber.Value,
+                episodeNumber.Value,
+                language,
+                cancellationToken).ConfigureAwait(false);
 
             var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
 
@@ -120,14 +130,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             return GetResponse(url, cancellationToken);
         }
 
-        public string Name => TmdbUtils.ProviderName;
-
         public bool Supports(BaseItem item)
         {
             return item is Controller.Entities.TV.Episode;
         }
-
-        // After TheTvDb
-        public int Order => 1;
     }
 }

+ 8 - 7
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs

@@ -29,7 +29,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     {
         public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
             : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
-        { }
+        {
+        }
+
+        // After TheTvDb
+        public int Order => 1;
+
+        public string Name => TmdbUtils.ProviderName;
 
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
         {
@@ -41,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 return list;
             }
 
-            var metadataResult = await GetMetadata(searchInfo, cancellationToken);
+            var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
 
             if (metadataResult.HasMetadata)
             {
@@ -205,10 +211,5 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         {
             return GetResponse(url, cancellationToken);
         }
-
-        // After TheTvDb
-        public int Order => 1;
-
-        public string Name => TmdbUtils.ProviderName;
     }
 }

+ 9 - 4
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs

@@ -21,11 +21,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     public abstract class TmdbEpisodeProviderBase
     {
         private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
+
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
-        private readonly ILocalizationManager _localization;
         private readonly ILogger<TmdbEpisodeProviderBase> _logger;
 
         protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
@@ -34,13 +34,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             _configurationManager = configurationManager;
             _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
-            _localization = localization;
             _logger = loggerFactory.CreateLogger<TmdbEpisodeProviderBase>();
         }
 
         protected ILogger Logger => _logger;
 
-        protected async Task<EpisodeResult> GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage,
+        protected async Task<EpisodeResult> GetEpisodeInfo(
+            string seriesTmdbId,
+            int season,
+            int episodeNumber,
+            string preferredMetadataLanguage,
             CancellationToken cancellationToken)
         {
             await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken)
@@ -93,7 +96,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
 
-            var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-episode-{1}-{2}.json",
+            var filename = string.Format(
+                CultureInfo.InvariantCulture,
+                "season-{0}-episode-{1}-{2}.json",
                 seasonNumber.ToString(CultureInfo.InvariantCulture),
                 episodeNumber.ToString(CultureInfo.InvariantCulture),
                 preferredLanguage);

+ 3 - 2
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs

@@ -112,9 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken)
         {
-            await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, item.IndexNumber.GetValueOrDefault(), language, cancellationToken).ConfigureAwait(false);
+            var seasonNumber = item.IndexNumber.GetValueOrDefault();
+            await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, seasonNumber, language, cancellationToken).ConfigureAwait(false);
 
-            var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
+            var path = TmdbSeasonProvider.Current.GetDataFilePath(tmdbId, seasonNumber, language);
 
             if (!string.IsNullOrEmpty(path))
             {

+ 16 - 7
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs

@@ -28,26 +28,32 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>
     {
         private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos";
+
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
-        private readonly ILocalizationManager _localization;
         private readonly ILogger<TmdbSeasonProvider> _logger;
 
         internal static TmdbSeasonProvider Current { get; private set; }
 
-        public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
+        public TmdbSeasonProvider(
+            IHttpClientFactory httpClientFactory,
+            IServerConfigurationManager configurationManager,
+            IFileSystem fileSystem,
+            IJsonSerializer jsonSerializer,
+            ILogger<TmdbSeasonProvider> logger)
         {
             _httpClientFactory = httpClientFactory;
             _configurationManager = configurationManager;
             _fileSystem = fileSystem;
-            _localization = localization;
             _jsonSerializer = jsonSerializer;
             _logger = logger;
             Current = this;
         }
 
+        public string Name => TmdbUtils.ProviderName;
+
         public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
         {
             var result = new MetadataResult<Season>();
@@ -116,8 +122,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             return result;
         }
 
-        public string Name => TmdbUtils.ProviderName;
-
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
         {
             return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
@@ -128,7 +132,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
         }
 
-        private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,
+        private async Task<SeasonResult> GetSeasonInfo(
+            string seriesTmdbId,
+            int season,
+            string preferredMetadataLanguage,
             CancellationToken cancellationToken)
         {
             await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken)
@@ -181,7 +188,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
 
-            var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-{1}.json",
+            var filename = string.Format(
+                CultureInfo.InvariantCulture,
+                "season-{0}-{1}.json",
                 seasonNumber.ToString(CultureInfo.InvariantCulture),
                 preferredLanguage);
 

+ 11 - 17
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Net.Http;
 using System.Threading;
@@ -12,7 +13,6 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
@@ -25,19 +25,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
     {
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IHttpClientFactory _httpClientFactory;
-        private readonly IFileSystem _fileSystem;
 
-        public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
+        public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
         {
             _jsonSerializer = jsonSerializer;
             _httpClientFactory = httpClientFactory;
-            _fileSystem = fileSystem;
         }
 
         public string Name => ProviderName;
 
         public static string ProviderName => TmdbUtils.ProviderName;
 
+        // After tvdb and fanart
+        public int Order => 2;
+
         public bool Supports(BaseItem item)
         {
             return item is Series;
@@ -56,7 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         {
             var list = new List<RemoteImageInfo>();
 
-            var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false);
+            var results = await FetchImages(item, null, cancellationToken).ConfigureAwait(false);
 
             if (results == null)
             {
@@ -148,10 +149,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="language">The language.</param>
-        /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{MovieImages}.</returns>
-        private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer,
+        private async Task<Images> FetchImages(
+            BaseItem item,
+            string language,
             CancellationToken cancellationToken)
         {
             var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
@@ -165,22 +167,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
 
-            if (!string.IsNullOrEmpty(path))
+            if (!string.IsNullOrEmpty(path) && File.Exists(path))
             {
-                var fileInfo = _fileSystem.GetFileInfo(path);
-
-                if (fileInfo.Exists)
-                {
-                    return jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
-                }
+                return _jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
             }
 
             return null;
         }
 
-        // After tvdb and fanart
-        public int Order => 2;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);

+ 16 - 21
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs

@@ -17,8 +17,6 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
@@ -33,38 +31,35 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
 
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly ILogger<TmdbSeriesProvider> _logger;
-        private readonly ILocalizationManager _localization;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILibraryManager _libraryManager;
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        internal static TmdbSeriesProvider Current { get; private set; }
-
         public TmdbSeriesProvider(
             IJsonSerializer jsonSerializer,
-            IFileSystem fileSystem,
             IServerConfigurationManager configurationManager,
             ILogger<TmdbSeriesProvider> logger,
-            ILocalizationManager localization,
             IHttpClientFactory httpClientFactory,
             ILibraryManager libraryManager)
         {
             _jsonSerializer = jsonSerializer;
-            _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _logger = logger;
-            _localization = localization;
             _httpClientFactory = httpClientFactory;
             _libraryManager = libraryManager;
             Current = this;
         }
 
+        internal static TmdbSeriesProvider Current { get; private set; }
+
         public string Name => TmdbUtils.ProviderName;
 
+        // After TheTVDB
+        public int Order => 1;
+
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
         {
             var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
@@ -129,8 +124,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
         public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
         {
-            var result = new MetadataResult<Series>();
-            result.QueriedById = true;
+            var result = new MetadataResult<Series>
+            {
+                QueriedById = true
+            };
 
             var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
 
@@ -206,9 +203,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
 
-            var result = new MetadataResult<Series>();
-            result.Item = new Series();
-            result.ResultLanguage = seriesInfo.ResultLanguage;
+            var result = new MetadataResult<Series>
+            {
+                Item = new Series(),
+                ResultLanguage = seriesInfo.ResultLanguage
+            };
 
             var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
 
@@ -474,12 +473,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             var path = GetDataFilePath(tmdbId, language);
 
-            var fileInfo = _fileSystem.GetFileSystemInfo(path);
-
+            var fileInfo = new FileInfo(path);
             if (fileInfo.Exists)
             {
                 // If it's recent or automatic updates are enabled, don't re-download
-                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+                if ((DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays <= 2)
                 {
                     return Task.CompletedTask;
                 }
@@ -549,9 +547,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             return null;
         }
 
-        // After TheTVDB
-        public int Order => 1;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);

+ 4 - 4
MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs

@@ -21,6 +21,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
             _httpClientFactory = httpClientFactory;
         }
 
+        public string Name => TmdbMovieProvider.Current.Name;
+
+        public int Order => 0;
+
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
         {
             return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken);
@@ -31,10 +35,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
             return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken);
         }
 
-        public string Name => TmdbMovieProvider.Current.Name;
-
-        public int Order => 0;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);

+ 8 - 8
MediaBrowser.Providers/Studios/StudiosImageProvider.cs

@@ -33,6 +33,8 @@ namespace MediaBrowser.Providers.Studios
 
         public string Name => "Emby Designs";
 
+        public int Order => 0;
+
         public bool Supports(BaseItem item)
         {
             return item is Studio;
@@ -119,8 +121,6 @@ namespace MediaBrowser.Providers.Studios
             return EnsureList(url, file, _fileSystem, cancellationToken);
         }
 
-        public int Order => 0;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
@@ -161,12 +161,12 @@ namespace MediaBrowser.Providers.Studios
 
         private string GetComparableName(string name)
         {
-            return name.Replace(" ", string.Empty)
-                .Replace(".", string.Empty)
-                .Replace("&", string.Empty)
-                .Replace("!", string.Empty)
-                .Replace(",", string.Empty)
-                .Replace("/", string.Empty);
+            return name.Replace(" ", string.Empty, StringComparison.Ordinal)
+                .Replace(".", string.Empty, StringComparison.Ordinal)
+                .Replace("&", string.Empty, StringComparison.Ordinal)
+                .Replace("!", string.Empty, StringComparison.Ordinal)
+                .Replace(",", string.Empty, StringComparison.Ordinal)
+                .Replace("/", string.Empty, StringComparison.Ordinal);
         }
 
         public IEnumerable<string> GetAvailableImages(string file)

+ 1 - 1
MediaBrowser.Providers/Subtitles/SubtitleManager.cs

@@ -303,7 +303,7 @@ namespace MediaBrowser.Providers.Subtitles
 
         private ISubtitleProvider GetProvider(string id)
         {
-            return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name)));
+            return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name), StringComparison.Ordinal));
         }
 
         /// <inheritdoc />

+ 11 - 4
MediaBrowser.Providers/TV/MissingEpisodeProvider.cs

@@ -48,18 +48,25 @@ namespace MediaBrowser.Providers.TV
 
         public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken)
         {
-            var tvdbId = series.GetProviderId(MetadataProvider.Tvdb);
-            if (string.IsNullOrEmpty(tvdbId))
+            var tvdbIdString = series.GetProviderId(MetadataProvider.Tvdb);
+            if (string.IsNullOrEmpty(tvdbIdString))
             {
                 return false;
             }
 
-            var episodes = await _tvdbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken);
+            var episodes = await _tvdbClientManager.GetAllEpisodesAsync(
+                int.Parse(tvdbIdString, CultureInfo.InvariantCulture),
+                series.GetPreferredMetadataLanguage(),
+                cancellationToken).ConfigureAwait(false);
 
             var episodeLookup = episodes
                 .Select(i =>
                 {
-                    DateTime.TryParse(i.FirstAired, out var firstAired);
+                    if (!DateTime.TryParse(i.FirstAired, out var firstAired))
+                    {
+                        firstAired = default;
+                    }
+
                     var seasonNumber = i.AiredSeason.GetValueOrDefault(-1);
                     var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1);
                     return (seasonNumber, episodeNumber, firstAired);

+ 3 - 3
MediaBrowser.Providers/TV/SeasonMetadataService.cs

@@ -27,6 +27,9 @@ namespace MediaBrowser.Providers.TV
         {
         }
 
+        /// <inheritdoc />
+        protected override bool EnableUpdatingPremiereDateFromChildren => true;
+
         /// <inheritdoc />
         protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType currentUpdateType)
         {
@@ -67,9 +70,6 @@ namespace MediaBrowser.Providers.TV
             return updateType;
         }
 
-        /// <inheritdoc />
-        protected override bool EnableUpdatingPremiereDateFromChildren => true;
-
         /// <inheritdoc />
         protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item)
             => item.GetEpisodes();

+ 1 - 1
README.md

@@ -124,7 +124,7 @@ To run the project with Visual Studio Code you will first need to open the repos
 
 Second, you need to [install the recommended extensions for the workspace](https://code.visualstudio.com/docs/editor/extension-gallery#_recommended-extensions). Note that extension recommendations are classified as either "Workspace Recommendations" or "Other Recommendations", but only the "Workspace Recommendations" are required.
 
-After the required extensions are installed, you can can run the server by pressing `F5`.
+After the required extensions are installed, you can run the server by pressing `F5`.
 
 #### Running From The Command Line
 

+ 2 - 2
RSSDP/DisposableManagedObjectBase.cs

@@ -43,13 +43,13 @@ namespace Rssdp.Infrastructure
         {
             var builder = new StringBuilder();
 
-            const string argFormat = "{0}: {1}\r\n";
+            const string ArgFormat = "{0}: {1}\r\n";
 
             builder.AppendFormat("{0}\r\n", header);
 
             foreach (var pair in values)
             {
-                builder.AppendFormat(argFormat, pair.Key, pair.Value);
+                builder.AppendFormat(ArgFormat, pair.Key, pair.Value);
             }
 
             builder.Append("\r\n");

+ 1 - 1
deployment/Dockerfile.debian.amd64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.debian.arm64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.debian.armhf

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.linux.amd64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.macos

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.portable

@@ -15,7 +15,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.amd64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.arm64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.armhf

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.windows.amd64

@@ -15,7 +15,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 4 - 0
fedora/jellyfin.spec

@@ -84,6 +84,10 @@ EOF
 %{_libdir}/jellyfin/*.so
 %{_libdir}/jellyfin/*.a
 %{_libdir}/jellyfin/createdump
+%{_libdir}/jellyfin/*.xml
+%{_libdir}/jellyfin/wwwroot/api-docs/*
+%{_libdir}/jellyfin/wwwroot/api-docs/redoc/*
+%{_libdir}/jellyfin/wwwroot/api-docs/swagger/*
 # Needs 755 else only root can run it since binary build by dotnet is 722
 %attr(755,root,root) %{_libdir}/jellyfin/jellyfin
 %{_libdir}/jellyfin/SOS_README.md

+ 2 - 2
tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj

@@ -16,8 +16,8 @@
     <PackageReference Include="AutoFixture" Version="4.13.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
     <PackageReference Include="AutoFixture.Xunit2" Version="4.13.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.7" />
-    <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.7" />
+    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />

+ 30 - 0
tests/Jellyfin.Naming.Tests/AudioBook/AudioBookFileInfoTests.cs

@@ -0,0 +1,30 @@
+using Emby.Naming.AudioBook;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+    public class AudioBookFileInfoTests
+    {
+        [Fact]
+        public void CompareTo_Same_Success()
+        {
+            var info = new AudioBookFileInfo();
+            Assert.Equal(0, info.CompareTo(info));
+        }
+
+        [Fact]
+        public void CompareTo_Null_Success()
+        {
+            var info = new AudioBookFileInfo();
+            Assert.Equal(1, info.CompareTo(null));
+        }
+
+        [Fact]
+        public void CompareTo_Empty_Success()
+        {
+            var info1 = new AudioBookFileInfo();
+            var info2 = new AudioBookFileInfo();
+            Assert.Equal(0, info1.CompareTo(info2));
+        }
+    }
+}

+ 8 - 8
tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs

@@ -44,14 +44,14 @@ namespace Jellyfin.Naming.Tests.Video
         }
 
         [Theory]
-        [InlineData(ExtraType.BehindTheScenes, "behind the scenes" )]
-        [InlineData(ExtraType.DeletedScene, "deleted scenes" )]
-        [InlineData(ExtraType.Interview, "interviews" )]
-        [InlineData(ExtraType.Scene, "scenes" )]
-        [InlineData(ExtraType.Sample, "samples" )]
-        [InlineData(ExtraType.Clip, "shorts" )]
-        [InlineData(ExtraType.Clip, "featurettes" )]
-        [InlineData(ExtraType.Unknown, "extras" )]
+        [InlineData(ExtraType.BehindTheScenes, "behind the scenes")]
+        [InlineData(ExtraType.DeletedScene, "deleted scenes")]
+        [InlineData(ExtraType.Interview, "interviews")]
+        [InlineData(ExtraType.Scene, "scenes")]
+        [InlineData(ExtraType.Sample, "samples")]
+        [InlineData(ExtraType.Clip, "shorts")]
+        [InlineData(ExtraType.Clip, "featurettes")]
+        [InlineData(ExtraType.Unknown, "extras")]
         public void TestDirectories(ExtraType type, string dirName)
         {
             Test(dirName + "/300.mp4", type, _videoOptions);

+ 1 - 0
tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs

@@ -10,6 +10,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
         [InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
         [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
         [InlineData("Superman: Red Son", "imdbid", null)]
+        [InlineData("Superman: Red Son", "something", null)]
         public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
         {
             Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));