浏览代码

Merge branch 'master' into bindfix

BaronGreenback 4 年之前
父节点
当前提交
5756c6dbad
共有 100 个文件被更改,包括 862 次插入728 次删除
  1. 1 1
      .ci/azure-pipelines-abi.yml
  2. 1 1
      .ci/azure-pipelines-api-client.yml
  3. 1 1
      .ci/azure-pipelines-main.yml
  4. 14 4
      .ci/azure-pipelines-package.yml
  5. 1 1
      .ci/azure-pipelines-test.yml
  6. 1 1
      .ci/azure-pipelines.yml
  7. 0 30
      .drone.yml
  8. 1 1
      .github/workflows/codeql-analysis.yml
  9. 1 0
      CONTRIBUTORS.md
  10. 1 0
      Emby.Dlna/Didl/DidlBuilder.cs
  11. 1 1
      Emby.Dlna/Main/DlnaEntryPoint.cs
  12. 46 9
      Emby.Dlna/PlayTo/Device.cs
  13. 4 4
      Emby.Dlna/PlayTo/PlayToController.cs
  14. 2 2
      Emby.Dlna/Profiles/SonyBravia2010Profile.cs
  15. 2 2
      Emby.Dlna/Profiles/SonyBravia2011Profile.cs
  16. 2 2
      Emby.Dlna/Profiles/SonyBravia2012Profile.cs
  17. 2 2
      Emby.Dlna/Profiles/SonyBravia2013Profile.cs
  18. 2 2
      Emby.Dlna/Profiles/SonyBravia2014Profile.cs
  19. 2 2
      Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml
  20. 2 2
      Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml
  21. 2 2
      Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml
  22. 2 2
      Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml
  23. 2 2
      Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml
  24. 4 4
      Emby.Naming/AudioBook/AudioBookInfo.cs
  25. 1 1
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  26. 8 7
      Emby.Naming/Common/NamingOptions.cs
  27. 1 1
      Emby.Naming/Emby.Naming.csproj
  28. 1 1
      Emby.Naming/TV/SeasonPathParser.cs
  29. 6 7
      Emby.Naming/Video/VideoListResolver.cs
  30. 2 2
      Emby.Naming/Video/VideoResolver.cs
  31. 0 8
      Emby.Notifications/CoreNotificationTypes.cs
  32. 5 5
      Emby.Server.Implementations/Channels/ChannelManager.cs
  33. 3 3
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  34. 42 42
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  35. 1 1
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  36. 17 11
      Emby.Server.Implementations/Dto/DtoService.cs
  37. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  38. 22 30
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  39. 23 16
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  40. 1 1
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  41. 0 5
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  42. 1 1
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  43. 3 3
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  44. 10 10
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  45. 2 2
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  46. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  47. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  48. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  49. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  50. 16 13
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  51. 12 7
      Emby.Server.Implementations/Localization/Core/ca.json
  52. 15 15
      Emby.Server.Implementations/Localization/Core/de.json
  53. 26 0
      Emby.Server.Implementations/Localization/Core/eo.json
  54. 2 1
      Emby.Server.Implementations/Localization/Core/es_419.json
  55. 5 2
      Emby.Server.Implementations/Localization/Core/fa.json
  56. 74 74
      Emby.Server.Implementations/Localization/Core/fi.json
  57. 53 50
      Emby.Server.Implementations/Localization/Core/fil.json
  58. 27 2
      Emby.Server.Implementations/Localization/Core/hi.json
  59. 110 110
      Emby.Server.Implementations/Localization/Core/kk.json
  60. 2 1
      Emby.Server.Implementations/Localization/Core/nb.json
  61. 2 2
      Emby.Server.Implementations/Localization/Core/ru.json
  62. 1 1
      Emby.Server.Implementations/Localization/Core/sv.json
  63. 3 2
      Emby.Server.Implementations/Localization/Core/tr.json
  64. 2 2
      Emby.Server.Implementations/Localization/Core/vi.json
  65. 5 1
      Emby.Server.Implementations/Localization/Core/zh-HK.json
  66. 1 1
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  67. 2 0
      Emby.Server.Implementations/Localization/iso6392.txt
  68. 1 1
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  69. 41 23
      Emby.Server.Implementations/Plugins/PluginManager.cs
  70. 13 14
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  71. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  72. 5 4
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
  73. 1 1
      Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
  74. 6 1
      Emby.Server.Implementations/Session/SessionManager.cs
  75. 22 1
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  76. 28 0
      Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
  77. 18 0
      Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
  78. 12 0
      Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
  79. 10 8
      Jellyfin.Api/Controllers/ArtistsController.cs
  80. 3 4
      Jellyfin.Api/Controllers/ChannelsController.cs
  81. 0 1
      Jellyfin.Api/Controllers/CollectionController.cs
  82. 8 56
      Jellyfin.Api/Controllers/DashboardController.cs
  83. 0 1
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  84. 18 19
      Jellyfin.Api/Controllers/FilterController.cs
  85. 5 4
      Jellyfin.Api/Controllers/GenresController.cs
  86. 0 2
      Jellyfin.Api/Controllers/HlsSegmentController.cs
  87. 4 0
      Jellyfin.Api/Controllers/ImageController.cs
  88. 0 1
      Jellyfin.Api/Controllers/InstantMixController.cs
  89. 0 2
      Jellyfin.Api/Controllers/ItemLookupController.cs
  90. 17 18
      Jellyfin.Api/Controllers/ItemsController.cs
  91. 0 1
      Jellyfin.Api/Controllers/LibraryController.cs
  92. 2 2
      Jellyfin.Api/Controllers/LiveTvController.cs
  93. 15 14
      Jellyfin.Api/Controllers/MediaInfoController.cs
  94. 5 4
      Jellyfin.Api/Controllers/MusicGenresController.cs
  95. 1 2
      Jellyfin.Api/Controllers/PackageController.cs
  96. 0 1
      Jellyfin.Api/Controllers/PersonsController.cs
  97. 6 5
      Jellyfin.Api/Controllers/PlaylistsController.cs
  98. 1 5
      Jellyfin.Api/Controllers/PluginsController.cs
  99. 5 4
      Jellyfin.Api/Controllers/SearchController.cs
  100. 5 4
      Jellyfin.Api/Controllers/StudiosController.cs

+ 1 - 1
.ci/azure-pipelines-abi.yml

@@ -7,7 +7,7 @@ parameters:
   default: "ubuntu-latest"
   default: "ubuntu-latest"
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
   type: string
   type: string
-  default: 5.0.100
+  default: 5.0.103
 
 
 jobs:
 jobs:
   - job: CompatibilityCheck
   - job: CompatibilityCheck

+ 1 - 1
.ci/azure-pipelines-api-client.yml

@@ -4,7 +4,7 @@
     default: "ubuntu-latest"
     default: "ubuntu-latest"
   - name: GeneratorVersion
   - name: GeneratorVersion
     type: string
     type: string
-    default: "5.0.0-beta2"
+    default: "5.0.1"
 
 
 jobs:
 jobs:
 - job: GenerateApiClients
 - job: GenerateApiClients

+ 1 - 1
.ci/azure-pipelines-main.yml

@@ -1,7 +1,7 @@
 parameters:
 parameters:
   LinuxImage: 'ubuntu-latest'
   LinuxImage: 'ubuntu-latest'
   RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
   RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
-  DotNetSdkVersion: 5.0.100
+  DotNetSdkVersion: 5.0.103
 
 
 jobs:
 jobs:
   - job: Build
   - job: Build

+ 14 - 4
.ci/azure-pipelines-package.yml

@@ -193,6 +193,10 @@ jobs:
   pool:
   pool:
     vmImage: 'ubuntu-latest'
     vmImage: 'ubuntu-latest'
 
 
+  variables:
+  - name: JellyfinVersion
+    value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
+
   steps:
   steps:
   - task: UseDotNet@2
   - task: UseDotNet@2
     displayName: 'Use .NET 5.0 sdk'
     displayName: 'Use .NET 5.0 sdk'
@@ -204,9 +208,15 @@ jobs:
     displayName: 'Build Stable Nuget packages'
     displayName: 'Build Stable Nuget packages'
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     inputs:
     inputs:
-      command: 'pack'
-      packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
-      versioningScheme: 'off'
+      command: 'custom'
+      projects: |
+        Jellyfin.Data/Jellyfin.Data.csproj
+        MediaBrowser.Common/MediaBrowser.Common.csproj
+        MediaBrowser.Controller/MediaBrowser.Controller.csproj
+        MediaBrowser.Model/MediaBrowser.Model.csproj
+        Emby.Naming/Emby.Naming.csproj
+      custom: 'pack'
+      arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
 
 
   - task: DotNetCoreCLI@2
   - task: DotNetCoreCLI@2
     displayName: 'Build Unstable Nuget packages'
     displayName: 'Build Unstable Nuget packages'
@@ -233,7 +243,7 @@ jobs:
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     inputs:
     inputs:
       command: 'push'
       command: 'push'
-      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
       nuGetFeedType: 'external'
       nuGetFeedType: 'external'
       publishFeedCredentials: 'NugetOrg'
       publishFeedCredentials: 'NugetOrg'
       allowPackageConflicts: true # This ignores an error if the version already exists
       allowPackageConflicts: true # This ignores an error if the version already exists

+ 1 - 1
.ci/azure-pipelines-test.yml

@@ -10,7 +10,7 @@ parameters:
   default: "tests/**/*Tests.csproj"
   default: "tests/**/*Tests.csproj"
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
   type: string
   type: string
-  default: 5.0.100
+  default: 5.0.103
 
 
 jobs:
 jobs:
   - job: Test
   - job: Test

+ 1 - 1
.ci/azure-pipelines.yml

@@ -6,7 +6,7 @@ variables:
 - name: RestoreBuildProjects
 - name: RestoreBuildProjects
   value: 'Jellyfin.Server/Jellyfin.Server.csproj'
   value: 'Jellyfin.Server/Jellyfin.Server.csproj'
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
-  value: 5.0.100
+  value: 5.0.103
 
 
 pr:
 pr:
   autoCancel: true
   autoCancel: true

+ 0 - 30
.drone.yml

@@ -1,30 +0,0 @@
----
-kind: pipeline
-name: build-debug
-
-steps:
-- name: submodules
-  image: docker:git
-  commands:
-    - git submodule update --init --recursive
-
-- name: build
-  image: microsoft/dotnet:2-sdk
-  commands:
-    - dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug"
-
----
-kind: pipeline
-name: build-release
-
-steps:
-- name: submodules
-  image: docker:git
-  commands:
-    - git submodule update --init --recursive
-
-- name: build
-  image: microsoft/dotnet:2-sdk
-  commands:
-    - dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"
-

+ 1 - 1
.github/workflows/codeql-analysis.yml

@@ -24,7 +24,7 @@ jobs:
     - name: Setup .NET Core
     - name: Setup .NET Core
       uses: actions/setup-dotnet@v1
       uses: actions/setup-dotnet@v1
       with:
       with:
-        dotnet-version: '5.0.100'
+        dotnet-version: '5.0.x'
     - name: Initialize CodeQL
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v1
       uses: github/codeql-action/init@v1
       with:
       with:

+ 1 - 0
CONTRIBUTORS.md

@@ -80,6 +80,7 @@
  - [nvllsvm](https://github.com/nvllsvm)
  - [nvllsvm](https://github.com/nvllsvm)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [OancaAndrei](https://github.com/OancaAndrei)
  - [OancaAndrei](https://github.com/OancaAndrei)
+ - [obradovichv](https://github.com/obradovichv)
  - [oddstr13](https://github.com/oddstr13)
  - [oddstr13](https://github.com/oddstr13)
  - [orryverducci](https://github.com/orryverducci)
  - [orryverducci](https://github.com/orryverducci)
  - [petermcneil](https://github.com/petermcneil)
  - [petermcneil](https://github.com/petermcneil)

+ 1 - 0
Emby.Dlna/Didl/DidlBuilder.cs

@@ -96,6 +96,7 @@ namespace Emby.Dlna.Didl
 
 
             using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
             using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
             {
             {
+                // If this using are changed to single lines, then write.Flush needs to be appended before the return.
                 using (var writer = XmlWriter.Create(builder, settings))
                 using (var writer = XmlWriter.Create(builder, settings))
                 {
                 {
                     // writer.WriteStartDocument();
                     // writer.WriteStartDocument();

+ 1 - 1
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -315,7 +315,7 @@ namespace Emby.Dlna.Main
                 var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
                 var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
                 // DLNA will only work over http, so we must reset to http:// : {port}
                 // DLNA will only work over http, so we must reset to http:// : {port}
                 uri.Scheme = "http://";
                 uri.Scheme = "http://";
-                uri.Port = _netConfig.PublicPort;
+                uri.Port = _netConfig.HttpServerPortNumber;
 
 
                 var device = new SsdpRootDevice
                 var device = new SsdpRootDevice
                 {
                 {

+ 46 - 9
Emby.Dlna/PlayTo/Device.cs

@@ -235,7 +235,13 @@ namespace Emby.Dlna.PlayTo
             _logger.LogDebug("Setting mute");
             _logger.LogDebug("Setting mute");
             var value = mute ? 1 : 0;
             var value = mute ? 1 : 0;
 
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    rendererCommands.BuildPost(command, service.ServiceType, value),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             IsMuted = mute;
             IsMuted = mute;
@@ -270,7 +276,13 @@ namespace Emby.Dlna.PlayTo
             // Remote control will perform better
             // Remote control will perform better
             Volume = value;
             Volume = value;
 
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    rendererCommands.BuildPost(command, service.ServiceType, value),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
         }
         }
 
 
@@ -291,7 +303,13 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
                 throw new InvalidOperationException("Unable to find service");
             }
             }
 
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             RestartTimer(true);
             RestartTimer(true);
@@ -325,14 +343,21 @@ namespace Emby.Dlna.PlayTo
             }
             }
 
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    post,
+                    header: header,
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
-            await Task.Delay(50).ConfigureAwait(false);
+            await Task.Delay(50, cancellationToken).ConfigureAwait(false);
 
 
             try
             try
             {
             {
-                await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
+                await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
             }
             }
             catch
             catch
             {
             {
@@ -396,7 +421,13 @@ namespace Emby.Dlna.PlayTo
 
 
             var service = GetAvTransportService();
             var service = GetAvTransportService();
 
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    avCommands.BuildPost(command, service.ServiceType, 1),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             RestartTimer(true);
             RestartTimer(true);
@@ -414,7 +445,13 @@ namespace Emby.Dlna.PlayTo
 
 
             var service = GetAvTransportService();
             var service = GetAvTransportService();
 
 
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
+            await new SsdpHttpClient(_httpClientFactory)
+                .SendCommandAsync(
+                    Properties.BaseUrl,
+                    service,
+                    command.Name,
+                    avCommands.BuildPost(command, service.ServiceType, 1),
+                    cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             TransportState = TransportState.Paused;
             TransportState = TransportState.Paused;
@@ -990,7 +1027,7 @@ namespace Emby.Dlna.PlayTo
 
 
             var deviceProperties = new DeviceInfo()
             var deviceProperties = new DeviceInfo()
             {
             {
-                Name = string.Join(" ", friendlyNames),
+                Name = string.Join(' ', friendlyNames),
                 BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
                 BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
             };
             };
 
 

+ 4 - 4
Emby.Dlna/PlayTo/PlayToController.cs

@@ -777,7 +777,7 @@ namespace Emby.Dlna.PlayTo
             var currentWait = 0;
             var currentWait = 0;
             while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
             while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
             {
             {
-                await Task.Delay(Interval).ConfigureAwait(false);
+                await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
                 currentWait += Interval;
                 currentWait += Interval;
             }
             }
 
 
@@ -896,16 +896,16 @@ namespace Emby.Dlna.PlayTo
 
 
                 var parts = url.Split('/');
                 var parts = url.Split('/');
 
 
-                for (var i = 0; i < parts.Length; i++)
+                for (var i = 0; i < parts.Length - 1; i++)
                 {
                 {
                     var part = parts[i];
                     var part = parts[i];
 
 
                     if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
                     if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
                         string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
                         string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
                     {
                     {
-                        if (parts.Length > i + 1)
+                        if (Guid.TryParse(parts[i + 1], out var result))
                         {
                         {
-                            return Guid.Parse(parts[i + 1]);
+                            return result;
                         }
                         }
                     }
                     }
                 }
                 }

+ 2 - 2
Emby.Dlna/Profiles/SonyBravia2010Profile.cs

@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
 
 
             Identification = new DeviceIdentification
             Identification = new DeviceIdentification
             {
             {
-                FriendlyName = @"KDL-\d{2}[EHLNPB]X\d[01]\d.*",
+                FriendlyName = @"KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
                 Manufacturer = "Sony",
                 Manufacturer = "Sony",
 
 
                 Headers = new[]
                 Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
                     new HttpHeaderInfo
                     new HttpHeaderInfo
                     {
                     {
                         Name = "X-AV-Client-Info",
                         Name = "X-AV-Client-Info",
-                        Value = @".*KDL-\d{2}[EHLNPB]X\d[01]\d.*",
+                        Value = @".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*",
                         Match = HeaderMatchType.Regex
                         Match = HeaderMatchType.Regex
                     }
                     }
                 }
                 }

+ 2 - 2
Emby.Dlna/Profiles/SonyBravia2011Profile.cs

@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
 
 
             Identification = new DeviceIdentification
             Identification = new DeviceIdentification
             {
             {
-                FriendlyName = @"KDL-\d{2}([A-Z]X\d2\d|CX400).*",
+                FriendlyName = @"KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
                 Manufacturer = "Sony",
                 Manufacturer = "Sony",
 
 
                 Headers = new[]
                 Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
                     new HttpHeaderInfo
                     new HttpHeaderInfo
                     {
                     {
                         Name = "X-AV-Client-Info",
                         Name = "X-AV-Client-Info",
-                        Value = @".*KDL-\d{2}([A-Z]X\d2\d|CX400).*",
+                        Value = @".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*",
                         Match = HeaderMatchType.Regex
                         Match = HeaderMatchType.Regex
                     }
                     }
                 }
                 }

+ 2 - 2
Emby.Dlna/Profiles/SonyBravia2012Profile.cs

@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
 
 
             Identification = new DeviceIdentification
             Identification = new DeviceIdentification
             {
             {
-                FriendlyName = @"KDL-\d{2}[A-Z]X\d5(\d|G).*",
+                FriendlyName = @"KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
                 Manufacturer = "Sony",
                 Manufacturer = "Sony",
 
 
                 Headers = new[]
                 Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
                     new HttpHeaderInfo
                     new HttpHeaderInfo
                     {
                     {
                         Name = "X-AV-Client-Info",
                         Name = "X-AV-Client-Info",
-                        Value = @".*KDL-\d{2}[A-Z]X\d5(\d|G).*",
+                        Value = @".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*",
                         Match = HeaderMatchType.Regex
                         Match = HeaderMatchType.Regex
                     }
                     }
                 }
                 }

+ 2 - 2
Emby.Dlna/Profiles/SonyBravia2013Profile.cs

@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
 
 
             Identification = new DeviceIdentification
             Identification = new DeviceIdentification
             {
             {
-                FriendlyName = @"KDL-\d{2}[WR][5689]\d{2}A.*",
+                FriendlyName = @"KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
                 Manufacturer = "Sony",
                 Manufacturer = "Sony",
 
 
                 Headers = new[]
                 Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
                     new HttpHeaderInfo
                     new HttpHeaderInfo
                     {
                     {
                         Name = "X-AV-Client-Info",
                         Name = "X-AV-Client-Info",
-                        Value = @".*KDL-\d{2}[WR][5689]\d{2}A.*",
+                        Value = @".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*",
                         Match = HeaderMatchType.Regex
                         Match = HeaderMatchType.Regex
                     }
                     }
                 }
                 }

+ 2 - 2
Emby.Dlna/Profiles/SonyBravia2014Profile.cs

@@ -13,7 +13,7 @@ namespace Emby.Dlna.Profiles
 
 
             Identification = new DeviceIdentification
             Identification = new DeviceIdentification
             {
             {
-                FriendlyName = @"(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*",
+                FriendlyName = @"(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
                 Manufacturer = "Sony",
                 Manufacturer = "Sony",
 
 
                 Headers = new[]
                 Headers = new[]
@@ -21,7 +21,7 @@ namespace Emby.Dlna.Profiles
                     new HttpHeaderInfo
                     new HttpHeaderInfo
                     {
                     {
                         Name = "X-AV-Client-Info",
                         Name = "X-AV-Client-Info",
-                        Value = @".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*",
+                        Value = @".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*",
                         Match = HeaderMatchType.Regex
                         Match = HeaderMatchType.Regex
                     }
                     }
                 }
                 }

+ 2 - 2
Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml

@@ -3,10 +3,10 @@
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Name>Sony Bravia (2010)</Name>
   <Name>Sony Bravia (2010)</Name>
   <Identification>
   <Identification>
-    <FriendlyName>KDL-\d{2}[EHLNPB]X\d[01]\d.*</FriendlyName>
+    <FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName>
     <Manufacturer>Sony</Manufacturer>
     <Manufacturer>Sony</Manufacturer>
     <Headers>
     <Headers>
-      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[EHLNPB]X\d[01]\d.*" match="Regex" />
+      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" />
     </Headers>
     </Headers>
   </Identification>
   </Identification>
   <Manufacturer>Microsoft Corporation</Manufacturer>
   <Manufacturer>Microsoft Corporation</Manufacturer>

+ 2 - 2
Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml

@@ -3,10 +3,10 @@
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Name>Sony Bravia (2011)</Name>
   <Name>Sony Bravia (2011)</Name>
   <Identification>
   <Identification>
-    <FriendlyName>KDL-\d{2}([A-Z]X\d2\d|CX400).*</FriendlyName>
+    <FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName>
     <Manufacturer>Sony</Manufacturer>
     <Manufacturer>Sony</Manufacturer>
     <Headers>
     <Headers>
-      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}([A-Z]X\d2\d|CX400).*" match="Regex" />
+      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" />
     </Headers>
     </Headers>
   </Identification>
   </Identification>
   <Manufacturer>Microsoft Corporation</Manufacturer>
   <Manufacturer>Microsoft Corporation</Manufacturer>

+ 2 - 2
Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml

@@ -3,10 +3,10 @@
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Name>Sony Bravia (2012)</Name>
   <Name>Sony Bravia (2012)</Name>
   <Identification>
   <Identification>
-    <FriendlyName>KDL-\d{2}[A-Z]X\d5(\d|G).*</FriendlyName>
+    <FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName>
     <Manufacturer>Sony</Manufacturer>
     <Manufacturer>Sony</Manufacturer>
     <Headers>
     <Headers>
-      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[A-Z]X\d5(\d|G).*" match="Regex" />
+      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" />
     </Headers>
     </Headers>
   </Identification>
   </Identification>
   <Manufacturer>Microsoft Corporation</Manufacturer>
   <Manufacturer>Microsoft Corporation</Manufacturer>

+ 2 - 2
Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml

@@ -3,10 +3,10 @@
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Name>Sony Bravia (2013)</Name>
   <Name>Sony Bravia (2013)</Name>
   <Identification>
   <Identification>
-    <FriendlyName>KDL-\d{2}[WR][5689]\d{2}A.*</FriendlyName>
+    <FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName>
     <Manufacturer>Sony</Manufacturer>
     <Manufacturer>Sony</Manufacturer>
     <Headers>
     <Headers>
-      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-\d{2}[WR][5689]\d{2}A.*" match="Regex" />
+      <HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" />
     </Headers>
     </Headers>
   </Identification>
   </Identification>
   <Manufacturer>Microsoft Corporation</Manufacturer>
   <Manufacturer>Microsoft Corporation</Manufacturer>

+ 2 - 2
Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml

@@ -3,10 +3,10 @@
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Name>Sony Bravia (2014)</Name>
   <Name>Sony Bravia (2014)</Name>
   <Identification>
   <Identification>
-    <FriendlyName>(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*</FriendlyName>
+    <FriendlyName>(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*</FriendlyName>
     <Manufacturer>Sony</Manufacturer>
     <Manufacturer>Sony</Manufacturer>
     <Headers>
     <Headers>
-      <HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-\d{2}W[5-9]\d{2}B|KDL-\d{2}R480|XBR-\d{2}X[89]\d{2}B|KD-\d{2}[SX][89]\d{3}B).*" match="Regex" />
+      <HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*" match="Regex" />
     </Headers>
     </Headers>
   </Identification>
   </Identification>
   <Manufacturer>Microsoft Corporation</Manufacturer>
   <Manufacturer>Microsoft Corporation</Manufacturer>

+ 4 - 4
Emby.Naming/AudioBook/AudioBookInfo.cs

@@ -15,13 +15,13 @@ namespace Emby.Naming.AudioBook
         /// <param name="files">List of files composing the actual audiobook.</param>
         /// <param name="files">List of files composing the actual audiobook.</param>
         /// <param name="extras">List of extra files.</param>
         /// <param name="extras">List of extra files.</param>
         /// <param name="alternateVersions">Alternative version of files.</param>
         /// <param name="alternateVersions">Alternative version of files.</param>
-        public AudioBookInfo(string name, int? year, List<AudioBookFileInfo>? files, List<AudioBookFileInfo>? extras, List<AudioBookFileInfo>? alternateVersions)
+        public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
         {
         {
             Name = name;
             Name = name;
             Year = year;
             Year = year;
-            Files = files ?? new List<AudioBookFileInfo>();
-            Extras = extras ?? new List<AudioBookFileInfo>();
-            AlternateVersions = alternateVersions ?? new List<AudioBookFileInfo>();
+            Files = files;
+            Extras = extras;
+            AlternateVersions = alternateVersions;
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 1 - 1
Emby.Naming/AudioBook/AudioBookListResolver.cs

@@ -73,7 +73,7 @@ namespace Emby.Naming.AudioBook
 
 
             var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
             var haveChaptersOrPages = stackFiles.Any(x => x.ChapterNumber != null || x.PartNumber != null);
             var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
             var groupedBy = stackFiles.GroupBy(file => new { file.ChapterNumber, file.PartNumber });
-            var nameWithReplacedDots = nameParserResult.Name.Replace(" ", ".");
+            var nameWithReplacedDots = nameParserResult.Name.Replace(' ', '.');
 
 
             foreach (var group in groupedBy)
             foreach (var group in groupedBy)
             {
             {

+ 8 - 7
Emby.Naming/Common/NamingOptions.cs

@@ -282,7 +282,13 @@ namespace Emby.Naming.Common
                     SupportsAbsoluteEpisodeNumbers = true
                     SupportsAbsoluteEpisodeNumbers = true
                 },
                 },
 
 
-                // Case Closed (1996-2007)/Case Closed - 317.mkv
+                // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
+                // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
+                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
+                {
+                    IsNamed = true
+                },
+
                 // /server/anything_102.mp4
                 // /server/anything_102.mp4
                 // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
                 // /server/james.corden.2017.04.20.anne.hathaway.720p.hdtv.x264-crooks.mkv
                 // /server/anything_1996.11.14.mp4
                 // /server/anything_1996.11.14.mp4
@@ -299,11 +305,6 @@ namespace Emby.Naming.Common
 
 
                 // *** End Kodi Standard Naming
                 // *** End Kodi Standard Naming
 
 
-                // [bar] Foo - 1 [baz]
-                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
-                {
-                    IsNamed = true
-                },
                 new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
@@ -587,7 +588,7 @@ namespace Emby.Naming.Common
             AudioBookNamesExpressions = new[]
             AudioBookNamesExpressions = new[]
             {
             {
                 // Detect year usually in brackets after name Batman (2020)
                 // Detect year usually in brackets after name Batman (2020)
-                @"^(?<name>.+?)\s*\(\s*(?<year>\d{4})\s*\)\s*$",
+                @"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
                 @"^\s*(?<name>[^ ].*?)\s*$"
                 @"^\s*(?<name>[^ ].*?)\s*$"
             };
             };
 
 

+ 1 - 1
Emby.Naming/Emby.Naming.csproj

@@ -33,7 +33,7 @@
   <PropertyGroup>
   <PropertyGroup>
     <Authors>Jellyfin Contributors</Authors>
     <Authors>Jellyfin Contributors</Authors>
     <PackageId>Jellyfin.Naming</PackageId>
     <PackageId>Jellyfin.Naming</PackageId>
-    <VersionPrefix>10.7.0</VersionPrefix>
+    <VersionPrefix>10.8.0</VersionPrefix>
     <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
     <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
     <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
     <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
   </PropertyGroup>
   </PropertyGroup>

+ 1 - 1
Emby.Naming/TV/SeasonPathParser.cs

@@ -60,7 +60,7 @@ namespace Emby.Naming.TV
             bool supportSpecialAliases,
             bool supportSpecialAliases,
             bool supportNumericSeasonFolders)
             bool supportNumericSeasonFolders)
         {
         {
-            var filename = Path.GetFileName(path) ?? string.Empty;
+            string filename = Path.GetFileName(path);
 
 
             if (supportSpecialAliases)
             if (supportSpecialAliases)
             {
             {

+ 6 - 7
Emby.Naming/Video/VideoListResolver.cs

@@ -185,8 +185,8 @@ namespace Emby.Naming.Video
             if (!string.IsNullOrEmpty(folderName)
             if (!string.IsNullOrEmpty(folderName)
                 && folderName.Length > 1
                 && folderName.Length > 1
                 && videos.All(i => i.Files.Count == 1
                 && videos.All(i => i.Files.Count == 1
-                && IsEligibleForMultiVersion(folderName, i.Files[0].Path))
-                && HaveSameYear(videos))
+                    && IsEligibleForMultiVersion(folderName, i.Files[0].Path))
+                    && HaveSameYear(videos))
             {
             {
                 var ordered = videos.OrderBy(i => i.Name).ToList();
                 var ordered = videos.OrderBy(i => i.Name).ToList();
 
 
@@ -216,10 +216,9 @@ namespace Emby.Naming.Video
             return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
             return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
         }
         }
 
 
-        private bool IsEligibleForMultiVersion(string folderName, string? testFilename)
+        private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
         {
         {
-            testFilename = Path.GetFileNameWithoutExtension(testFilename) ?? string.Empty;
-
+            string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
             if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
             if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
             {
             {
                 if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
                 if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
@@ -233,8 +232,8 @@ namespace Emby.Naming.Video
                 }
                 }
 
 
                 return string.IsNullOrEmpty(testFilename)
                 return string.IsNullOrEmpty(testFilename)
-                   || testFilename[0].Equals('-')
-                   || testFilename[0].Equals('_')
+                   || testFilename[0] == '-'
+                   || testFilename[0] == '_'
                    || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
                    || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
             }
             }
 
 

+ 2 - 2
Emby.Naming/Video/VideoResolver.cs

@@ -125,7 +125,7 @@ namespace Emby.Naming.Video
         /// <returns>True if is video file.</returns>
         /// <returns>True if is video file.</returns>
         public bool IsVideoFile(string path)
         public bool IsVideoFile(string path)
         {
         {
-            var extension = Path.GetExtension(path) ?? string.Empty;
+            var extension = Path.GetExtension(path);
             return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
             return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
         }
         }
 
 
@@ -136,7 +136,7 @@ namespace Emby.Naming.Video
         /// <returns>True if is video file stub.</returns>
         /// <returns>True if is video file stub.</returns>
         public bool IsStubFile(string path)
         public bool IsStubFile(string path)
         {
         {
-            var extension = Path.GetExtension(path) ?? string.Empty;
+            var extension = Path.GetExtension(path);
             return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
             return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
         }
         }
 
 

+ 0 - 8
Emby.Notifications/CoreNotificationTypes.cs

@@ -75,10 +75,6 @@ namespace Emby.Notifications
                      Type = NotificationType.VideoPlaybackStopped.ToString()
                      Type = NotificationType.VideoPlaybackStopped.ToString()
                 },
                 },
                 new NotificationTypeInfo
                 new NotificationTypeInfo
-                {
-                     Type = NotificationType.CameraImageUploaded.ToString()
-                },
-                new NotificationTypeInfo
                 {
                 {
                      Type = NotificationType.UserLockedOut.ToString()
                      Type = NotificationType.UserLockedOut.ToString()
                 },
                 },
@@ -114,10 +110,6 @@ namespace Emby.Notifications
             {
             {
                 note.Category = _localization.GetLocalizedString("Plugin");
                 note.Category = _localization.GetLocalizedString("Plugin");
             }
             }
-            else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                note.Category = _localization.GetLocalizedString("Sync");
-            }
             else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
             else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
             {
             {
                 note.Category = _localization.GetLocalizedString("User");
                 note.Category = _localization.GetLocalizedString("User");

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

@@ -336,19 +336,19 @@ namespace Emby.Server.Implementations.Channels
             return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
             return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
         }
         }
 
 
-        private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item)
+        private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
         {
         {
             var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
             var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
 
 
             try
             try
             {
             {
-                var jsonString = File.ReadAllText(path, Encoding.UTF8);
-                return JsonSerializer.Deserialize<List<MediaSourceInfo>>(jsonString, _jsonOptions)
-                    ?? new List<MediaSourceInfo>();
+                var bytes = File.ReadAllBytes(path);
+                return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions)
+                    ?? Array.Empty<MediaSourceInfo>();
             }
             }
             catch
             catch
             {
             {
-                return new List<MediaSourceInfo>();
+                return Array.Empty<MediaSourceInfo>();
             }
             }
         }
         }
 
 

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

@@ -2,6 +2,7 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Globalization;
 using System.Globalization;
 using SQLitePCL.pretty;
 using SQLitePCL.pretty;
 
 
@@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Data
 
 
             connection.RunInTransaction(conn =>
             connection.RunInTransaction(conn =>
             {
             {
-                conn.ExecuteAll(string.Join(";", queries));
+                conn.ExecuteAll(string.Join(';', queries));
             });
             });
         }
         }
 
 
@@ -142,11 +143,10 @@ namespace Emby.Server.Implementations.Data
             return result[index].ReadGuidFromBlob();
             return result[index].ReadGuidFromBlob();
         }
         }
 
 
+        [Conditional("DEBUG")]
         private static void CheckName(string name)
         private static void CheckName(string name)
         {
         {
-#if DEBUG
             throw new ArgumentException("Invalid param name: " + name, nameof(name));
             throw new ArgumentException("Invalid param name: " + name, nameof(name));
-#endif
         }
         }
 
 
         public static void TryBind(this IStatement statement, string name, double value)
         public static void TryBind(this IStatement statement, string name, double value)

+ 42 - 42
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -687,7 +687,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (item.Genres.Length > 0)
             if (item.Genres.Length > 0)
             {
             {
-                saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres));
+                saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
             }
             }
             else
             else
             {
             {
@@ -749,7 +749,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (item.LockedFields.Length > 0)
             if (item.LockedFields.Length > 0)
             {
             {
-                saveItemStatement.TryBind("@LockedFields", string.Join("|", item.LockedFields));
+                saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
             }
             }
             else
             else
             {
             {
@@ -758,7 +758,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (item.Studios.Length > 0)
             if (item.Studios.Length > 0)
             {
             {
-                saveItemStatement.TryBind("@Studios", string.Join("|", item.Studios));
+                saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
             }
             }
             else
             else
             {
             {
@@ -785,7 +785,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (item.Tags.Length > 0)
             if (item.Tags.Length > 0)
             {
             {
-                saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags));
+                saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
             }
             }
             else
             else
             {
             {
@@ -807,7 +807,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
             if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
             {
             {
-                saveItemStatement.TryBind("@TrailerTypes", string.Join("|", trailer.TrailerTypes));
+                saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
             }
             }
             else
             else
             {
             {
@@ -902,7 +902,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (item.ProductionLocations.Length > 0)
             if (item.ProductionLocations.Length > 0)
             {
             {
-                saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations));
+                saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
             }
             }
             else
             else
             {
             {
@@ -911,7 +911,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (item.ExtraIds.Length > 0)
             if (item.ExtraIds.Length > 0)
             {
             {
-                saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds));
+                saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
             }
             }
             else
             else
             {
             {
@@ -931,7 +931,7 @@ namespace Emby.Server.Implementations.Data
             string artists = null;
             string artists = null;
             if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
             if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
             {
             {
-                artists = string.Join("|", hasArtists.Artists);
+                artists = string.Join('|', hasArtists.Artists);
             }
             }
 
 
             saveItemStatement.TryBind("@Artists", artists);
             saveItemStatement.TryBind("@Artists", artists);
@@ -940,7 +940,7 @@ namespace Emby.Server.Implementations.Data
             if (item is IHasAlbumArtist hasAlbumArtists
             if (item is IHasAlbumArtist hasAlbumArtists
                 && hasAlbumArtists.AlbumArtists.Count > 0)
                 && hasAlbumArtists.AlbumArtists.Count > 0)
             {
             {
-                albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists);
+                albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
             }
             }
 
 
             saveItemStatement.TryBind("@AlbumArtists", albumArtists);
             saveItemStatement.TryBind("@AlbumArtists", albumArtists);
@@ -2549,7 +2549,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (groups.Count > 0)
             if (groups.Count > 0)
             {
             {
-                return " Group by " + string.Join(",", groups);
+                return " Group by " + string.Join(',', groups);
             }
             }
 
 
             return string.Empty;
             return string.Empty;
@@ -2578,7 +2578,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
 
 
             var commandText = "select "
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
                             + GetFromText()
                             + GetFromText()
                             + GetJoinUserDataText(query);
                             + GetJoinUserDataText(query);
 
 
@@ -2630,7 +2630,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
 
 
             var commandText = "select "
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
+                            + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
                             + GetFromText()
                             + GetFromText()
                             + GetJoinUserDataText(query);
                             + GetJoinUserDataText(query);
 
 
@@ -2880,7 +2880,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
 
 
             var commandText = "select "
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns))
+                            + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
                             + GetFromText()
                             + GetFromText()
                             + GetJoinUserDataText(query);
                             + GetJoinUserDataText(query);
 
 
@@ -2923,15 +2923,15 @@ namespace Emby.Server.Implementations.Data
 
 
                 if (EnableGroupByPresentationUniqueKey(query))
                 if (EnableGroupByPresentationUniqueKey(query))
                 {
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
                 }
                 }
                 else if (query.GroupBySeriesPresentationUniqueKey)
                 else if (query.GroupBySeriesPresentationUniqueKey)
                 {
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
                 }
                 }
                 else
                 else
                 {
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
                 }
                 }
 
 
                 commandText += GetJoinUserDataText(query)
                 commandText += GetJoinUserDataText(query)
@@ -3039,7 +3039,7 @@ namespace Emby.Server.Implementations.Data
                 return string.Empty;
                 return string.Empty;
             }
             }
 
 
-            return " ORDER BY " + string.Join(",", orderBy.Select(i =>
+            return " ORDER BY " + string.Join(',', orderBy.Select(i =>
             {
             {
                 var columnMap = MapOrderByField(i.Item1, query);
                 var columnMap = MapOrderByField(i.Item1, query);
 
 
@@ -3137,7 +3137,7 @@ namespace Emby.Server.Implementations.Data
             var now = DateTime.UtcNow;
             var now = DateTime.UtcNow;
 
 
             var commandText = "select "
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
                             + GetFromText()
                             + GetFromText()
                             + GetJoinUserDataText(query);
                             + GetJoinUserDataText(query);
 
 
@@ -3203,7 +3203,7 @@ namespace Emby.Server.Implementations.Data
 
 
             var now = DateTime.UtcNow;
             var now = DateTime.UtcNow;
 
 
-            var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
+            var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
 
 
             var whereClauses = GetWhereClauses(query, null);
             var whereClauses = GetWhereClauses(query, null);
             if (whereClauses.Count != 0)
             if (whereClauses.Count != 0)
@@ -3284,7 +3284,7 @@ namespace Emby.Server.Implementations.Data
             var now = DateTime.UtcNow;
             var now = DateTime.UtcNow;
 
 
             var commandText = "select "
             var commandText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
                             + GetFromText()
                             + GetFromText()
                             + GetJoinUserDataText(query);
                             + GetJoinUserDataText(query);
 
 
@@ -3327,15 +3327,15 @@ namespace Emby.Server.Implementations.Data
 
 
                 if (EnableGroupByPresentationUniqueKey(query))
                 if (EnableGroupByPresentationUniqueKey(query))
                 {
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
                 }
                 }
                 else if (query.GroupBySeriesPresentationUniqueKey)
                 else if (query.GroupBySeriesPresentationUniqueKey)
                 {
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
                 }
                 }
                 else
                 else
                 {
                 {
-                    commandText += " select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+                    commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
                 }
                 }
 
 
                 commandText += GetJoinUserDataText(query)
                 commandText += GetJoinUserDataText(query)
@@ -3596,7 +3596,7 @@ namespace Emby.Server.Implementations.Data
                 }
                 }
                 else if (excludeTypes.Length > 1)
                 else if (excludeTypes.Length > 1)
                 {
                 {
-                    var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'"));
+                    var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'"));
                     whereClauses.Add($"type not in ({inClause})");
                     whereClauses.Add($"type not in ({inClause})");
                 }
                 }
             }
             }
@@ -3607,7 +3607,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
             else if (includeTypes.Length > 1)
             else if (includeTypes.Length > 1)
             {
             {
-                var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
+                var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'"));
                 whereClauses.Add($"type in ({inClause})");
                 whereClauses.Add($"type in ({inClause})");
             }
             }
 
 
@@ -3618,7 +3618,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
             else if (query.ChannelIds.Count > 1)
             else if (query.ChannelIds.Count > 1)
             {
             {
-                var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+                var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
                 whereClauses.Add($"ChannelId in ({inClause})");
                 whereClauses.Add($"ChannelId in ({inClause})");
             }
             }
 
 
@@ -4351,7 +4351,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
             else if (query.Years.Length > 1)
             else if (query.Years.Length > 1)
             {
             {
-                var val = string.Join(",", query.Years);
+                var val = string.Join(',', query.Years);
 
 
                 whereClauses.Add("ProductionYear in (" + val + ")");
                 whereClauses.Add("ProductionYear in (" + val + ")");
             }
             }
@@ -4401,7 +4401,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
             else if (queryMediaTypes.Length > 1)
             else if (queryMediaTypes.Length > 1)
             {
             {
-                var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'"));
+                var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
 
 
                 whereClauses.Add("MediaType in (" + val + ")");
                 whereClauses.Add("MediaType in (" + val + ")");
             }
             }
@@ -4498,7 +4498,7 @@ namespace Emby.Server.Implementations.Data
                     var paramName = "@HasAnyProviderId" + index;
                     var paramName = "@HasAnyProviderId" + index;
 
 
                     // this is a search for the placeholder
                     // this is a search for the placeholder
-                    hasProviderIds.Add("ProviderIds like " + paramName + "");
+                    hasProviderIds.Add("ProviderIds like " + paramName);
 
 
                     // this replaces the placeholder with a value, here: %key=val%
                     // this replaces the placeholder with a value, here: %key=val%
                     if (statement != null)
                     if (statement != null)
@@ -4549,7 +4549,7 @@ namespace Emby.Server.Implementations.Data
                 }
                 }
                 else if (enableItemsByName && includedItemByNameTypes.Count > 1)
                 else if (enableItemsByName && includedItemByNameTypes.Count > 1)
                 {
                 {
-                    var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
+                    var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
                     whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
                     whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
                 }
                 }
                 else
                 else
@@ -4564,7 +4564,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
             else if (queryTopParentIds.Length > 1)
             else if (queryTopParentIds.Length > 1)
             {
             {
-                var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+                var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
 
 
                 if (enableItemsByName && includedItemByNameTypes.Count == 1)
                 if (enableItemsByName && includedItemByNameTypes.Count == 1)
                 {
                 {
@@ -4576,7 +4576,7 @@ namespace Emby.Server.Implementations.Data
                 }
                 }
                 else if (enableItemsByName && includedItemByNameTypes.Count > 1)
                 else if (enableItemsByName && includedItemByNameTypes.Count > 1)
                 {
                 {
-                    var itemByNameTypeVal = string.Join(",", includedItemByNameTypes.Select(i => "'" + i + "'"));
+                    var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
                     whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
                     whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
                 }
                 }
                 else
                 else
@@ -4597,7 +4597,7 @@ namespace Emby.Server.Implementations.Data
 
 
             if (query.AncestorIds.Length > 1)
             if (query.AncestorIds.Length > 1)
             {
             {
-                var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+                var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
                 whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
                 whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
             }
             }
 
 
@@ -5148,7 +5148,7 @@ AND Type = @InternalPersonType)");
             }
             }
             else if (queryPersonTypes.Count > 1)
             else if (queryPersonTypes.Count > 1)
             {
             {
-                var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'"));
+                var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'"));
 
 
                 whereClauses.Add("PersonType in (" + val + ")");
                 whereClauses.Add("PersonType in (" + val + ")");
             }
             }
@@ -5162,7 +5162,7 @@ AND Type = @InternalPersonType)");
             }
             }
             else if (queryExcludePersonTypes.Count > 1)
             else if (queryExcludePersonTypes.Count > 1)
             {
             {
-                var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'"));
+                var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'"));
 
 
                 whereClauses.Add("PersonType not in (" + val + ")");
                 whereClauses.Add("PersonType not in (" + val + ")");
             }
             }
@@ -5308,19 +5308,19 @@ AND Type = @InternalPersonType)");
 
 
             var typeClause = itemValueTypes.Length == 1 ?
             var typeClause = itemValueTypes.Length == 1 ?
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
-                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+                ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
 
 
             var commandText = "Select Value From ItemValues where " + typeClause;
             var commandText = "Select Value From ItemValues where " + typeClause;
 
 
             if (withItemTypes.Count > 0)
             if (withItemTypes.Count > 0)
             {
             {
-                var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'"));
+                var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'"));
                 commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
                 commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
             }
             }
 
 
             if (excludeItemTypes.Count > 0)
             if (excludeItemTypes.Count > 0)
             {
             {
-                var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'"));
+                var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'"));
                 commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
                 commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
             }
             }
 
 
@@ -5363,7 +5363,7 @@ AND Type = @InternalPersonType)");
 
 
             var typeClause = itemValueTypes.Length == 1 ?
             var typeClause = itemValueTypes.Length == 1 ?
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
-                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+                ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
 
 
             InternalItemsQuery typeSubQuery = null;
             InternalItemsQuery typeSubQuery = null;
 
 
@@ -5427,7 +5427,7 @@ AND Type = @InternalPersonType)");
             columns = GetFinalColumnsToSelect(query, columns);
             columns = GetFinalColumnsToSelect(query, columns);
 
 
             var commandText = "select "
             var commandText = "select "
-                            + string.Join(",", columns)
+                            + string.Join(',', columns)
                             + GetFromText()
                             + GetFromText()
                             + GetJoinUserDataText(query);
                             + GetJoinUserDataText(query);
 
 
@@ -5504,7 +5504,7 @@ AND Type = @InternalPersonType)");
             if (query.EnableTotalRecordCount)
             if (query.EnableTotalRecordCount)
             {
             {
                 var countText = "select "
                 var countText = "select "
-                            + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+                            + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
                             + GetFromText()
                             + GetFromText()
                             + GetJoinUserDataText(query)
                             + GetJoinUserDataText(query)
                             + whereText;
                             + whereText;
@@ -5565,7 +5565,7 @@ AND Type = @InternalPersonType)");
                         if (query.EnableTotalRecordCount)
                         if (query.EnableTotalRecordCount)
                         {
                         {
                             commandText = "select "
                             commandText = "select "
-                                        + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+                                        + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
                                         + GetFromText()
                                         + GetFromText()
                                         + GetJoinUserDataText(query)
                                         + GetJoinUserDataText(query)
                                         + whereText;
                                         + whereText;

+ 1 - 1
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Data
                 connection.RunInTransaction(
                 connection.RunInTransaction(
                 db =>
                 db =>
                 {
                 {
-                    db.ExecuteAll(string.Join(";", new[] {
+                    db.ExecuteAll(string.Join(';', new[] {
 
 
                         "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
                         "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
 
 

+ 17 - 11
Emby.Server.Implementations/Dto/DtoService.cs

@@ -249,7 +249,7 @@ namespace Emby.Server.Implementations.Dto
             var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
             var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
             if (activeRecording != null)
             if (activeRecording != null)
             {
             {
-                dto.Type = "Recording";
+                dto.Type = BaseItemKind.Recording;
                 dto.CanDownload = false;
                 dto.CanDownload = false;
                 dto.RunTimeTicks = null;
                 dto.RunTimeTicks = null;
 
 
@@ -582,16 +582,22 @@ namespace Emby.Server.Implementations.Dto
                 {
                 {
                     baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
                     baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
                     baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
                     baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
-                    // Only add BlurHash for the person's image.
-                    baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
-                    foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
+                    if (dto.ImageBlurHashes != null)
                     {
                     {
-                        baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
-                        foreach (var (imageId, blurHashValue) in blurHash)
+                        // Only add BlurHash for the person's image.
+                        baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+                        foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
                         {
                         {
-                            if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
+                            if (blurHash != null)
                             {
                             {
-                                baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
+                                baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
+                                foreach (var (imageId, blurHashValue) in blurHash)
+                                {
+                                    if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
+                                    {
+                                        baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
+                                    }
+                                }
                             }
                             }
                         }
                         }
                     }
                     }
@@ -898,7 +904,7 @@ namespace Emby.Server.Implementations.Dto
                 }
                 }
             }
             }
 
 
-            dto.Type = item.GetClientTypeName();
+            dto.Type = item.GetBaseItemKind();
             if ((item.CommunityRating ?? 0) > 0)
             if ((item.CommunityRating ?? 0) > 0)
             {
             {
                 dto.CommunityRating = item.CommunityRating;
                 dto.CommunityRating = item.CommunityRating;
@@ -1151,7 +1157,7 @@ namespace Emby.Server.Implementations.Dto
                     if (episodeSeries != null)
                     if (episodeSeries != null)
                     {
                     {
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
-                        if (!dto.ImageTags.ContainsKey(ImageType.Primary))
+                        if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
                         {
                         {
                             AttachPrimaryImageAspectRatio(dto, episodeSeries);
                             AttachPrimaryImageAspectRatio(dto, episodeSeries);
                         }
                         }
@@ -1201,7 +1207,7 @@ namespace Emby.Server.Implementations.Dto
                     if (series != null)
                     if (series != null)
                     {
                     {
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
-                        if (!dto.ImageTags.ContainsKey(ImageType.Primary))
+                        if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
                         {
                         {
                             AttachPrimaryImageAspectRatio(dto, series);
                             AttachPrimaryImageAspectRatio(dto, series);
                         }
                         }

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

@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
-    <PackageReference Include="sharpcompress" Version="0.26.0" />
+    <PackageReference Include="sharpcompress" Version="0.28.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />
   </ItemGroup>
   </ItemGroup>

+ 22 - 30
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -5,6 +5,7 @@ using System.Buffers;
 using System.IO.Pipelines;
 using System.IO.Pipelines;
 using System.Net;
 using System.Net;
 using System.Net.WebSockets;
 using System.Net.WebSockets;
+using System.Text;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -138,7 +139,7 @@ namespace Emby.Server.Implementations.HttpServer
                 writer.Advance(bytesRead);
                 writer.Advance(bytesRead);
 
 
                 // Make the data available to the PipeReader
                 // Make the data available to the PipeReader
-                FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
+                FlushResult flushResult = await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
                 if (flushResult.IsCompleted)
                 if (flushResult.IsCompleted)
                 {
                 {
                     // The PipeReader stopped reading
                     // The PipeReader stopped reading
@@ -181,32 +182,16 @@ namespace Emby.Server.Implementations.HttpServer
             }
             }
 
 
             WebSocketMessage<object>? stub;
             WebSocketMessage<object>? stub;
+            long bytesConsumed = 0;
             try
             try
             {
             {
-
-                if (buffer.IsSingleSegment)
-                {
-                    stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
-                }
-                else
-                {
-                    var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
-                    try
-                    {
-                        buffer.CopyTo(buf);
-                        stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
-                    }
-                    finally
-                    {
-                        ArrayPool<byte>.Shared.Return(buf);
-                    }
-                }
+                stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
             }
             }
             catch (JsonException ex)
             catch (JsonException ex)
             {
             {
                 // Tell the PipeReader how much of the buffer we have consumed
                 // Tell the PipeReader how much of the buffer we have consumed
                 reader.AdvanceTo(buffer.End);
                 reader.AdvanceTo(buffer.End);
-                _logger.LogError(ex, "Error processing web socket message");
+                _logger.LogError(ex, "Error processing web socket message: {Data}", Encoding.UTF8.GetString(buffer));
                 return;
                 return;
             }
             }
 
 
@@ -217,27 +202,34 @@ namespace Emby.Server.Implementations.HttpServer
             }
             }
 
 
             // Tell the PipeReader how much of the buffer we have consumed
             // Tell the PipeReader how much of the buffer we have consumed
-            reader.AdvanceTo(buffer.End);
+            reader.AdvanceTo(buffer.GetPosition(bytesConsumed));
 
 
             _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
             _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
 
 
-            var info = new WebSocketMessageInfo
-            {
-                MessageType = stub.MessageType,
-                Data = stub.Data?.ToString(), // Data can be null
-                Connection = this
-            };
-
-            if (info.MessageType == SessionMessageType.KeepAlive)
+            if (stub.MessageType == SessionMessageType.KeepAlive)
             {
             {
                 await SendKeepAliveResponse().ConfigureAwait(false);
                 await SendKeepAliveResponse().ConfigureAwait(false);
             }
             }
             else
             else
             {
             {
-                await OnReceive(info).ConfigureAwait(false);
+                await OnReceive(
+                    new WebSocketMessageInfo
+                    {
+                        MessageType = stub.MessageType,
+                        Data = stub.Data?.ToString(), // Data can be null
+                        Connection = this
+                    }).ConfigureAwait(false);
             }
             }
         }
         }
 
 
+        internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
+        {
+            var jsonReader = new Utf8JsonReader(bytes);
+            var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions);
+            bytesConsumed = jsonReader.BytesConsumed;
+            return ret;
+        }
+
         private Task SendKeepAliveResponse()
         private Task SendKeepAliveResponse()
         {
         {
             LastKeepAliveDate = DateTime.UtcNow;
             LastKeepAliveDate = DateTime.UtcNow;

+ 23 - 16
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -582,9 +582,7 @@ namespace Emby.Server.Implementations.IO
 
 
         public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
         public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
         {
         {
-            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
-
-            return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
+            return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
         }
         }
 
 
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
@@ -594,16 +592,16 @@ namespace Emby.Server.Implementations.IO
 
 
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
         {
-            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+            var enumerationOptions = GetEnumerationOptions(recursive);
 
 
             // On linux and osx the search pattern is case sensitive
             // On linux and osx the search pattern is case sensitive
             // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
             // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
             if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
             if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
             {
             {
-                return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
+                return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
             }
             }
 
 
-            var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
+            var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
 
 
             if (extensions != null && extensions.Count > 0)
             if (extensions != null && extensions.Count > 0)
             {
             {
@@ -625,10 +623,10 @@ namespace Emby.Server.Implementations.IO
         public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
         public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
         {
         {
             var directoryInfo = new DirectoryInfo(path);
             var directoryInfo = new DirectoryInfo(path);
-            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+            var enumerationOptions = GetEnumerationOptions(recursive);
 
 
-            return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
-                .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
+            return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
+                .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
         }
         }
 
 
         private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
         private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -638,8 +636,7 @@ namespace Emby.Server.Implementations.IO
 
 
         public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
         public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
         {
         {
-            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
-            return Directory.EnumerateDirectories(path, "*", searchOption);
+            return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
         }
         }
 
 
         public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
         public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
@@ -649,16 +646,16 @@ namespace Emby.Server.Implementations.IO
 
 
         public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
         {
-            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+            var enumerationOptions = GetEnumerationOptions(recursive);
 
 
             // On linux and osx the search pattern is case sensitive
             // On linux and osx the search pattern is case sensitive
             // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
             // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
             if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
             if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
             {
             {
-                return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption);
+                return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
             }
             }
 
 
-            var files = Directory.EnumerateFiles(path, "*", searchOption);
+            var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
 
 
             if (extensions != null && extensions.Length > 0)
             if (extensions != null && extensions.Length > 0)
             {
             {
@@ -679,8 +676,18 @@ namespace Emby.Server.Implementations.IO
 
 
         public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
         public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
         {
         {
-            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
-            return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
+            return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
+        }
+
+        private EnumerationOptions GetEnumerationOptions(bool recursive)
+        {
+            return new EnumerationOptions
+            {
+                RecurseSubdirectories = recursive,
+                IgnoreInaccessible = true,
+                // Don't skip any files.
+                AttributesToSkip = 0
+            };
         }
         }
 
 
         private static void RunProcess(string path, string args, string workingDirectory)
         private static void RunProcess(string path, string args, string workingDirectory)

+ 1 - 1
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -515,7 +515,7 @@ namespace Emby.Server.Implementations.Library
             }
             }
 
 
             // TODO: @bond Fix
             // TODO: @bond Fix
-            var json = JsonSerializer.Serialize(mediaSource, _jsonOptions);
+            var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
             _logger.LogInformation("Live stream opened: " + json);
             _logger.LogInformation("Live stream opened: " + json);
             var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
             var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
 
 

+ 0 - 5
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -79,11 +79,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 return new MusicArtist();
                 return new MusicArtist();
             }
             }
 
 
-            if (_config.Configuration.EnableSimpleArtistDetection)
-            {
-                return null;
-            }
-
             // Avoid mis-identifying top folders
             // Avoid mis-identifying top folders
             if (args.Parent.IsRoot)
             if (args.Parent.IsRoot)
             {
             {

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 }
                 }
 
 
                 // It's a directory-based playlist if the directory contains a playlist file
                 // It's a directory-based playlist if the directory contains a playlist file
-                var filePaths = Directory.EnumerateFiles(args.Path);
+                var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
                 if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
                 if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
                 {
                 {
                     return new Playlist
                     return new Playlist

+ 3 - 3
Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

@@ -47,11 +47,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 try
                 try
                 {
                 {
-                    var jsonString = File.ReadAllText(_dataPath, Encoding.UTF8);
-                    _items = JsonSerializer.Deserialize<T[]>(jsonString, _jsonOptions);
+                    var bytes = File.ReadAllBytes(_dataPath);
+                    _items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
                     return;
                     return;
                 }
                 }
-                catch (Exception ex)
+                catch (JsonException ex)
                 {
                 {
                     Logger.LogError(ex, "Error deserializing {Path}", _dataPath);
                     Logger.LogError(ex, "Error deserializing {Path}", _dataPath);
                 }
                 }

+ 10 - 10
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -10,6 +10,7 @@ using System.Net.Http;
 using System.Net.Mime;
 using System.Net.Mime;
 using System.Text;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json;
+using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
@@ -35,8 +36,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private readonly ICryptoProvider _cryptoProvider;
         private readonly ICryptoProvider _cryptoProvider;
 
 
         private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
         private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
-        private DateTime _lastErrorResponse;
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private DateTime _lastErrorResponse;
 
 
         public SchedulesDirect(
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
             ILogger<SchedulesDirect> logger,
@@ -111,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             options.Headers.TryAddWithoutValidation("token", token);
             options.Headers.TryAddWithoutValidation("token", token);
             using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions).ConfigureAwait(false);
+            var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
             _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
             _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
 
 
             using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
             using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
@@ -122,12 +123,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions).ConfigureAwait(false);
+            var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
             var programDict = programDetails.ToDictionary(p => p.programID, y => y);
             var programDict = programDetails.ToDictionary(p => p.programID, y => y);
 
 
-            var programIdsWithImages =
-                programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
-                    .ToList();
+            var programIdsWithImages = programDetails
+                .Where(p => p.hasImageArtwork).Select(p => p.programID)
+                .ToList();
 
 
             var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
             var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
 
 
@@ -182,8 +183,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
         private static int GetSizeOrder(ScheduleDirect.ImageData image)
         private static int GetSizeOrder(ScheduleDirect.ImageData image)
         {
         {
-            if (!string.IsNullOrWhiteSpace(image.height)
-                && int.TryParse(image.height, out int value))
+            if (int.TryParse(image.height, out int value))
             {
             {
                 return value;
                 return value;
             }
             }
@@ -704,7 +704,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 httpResponse.EnsureSuccessStatusCode();
                 httpResponse.EnsureSuccessStatusCode();
                 await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
                 await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
                 using var response = httpResponse.Content;
                 using var response = httpResponse.Content;
-                var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions).ConfigureAwait(false);
+                var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
 
                 return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
                 return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
             }
             }
@@ -776,7 +776,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions).ConfigureAwait(false);
+            var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
             _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
             _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
             _logger.LogInformation("Mapping Stations to Channel");
             _logger.LogInformation("Mapping Stations to Channel");
 
 

+ 2 - 2
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -2239,7 +2239,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
         public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
         public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
         {
         {
-            info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
+            info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
 
 
             var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
             var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
 
 
@@ -2283,7 +2283,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
         {
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
             // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
             // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
-            info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
+            info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
 
 
             var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
             var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
 
 

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         public LegacyHdHomerunChannelCommands(string url)
         public LegacyHdHomerunChannelCommands(string url)
         {
         {
             // parse url for channel and program
             // parse url for channel and program
-            var regExp = new Regex(@"\/ch(\d+)-?(\d*)");
+            var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
             var match = regExp.Match(url);
             var match = regExp.Match(url);
             if (match.Success)
             if (match.Success)
             {
             {

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             {
             {
                 try
                 try
                 {
                 {
-                    await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
+                    await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort, openCancellationToken).ConfigureAwait(false);
                     localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
                     localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
                     tcpClient.Close();
                     tcpClient.Close();
                 }
                 }

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

@@ -155,7 +155,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
 
             if (channelIdValues.Count > 0)
             if (channelIdValues.Count > 0)
             {
             {
-                channel.Id = string.Join("_", channelIdValues);
+                channel.Id = string.Join('_', channelIdValues);
             }
             }
 
 
             return channel;
             return channel;

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
 
                 EnableStreamSharing = false;
                 EnableStreamSharing = false;
                 await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
                 await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
-            });
+            }, CancellationToken.None);
         }
         }
 
 
         private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)
         private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)

+ 16 - 13
Emby.Server.Implementations/Localization/Core/bg-BG.json

@@ -8,7 +8,7 @@
     "CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
     "CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
     "Channels": "Канали",
     "Channels": "Канали",
     "ChapterNameValue": "Глава {0}",
     "ChapterNameValue": "Глава {0}",
-    "Collections": "Колекции",
+    "Collections": "Поредици",
     "DeviceOfflineWithName": "{0} се разкачи",
     "DeviceOfflineWithName": "{0} се разкачи",
     "DeviceOnlineWithName": "{0} е свързан",
     "DeviceOnlineWithName": "{0} е свързан",
     "FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
     "FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
@@ -55,26 +55,26 @@
     "NotificationOptionPluginInstalled": "Приставката е инсталирана",
     "NotificationOptionPluginInstalled": "Приставката е инсталирана",
     "NotificationOptionPluginUninstalled": "Приставката е деинсталирана",
     "NotificationOptionPluginUninstalled": "Приставката е деинсталирана",
     "NotificationOptionPluginUpdateInstalled": "Обновлението на приставката е инсталирано",
     "NotificationOptionPluginUpdateInstalled": "Обновлението на приставката е инсталирано",
-    "NotificationOptionServerRestartRequired": "Нужно е повторно пускане на сървъра",
+    "NotificationOptionServerRestartRequired": "Сървърът трябва да се рестартира",
     "NotificationOptionTaskFailed": "Грешка в планирана задача",
     "NotificationOptionTaskFailed": "Грешка в планирана задача",
-    "NotificationOptionUserLockedOut": "Потребителя е заключен",
+    "NotificationOptionUserLockedOut": "Потребителят е заключен",
     "NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна",
     "NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна",
     "NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
     "NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
     "Photos": "Снимки",
     "Photos": "Снимки",
     "Playlists": "Списъци",
     "Playlists": "Списъци",
     "Plugin": "Приставка",
     "Plugin": "Приставка",
-    "PluginInstalledWithName": "{0} е инсталирано",
-    "PluginUninstalledWithName": "{0} е деинсталирано",
-    "PluginUpdatedWithName": "{0} е обновено",
+    "PluginInstalledWithName": "{0} е инсталиранa",
+    "PluginUninstalledWithName": "{0} е деинсталиранa",
+    "PluginUpdatedWithName": "{0} е обновенa",
     "ProviderValue": "Доставчик: {0}",
     "ProviderValue": "Доставчик: {0}",
     "ScheduledTaskFailedWithName": "{0} се провали",
     "ScheduledTaskFailedWithName": "{0} се провали",
     "ScheduledTaskStartedWithName": "{0} започна",
     "ScheduledTaskStartedWithName": "{0} започна",
-    "ServerNameNeedsToBeRestarted": "{0} е нужно да се рестартира",
+    "ServerNameNeedsToBeRestarted": "{0} трябва да се рестартира",
     "Shows": "Сериали",
     "Shows": "Сериали",
     "Songs": "Песни",
     "Songs": "Песни",
     "StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
     "StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
     "SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}",
     "SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}",
-    "SubtitleDownloadFailureFromForItem": "Поднадписите за {1} от {0} не можаха да се изтеглят",
+    "SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
     "Sync": "Синхронизиране",
     "Sync": "Синхронизиране",
     "System": "Система",
     "System": "Система",
     "TvShows": "Телевизионни сериали",
     "TvShows": "Телевизионни сериали",
@@ -92,12 +92,12 @@
     "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
     "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
     "ValueSpecialEpisodeName": "Специални - {0}",
     "ValueSpecialEpisodeName": "Специални - {0}",
     "VersionNumber": "Версия {0}",
     "VersionNumber": "Версия {0}",
-    "TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи поднадписи, на база конфигурацията за мета-данни.",
-    "TaskDownloadMissingSubtitles": "Изтегляне на липсващи поднадписи",
+    "TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи субтитри, на база конфигурацията за мета-данни.",
+    "TaskDownloadMissingSubtitles": "Изтегляне на липсващи субтитри",
     "TaskRefreshChannelsDescription": "Обновява информацията за интернет канала.",
     "TaskRefreshChannelsDescription": "Обновява информацията за интернет канала.",
     "TaskRefreshChannels": "Обновяване на Канали",
     "TaskRefreshChannels": "Обновяване на Канали",
-    "TaskCleanTranscodeDescription": "Изтрива прекодирани файлове по-стари от един ден.",
-    "TaskCleanTranscode": "Изчиства директорията за прекодиране",
+    "TaskCleanTranscodeDescription": "Изтрива транскодирани файлове по-стари от един ден.",
+    "TaskCleanTranscode": "Изчиства директорията за транскодиране",
     "TaskUpdatePluginsDescription": "Изтегля и инсталира актуализации за добавките, които са настроени за автоматична актуализация.",
     "TaskUpdatePluginsDescription": "Изтегля и инсталира актуализации за добавките, които са настроени за автоматична актуализация.",
     "TaskUpdatePlugins": "Актуализира добавките",
     "TaskUpdatePlugins": "Актуализира добавките",
     "TaskRefreshPeopleDescription": "Актуализира мета-данните за артистите и режисьорите за Вашата медийна библиотека.",
     "TaskRefreshPeopleDescription": "Актуализира мета-данните за артистите и режисьорите за Вашата медийна библиотека.",
@@ -113,5 +113,8 @@
     "TasksChannelsCategory": "Интернет Канали",
     "TasksChannelsCategory": "Интернет Канали",
     "TasksApplicationCategory": "Приложение",
     "TasksApplicationCategory": "Приложение",
     "TasksLibraryCategory": "Библиотека",
     "TasksLibraryCategory": "Библиотека",
-    "TasksMaintenanceCategory": "Поддръжка"
+    "TasksMaintenanceCategory": "Поддръжка",
+    "Undefined": "Неопределено",
+    "Forced": "Принудително",
+    "Default": "По подразбиране"
 }
 }

+ 12 - 7
Emby.Server.Implementations/Localization/Core/ca.json

@@ -18,10 +18,10 @@
     "HeaderAlbumArtists": "Artistes del Àlbum",
     "HeaderAlbumArtists": "Artistes del Àlbum",
     "HeaderContinueWatching": "Continua Veient",
     "HeaderContinueWatching": "Continua Veient",
     "HeaderFavoriteAlbums": "Àlbums Preferits",
     "HeaderFavoriteAlbums": "Àlbums Preferits",
-    "HeaderFavoriteArtists": "Artistes Preferits",
-    "HeaderFavoriteEpisodes": "Episodis Preferits",
-    "HeaderFavoriteShows": "Programes Preferits",
-    "HeaderFavoriteSongs": "Cançons Preferides",
+    "HeaderFavoriteArtists": "Artistes Predilectes",
+    "HeaderFavoriteEpisodes": "Episodis Predilectes",
+    "HeaderFavoriteShows": "Programes Predilectes",
+    "HeaderFavoriteSongs": "Cançons Predilectes",
     "HeaderLiveTV": "TV en Directe",
     "HeaderLiveTV": "TV en Directe",
     "HeaderNextUp": "A continuació",
     "HeaderNextUp": "A continuació",
     "HeaderRecordingGroups": "Grups d'Enregistrament",
     "HeaderRecordingGroups": "Grups d'Enregistrament",
@@ -36,7 +36,7 @@
     "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}",
     "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
     "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
     "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
     "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
-    "MixedContent": "Contingut mesclat",
+    "MixedContent": "Contingut barrejat",
     "Movies": "Pel·lícules",
     "Movies": "Pel·lícules",
     "Music": "Música",
     "Music": "Música",
     "MusicVideos": "Vídeos musicals",
     "MusicVideos": "Vídeos musicals",
@@ -76,7 +76,7 @@
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}",
     "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}",
     "Sync": "Sincronitzar",
     "Sync": "Sincronitzar",
-    "System": "System",
+    "System": "Sistema",
     "TvShows": "Espectacles de TV",
     "TvShows": "Espectacles de TV",
     "User": "User",
     "User": "User",
     "UserCreatedWithName": "S'ha creat l'usuari {0}",
     "UserCreatedWithName": "S'ha creat l'usuari {0}",
@@ -113,5 +113,10 @@
     "TasksChannelsCategory": "Canals d'internet",
     "TasksChannelsCategory": "Canals d'internet",
     "TasksApplicationCategory": "Aplicació",
     "TasksApplicationCategory": "Aplicació",
     "TasksLibraryCategory": "Biblioteca",
     "TasksLibraryCategory": "Biblioteca",
-    "TasksMaintenanceCategory": "Manteniment"
+    "TasksMaintenanceCategory": "Manteniment",
+    "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
+    "TaskCleanActivityLog": "Buidar Registre d'Activitat",
+    "Undefined": "Indefinit",
+    "Forced": "Forçat",
+    "Default": "Defecto"
 }
 }

+ 15 - 15
Emby.Server.Implementations/Localization/Core/de.json

@@ -3,7 +3,7 @@
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "Application": "Anwendung",
     "Application": "Anwendung",
     "Artists": "Interpreten",
     "Artists": "Interpreten",
-    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
+    "AuthenticationSucceededWithUserName": "{0} wurde angemeldet",
     "Books": "Bücher",
     "Books": "Bücher",
     "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
     "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
     "Channels": "Kanäle",
     "Channels": "Kanäle",
@@ -94,22 +94,22 @@
     "VersionNumber": "Version {0}",
     "VersionNumber": "Version {0}",
     "TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Meta Einstellungen.",
     "TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Meta Einstellungen.",
     "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
     "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
-    "TaskRefreshChannelsDescription": "Erneuere Internet Kanal Informationen.",
-    "TaskRefreshChannels": "Erneuere Kanäle",
-    "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
-    "TaskCleanTranscode": "Lösche Transkodier Pfad",
-    "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
-    "TaskUpdatePlugins": "Update Plugins",
-    "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
-    "TaskRefreshPeople": "Erneuere Schauspieler",
-    "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
-    "TaskCleanLogs": "Lösche Log Pfad",
-    "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
+    "TaskRefreshChannelsDescription": "Aktualisiere Internet Kanal Informationen.",
+    "TaskRefreshChannels": "Aktualisiere Kanäle",
+    "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, welche älter als einen Tag sind.",
+    "TaskCleanTranscode": "Lösche Transkodier-Pfad",
+    "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
+    "TaskUpdatePlugins": "Aktualisiere Plugins",
+    "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+    "TaskRefreshPeople": "Aktualisiere Schauspieler",
+    "TaskCleanLogsDescription": "Lösche Log Dateien, die älter als {0} Tage sind.",
+    "TaskCleanLogs": "Lösche Log-Verzeichnis",
+    "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.",
     "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
     "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
-    "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
+    "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, welche Kapitel besitzen.",
     "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
     "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
-    "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
-    "TaskCleanCache": "Leere Cache Pfad",
+    "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.",
+    "TaskCleanCache": "Leere Zwischenspeicher",
     "TasksChannelsCategory": "Internet Kanäle",
     "TasksChannelsCategory": "Internet Kanäle",
     "TasksApplicationCategory": "Anwendung",
     "TasksApplicationCategory": "Anwendung",
     "TasksLibraryCategory": "Bibliothek",
     "TasksLibraryCategory": "Bibliothek",

+ 26 - 0
Emby.Server.Implementations/Localization/Core/eo.json

@@ -0,0 +1,26 @@
+{
+    "NotificationOptionInstallationFailed": "Instalada fiasko",
+    "NotificationOptionAudioPlaybackStopped": "Sono de ludado haltis",
+    "NotificationOptionAudioPlayback": "Ludado de sono startis",
+    "NameSeasonUnknown": "Sezono Nekonata",
+    "NameSeasonNumber": "Sezono {0}",
+    "NameInstallFailed": "{0} instalado fiaskis",
+    "Music": "Muziko",
+    "Movies": "Filmoj",
+    "ItemRemovedWithName": "{0} forigis el la biblioteko",
+    "ItemAddedWithName": "{0} aldonis al la biblioteko",
+    "HeaderLiveTV": "Viva Televido",
+    "HeaderContinueWatching": "Daŭrigi Spektado",
+    "HeaderAlbumArtists": "Artistoj de Albumo",
+    "Folders": "Dosierujoj",
+    "DeviceOnlineWithName": "{0} estas konektita",
+    "Default": "Defaŭlte",
+    "Collections": "Kolektoj",
+    "ChapterNameValue": "Ĉapitro {0}",
+    "Channels": "Kanaloj",
+    "Books": "Libroj",
+    "Artists": "Artistoj",
+    "Application": "Aplikaĵo",
+    "AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
+    "Albums": "Albumoj"
+}

+ 2 - 1
Emby.Server.Implementations/Localization/Core/es_419.json

@@ -116,5 +116,6 @@
     "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
     "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
     "TaskCleanActivityLog": "Limpiar Registro de Actividades",
     "TaskCleanActivityLog": "Limpiar Registro de Actividades",
     "Undefined": "Sin definir",
     "Undefined": "Sin definir",
-    "Forced": "Forzado"
+    "Forced": "Forzado",
+    "Default": "Por Defecto"
 }
 }

+ 5 - 2
Emby.Server.Implementations/Localization/Core/fa.json

@@ -49,7 +49,7 @@
     "NotificationOptionAudioPlayback": "پخش صدا آغاز شد",
     "NotificationOptionAudioPlayback": "پخش صدا آغاز شد",
     "NotificationOptionAudioPlaybackStopped": "پخش صدا متوقف شد",
     "NotificationOptionAudioPlaybackStopped": "پخش صدا متوقف شد",
     "NotificationOptionCameraImageUploaded": "تصاویر دوربین آپلود شد",
     "NotificationOptionCameraImageUploaded": "تصاویر دوربین آپلود شد",
-    "NotificationOptionInstallationFailed": "نصب شکست خورد",
+    "NotificationOptionInstallationFailed": "نصب ناموفق",
     "NotificationOptionNewLibraryContent": "محتوای جدید افزوده شد",
     "NotificationOptionNewLibraryContent": "محتوای جدید افزوده شد",
     "NotificationOptionPluginError": "خرابی افزونه",
     "NotificationOptionPluginError": "خرابی افزونه",
     "NotificationOptionPluginInstalled": "افزونه نصب شد",
     "NotificationOptionPluginInstalled": "افزونه نصب شد",
@@ -115,5 +115,8 @@
     "TasksLibraryCategory": "کتابخانه",
     "TasksLibraryCategory": "کتابخانه",
     "TasksMaintenanceCategory": "تعمیر",
     "TasksMaintenanceCategory": "تعمیر",
     "Forced": "اجباری",
     "Forced": "اجباری",
-    "Default": "پیشفرض"
+    "Default": "پیشفرض",
+    "TaskCleanActivityLogDescription": "ورودی‌های قدیمی‌تر از سن تنظیم شده در سیاهه فعالیت را حذف می‌کند.",
+    "TaskCleanActivityLog": "پاکسازی سیاهه فعالیت",
+    "Undefined": "تعریف نشده"
 }
 }

+ 74 - 74
Emby.Server.Implementations/Localization/Core/fi.json

@@ -1,121 +1,121 @@
 {
 {
-    "HeaderLiveTV": "Live-TV",
-    "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
+    "HeaderLiveTV": "Live TV",
+    "NewVersionIsAvailable": "Uusi versio Jellyfin-palvelimesta on ladattavissa.",
     "NameSeasonUnknown": "Tuntematon kausi",
     "NameSeasonUnknown": "Tuntematon kausi",
     "NameSeasonNumber": "Kausi {0}",
     "NameSeasonNumber": "Kausi {0}",
     "NameInstallFailed": "{0} asennus epäonnistui",
     "NameInstallFailed": "{0} asennus epäonnistui",
     "MusicVideos": "Musiikkivideot",
     "MusicVideos": "Musiikkivideot",
     "Music": "Musiikki",
     "Music": "Musiikki",
     "Movies": "Elokuvat",
     "Movies": "Elokuvat",
-    "MixedContent": "Sekoitettu sisältö",
+    "MixedContent": "Sekalainen sisältö",
     "MessageServerConfigurationUpdated": "Palvelimen asetukset on päivitetty",
     "MessageServerConfigurationUpdated": "Palvelimen asetukset on päivitetty",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusryhmä {0} on päivitetty",
-    "MessageApplicationUpdatedTo": "Jellyfin palvelin on päivitetty versioon {0}",
-    "MessageApplicationUpdated": "Jellyfin palvelin on päivitetty",
-    "Latest": "Uusimmat",
-    "LabelRunningTimeValue": "Toiston kesto: {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusten osio {0} on päivitetty",
+    "MessageApplicationUpdatedTo": "Jellyfin-palvelin on päivitetty versioon {0}",
+    "MessageApplicationUpdated": "Jellyfin-palvelin on päivitetty",
+    "Latest": "Viimeisimmät",
+    "LabelRunningTimeValue": "Kesto: {0}",
     "LabelIpAddressValue": "IP-osoite: {0}",
     "LabelIpAddressValue": "IP-osoite: {0}",
     "ItemRemovedWithName": "{0} poistettiin kirjastosta",
     "ItemRemovedWithName": "{0} poistettiin kirjastosta",
     "ItemAddedWithName": "{0} lisättiin kirjastoon",
     "ItemAddedWithName": "{0} lisättiin kirjastoon",
-    "Inherit": "Periytyä",
+    "Inherit": "Peri",
     "HomeVideos": "Kotivideot",
     "HomeVideos": "Kotivideot",
     "HeaderRecordingGroups": "Tallennusryhmät",
     "HeaderRecordingGroups": "Tallennusryhmät",
     "HeaderNextUp": "Seuraavaksi",
     "HeaderNextUp": "Seuraavaksi",
     "HeaderFavoriteSongs": "Suosikkikappaleet",
     "HeaderFavoriteSongs": "Suosikkikappaleet",
     "HeaderFavoriteShows": "Suosikkisarjat",
     "HeaderFavoriteShows": "Suosikkisarjat",
     "HeaderFavoriteEpisodes": "Suosikkijaksot",
     "HeaderFavoriteEpisodes": "Suosikkijaksot",
-    "HeaderFavoriteArtists": "Suosikkiartistit",
+    "HeaderFavoriteArtists": "Suosikkiesittäjät",
     "HeaderFavoriteAlbums": "Suosikkialbumit",
     "HeaderFavoriteAlbums": "Suosikkialbumit",
-    "HeaderContinueWatching": "Jatka katsomista",
-    "HeaderAlbumArtists": "Albumin artistit",
+    "HeaderContinueWatching": "Jatka katselua",
+    "HeaderAlbumArtists": "Albumin esittäjät",
     "Genres": "Tyylilajit",
     "Genres": "Tyylilajit",
     "Folders": "Kansiot",
     "Folders": "Kansiot",
     "Favorites": "Suosikit",
     "Favorites": "Suosikit",
-    "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
+    "FailedLoginAttemptWithUserName": "Epäonnistunut kirjautumisyritys lähteestä \"{0}\"",
     "DeviceOnlineWithName": "{0} on yhdistetty",
     "DeviceOnlineWithName": "{0} on yhdistetty",
-    "DeviceOfflineWithName": "{0} yhteys on katkaistu",
+    "DeviceOfflineWithName": "{0} on katkaissut yhteyden",
     "Collections": "Kokoelmat",
     "Collections": "Kokoelmat",
-    "ChapterNameValue": "Jakso: {0}",
+    "ChapterNameValue": "Kappale {0}",
     "Channels": "Kanavat",
     "Channels": "Kanavat",
-    "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
+    "CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
     "Books": "Kirjat",
     "Books": "Kirjat",
-    "AuthenticationSucceededWithUserName": "{0} todennus onnistui",
-    "Artists": "Artistit",
+    "AuthenticationSucceededWithUserName": "{0} on todennettu",
+    "Artists": "Esittäjät",
     "Application": "Sovellus",
     "Application": "Sovellus",
     "AppDeviceValues": "Sovellus: {0}, Laite: {1}",
     "AppDeviceValues": "Sovellus: {0}, Laite: {1}",
     "Albums": "Albumit",
     "Albums": "Albumit",
     "User": "Käyttäjä",
     "User": "Käyttäjä",
     "System": "Järjestelmä",
     "System": "Järjestelmä",
     "ScheduledTaskFailedWithName": "{0} epäonnistui",
     "ScheduledTaskFailedWithName": "{0} epäonnistui",
-    "PluginUpdatedWithName": "{0} päivitetty",
-    "PluginInstalledWithName": "{0} asennettu",
-    "Photos": "Kuvat",
-    "ScheduledTaskStartedWithName": "{0} aloitettu",
-    "PluginUninstalledWithName": "{0} poistettu",
+    "PluginUpdatedWithName": "{0} päivitettiin",
+    "PluginInstalledWithName": "{0} asennettiin",
+    "Photos": "Valokuvat",
+    "ScheduledTaskStartedWithName": "\"{0}\" käynnistetty",
+    "PluginUninstalledWithName": "{0} poistettiin",
     "Playlists": "Soittolistat",
     "Playlists": "Soittolistat",
     "VersionNumber": "Versio {0}",
     "VersionNumber": "Versio {0}",
-    "ValueSpecialEpisodeName": "Erikois - {0}",
-    "ValueHasBeenAddedToLibrary": "{0} lisättiin mediakirjastoon",
-    "UserStoppedPlayingItemWithValues": "{0} toistaminen valmistui {1} laitteella {2}",
-    "UserStartedPlayingItemWithValues": "{0} toistaa {1} laitteella {2}",
-    "UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
-    "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
-    "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
-    "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
-    "UserLockedOutWithName": "Käyttäjä {0} lukittu",
-    "UserDownloadingItemWithValues": "{0} lataa {1}",
-    "UserDeletedWithName": "Käyttäjä {0} poistettu",
-    "UserCreatedWithName": "Käyttäjä {0} luotu",
-    "TvShows": "TV-ohjelmat",
-    "Sync": "Synkronoi",
-    "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
-    "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
+    "ValueSpecialEpisodeName": "Erikoisjakso - {0}",
+    "ValueHasBeenAddedToLibrary": "\"{0}\" on lisätty mediakirjastoon",
+    "UserStoppedPlayingItemWithValues": "{0} lopetti kohteen \"{1}\" toiston sijainnissa \"{2}\"",
+    "UserStartedPlayingItemWithValues": "{0} toistaa kohdetta \"{1}\" sijainnissa \"{2}\"",
+    "UserPolicyUpdatedWithName": "Käyttäjän {0} käyttöoikeudet on päivitetty",
+    "UserPasswordChangedWithName": "Käyttäjän {0} salasana on vaihdettu",
+    "UserOnlineFromDevice": "{0} on yhdistänyt sijainnista \"{1}\"",
+    "UserOfflineFromDevice": "{0} on katkaissut yhteyden sijainnista \"{1}\"",
+    "UserLockedOutWithName": "Käyttäjä {0} on lukittu",
+    "UserDownloadingItemWithValues": "{0} lataa kohdetta \"{1}\"",
+    "UserDeletedWithName": "Käyttäjä {0} on poistettu",
+    "UserCreatedWithName": "Käyttäjä {0} on luotu",
+    "TvShows": "Sarjat",
+    "Sync": "Synkronointi",
+    "SubtitleDownloadFailureFromForItem": "Tekstityksen lataus lähteestä \"{0}\" kohteelle \"{1}\" epäonnistui",
+    "StartupEmbyServerIsLoading": "Jellyfin-palvelin latautuu. Yritä hetken kuluttua uudelleen.",
     "Songs": "Kappaleet",
     "Songs": "Kappaleet",
-    "Shows": "Ohjelmat",
-    "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
-    "ProviderValue": "Tarjoaja: {0}",
-    "Plugin": "Liitännäinen",
-    "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
-    "NotificationOptionVideoPlayback": "Videota toistetaan",
-    "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
-    "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
-    "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
-    "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
-    "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
-    "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
-    "NotificationOptionPluginError": "Ongelma liitännäisessä",
-    "NotificationOptionNewLibraryContent": "Uutta sisältöä lisätty",
+    "Shows": "Sarjat",
+    "ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
+    "ProviderValue": "Lähde: {0}",
+    "Plugin": "Laajennus",
+    "NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu",
+    "NotificationOptionVideoPlayback": "Videon toisto aloitettu",
+    "NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
+    "NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui",
+    "NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys",
+    "NotificationOptionPluginUpdateInstalled": "Laajennus on päivitetty",
+    "NotificationOptionPluginUninstalled": "Laajennus on poistettu",
+    "NotificationOptionPluginInstalled": "Laajennus on asennettu",
+    "NotificationOptionPluginError": "Laajennuksen virhe",
+    "NotificationOptionNewLibraryContent": "Sisältöä on lisätty",
     "NotificationOptionInstallationFailed": "Asennus epäonnistui",
     "NotificationOptionInstallationFailed": "Asennus epäonnistui",
-    "NotificationOptionCameraImageUploaded": "Kameran kuva ladattu",
+    "NotificationOptionCameraImageUploaded": "Kameran kuva on tallennettu",
     "NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu",
     "NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu",
-    "NotificationOptionAudioPlayback": "Toistetaan ääntä",
-    "NotificationOptionApplicationUpdateInstalled": "Sovelluspäivitys asennettu",
-    "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla",
+    "NotificationOptionAudioPlayback": "Äänen toisto aloitettu",
+    "NotificationOptionApplicationUpdateInstalled": "Sovelluspäivitys asennettiin",
+    "NotificationOptionApplicationUpdateAvailable": "Sovelluspäivitys on saatavilla",
     "TasksMaintenanceCategory": "Ylläpito",
     "TasksMaintenanceCategory": "Ylläpito",
-    "TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.",
+    "TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä määritettyjen metatietoasetusten mukaisesti.",
     "TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset",
     "TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset",
     "TaskRefreshChannelsDescription": "Päivittää internet-kanavien tiedot.",
     "TaskRefreshChannelsDescription": "Päivittää internet-kanavien tiedot.",
     "TaskRefreshChannels": "Päivitä kanavat",
     "TaskRefreshChannels": "Päivitä kanavat",
-    "TaskCleanTranscodeDescription": "Poistaa transkoodatut tiedostot jotka ovat yli päivän vanhoja.",
-    "TaskCleanTranscode": "Puhdista transkoodaushakemisto",
-    "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset liitännäisille jotka on asetettu päivittymään automaattisesti.",
-    "TaskUpdatePlugins": "Päivitä liitännäiset",
-    "TaskRefreshPeopleDescription": "Päivittää näyttelijöiden ja ohjaajien mediatiedot kirjastossasi.",
+    "TaskCleanTranscodeDescription": "Poistaa päivää vanhemmat transkoodaustiedostot.",
+    "TaskCleanTranscode": "Puhdista transkoodauskansio",
+    "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset laajennuksille, jotka on määritetty päivittymään automaattisesti.",
+    "TaskUpdatePlugins": "Päivitä laajennukset",
+    "TaskRefreshPeopleDescription": "Päivittää mediakirjaston näyttelijöiden ja ohjaajien metatiedot.",
     "TaskRefreshPeople": "Päivitä henkilöt",
     "TaskRefreshPeople": "Päivitä henkilöt",
-    "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
-    "TaskCleanLogs": "Puhdista lokihakemisto",
-    "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
-    "TaskRefreshLibrary": "Skannaa mediakirjasto",
-    "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
-    "TaskRefreshChapterImages": "Pura jakson kuvat",
-    "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
-    "TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
-    "TasksChannelsCategory": "Internet kanavat",
+    "TaskCleanLogsDescription": "Poistaa {0} päivää vanhemmat lokitiedostot.",
+    "TaskCleanLogs": "Siivoa lokikansio",
+    "TaskRefreshLibraryDescription": "Tarkastaa mediakirjastosi sisällön uusien tiedostojen varalta ja päivittää metatiedot.",
+    "TaskRefreshLibrary": "Päivitä mediakirjasto",
+    "TaskRefreshChapterImagesDescription": "Luo esikatselukuvat videoille, jotka sisältävät kappalejaon.",
+    "TaskRefreshChapterImages": "Pura kappalejaon kuvat",
+    "TaskCleanCacheDescription": "Poistaa tarpeettomiksi jääneet väliaikaistiedostot.",
+    "TaskCleanCache": "Tyhjennä välimuistikansio",
+    "TasksChannelsCategory": "Internet-kanavat",
     "TasksApplicationCategory": "Sovellus",
     "TasksApplicationCategory": "Sovellus",
     "TasksLibraryCategory": "Kirjasto",
     "TasksLibraryCategory": "Kirjasto",
     "Forced": "Pakotettu",
     "Forced": "Pakotettu",
     "Default": "Oletus",
     "Default": "Oletus",
-    "TaskCleanActivityLogDescription": "Poistaa määritettyä vanhemmat tapahtumat aktiviteettilokista.",
-    "TaskCleanActivityLog": "Tyhjennä aktiviteettiloki",
+    "TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
+    "TaskCleanActivityLog": "Tyhjennä toimintahistoria",
     "Undefined": "Määrittelemätön"
     "Undefined": "Määrittelemätön"
 }
 }

+ 53 - 50
Emby.Server.Implementations/Localization/Core/fil.json

@@ -3,101 +3,101 @@
     "ValueSpecialEpisodeName": "Espesyal - {0}",
     "ValueSpecialEpisodeName": "Espesyal - {0}",
     "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
     "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
     "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
     "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
-    "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
-    "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
+    "UserStartedPlayingItemWithValues": "Si {0} ay nagpla-play ng {1} sa {2}",
+    "UserPolicyUpdatedWithName": "Ang user policy ay nai-update para kay {0}",
     "UserPasswordChangedWithName": "Napalitan na ang password ni {0}",
     "UserPasswordChangedWithName": "Napalitan na ang password ni {0}",
-    "UserOnlineFromDevice": "Si {0} ay nakakonekta galing sa {1}",
-    "UserOfflineFromDevice": "Si {0} ay nadiskonekta galing sa {1}",
+    "UserOnlineFromDevice": "Si {0} ay naka-konekta galing sa {1}",
+    "UserOfflineFromDevice": "Si {0} ay na-diskonekta galing sa {1}",
     "UserLockedOutWithName": "Si {0} ay nalock out",
     "UserLockedOutWithName": "Si {0} ay nalock out",
     "UserDownloadingItemWithValues": "Nagdadownload si {0} ng {1}",
     "UserDownloadingItemWithValues": "Nagdadownload si {0} ng {1}",
     "UserDeletedWithName": "Natanggal na is user {0}",
     "UserDeletedWithName": "Natanggal na is user {0}",
     "UserCreatedWithName": "Nagawa na si user {0}",
     "UserCreatedWithName": "Nagawa na si user {0}",
     "User": "User",
     "User": "User",
-    "TvShows": "Pelikula",
+    "TvShows": "Mga Palabas sa Telebisyon",
     "System": "Sistema",
     "System": "Sistema",
     "Sync": "Pag-sync",
     "Sync": "Pag-sync",
-    "SubtitleDownloadFailureFromForItem": "Hindi naidownload ang subtitles {0} para sa {1}",
-    "StartupEmbyServerIsLoading": "Nagloload ang Jellyfin Server. Sandaling maghintay.",
-    "Songs": "Kanta",
-    "Shows": "Pelikula",
+    "SubtitleDownloadFailureFromForItem": "Hindi nai-download ang subtitles {0} para sa {1}",
+    "StartupEmbyServerIsLoading": "Naglo-load ang Jellyfin Server. Mangyaring subukan ulit sandali.",
+    "Songs": "Mga Kanta",
+    "Shows": "Mga Pelikula",
     "ServerNameNeedsToBeRestarted": "Kailangan irestart ang {0}",
     "ServerNameNeedsToBeRestarted": "Kailangan irestart ang {0}",
     "ScheduledTaskStartedWithName": "Nagsimula na ang {0}",
     "ScheduledTaskStartedWithName": "Nagsimula na ang {0}",
-    "ScheduledTaskFailedWithName": "Hindi gumana and {0}",
-    "ProviderValue": "Ang provider ay {0}",
+    "ScheduledTaskFailedWithName": "Hindi gumana ang {0}",
+    "ProviderValue": "Tagapagtustos: {0}",
     "PluginUpdatedWithName": "Naiupdate na ang {0}",
     "PluginUpdatedWithName": "Naiupdate na ang {0}",
     "PluginUninstalledWithName": "Naiuninstall na ang {0}",
     "PluginUninstalledWithName": "Naiuninstall na ang {0}",
     "PluginInstalledWithName": "Nainstall na ang {0}",
     "PluginInstalledWithName": "Nainstall na ang {0}",
     "Plugin": "Plugin",
     "Plugin": "Plugin",
-    "Playlists": "Playlists",
-    "Photos": "Larawan",
+    "Playlists": "Mga Playlist",
+    "Photos": "Mga Larawan",
     "NotificationOptionVideoPlaybackStopped": "Huminto na ang pelikula",
     "NotificationOptionVideoPlaybackStopped": "Huminto na ang pelikula",
     "NotificationOptionVideoPlayback": "Nagsimula na ang pelikula",
     "NotificationOptionVideoPlayback": "Nagsimula na ang pelikula",
-    "NotificationOptionUserLockedOut": "Nakalock out ang user",
+    "NotificationOptionUserLockedOut": "Naka-lock out ang user",
     "NotificationOptionTaskFailed": "Hindi gumana ang scheduled task",
     "NotificationOptionTaskFailed": "Hindi gumana ang scheduled task",
-    "NotificationOptionServerRestartRequired": "Kailangan irestart ang server",
-    "NotificationOptionPluginUpdateInstalled": "Naiupdate na ang plugin",
-    "NotificationOptionPluginUninstalled": "Naiuninstall na ang plugin",
+    "NotificationOptionServerRestartRequired": "Kailangan i-restart ang server",
+    "NotificationOptionPluginUpdateInstalled": "Nai-update na ang plugin",
+    "NotificationOptionPluginUninstalled": "Nai-uninstall na ang plugin",
     "NotificationOptionPluginInstalled": "Nainstall na ang plugin",
     "NotificationOptionPluginInstalled": "Nainstall na ang plugin",
     "NotificationOptionPluginError": "Hindi gumagana ang plugin",
     "NotificationOptionPluginError": "Hindi gumagana ang plugin",
     "NotificationOptionNewLibraryContent": "May bagong content na naidagdag",
     "NotificationOptionNewLibraryContent": "May bagong content na naidagdag",
     "NotificationOptionInstallationFailed": "Hindi nainstall ng mabuti",
     "NotificationOptionInstallationFailed": "Hindi nainstall ng mabuti",
-    "NotificationOptionCameraImageUploaded": "Naiupload na ang picture",
+    "NotificationOptionCameraImageUploaded": "Naiupload na ang litrato",
     "NotificationOptionAudioPlaybackStopped": "Huminto na ang patugtog",
     "NotificationOptionAudioPlaybackStopped": "Huminto na ang patugtog",
     "NotificationOptionAudioPlayback": "Nagsimula na ang patugtog",
     "NotificationOptionAudioPlayback": "Nagsimula na ang patugtog",
     "NotificationOptionApplicationUpdateInstalled": "Naiupdate na ang aplikasyon",
     "NotificationOptionApplicationUpdateInstalled": "Naiupdate na ang aplikasyon",
     "NotificationOptionApplicationUpdateAvailable": "May bagong update ang aplikasyon",
     "NotificationOptionApplicationUpdateAvailable": "May bagong update ang aplikasyon",
-    "NewVersionIsAvailable": "May bagong version ng Jellyfin Server na pwede idownload.",
-    "NameSeasonUnknown": "Hindi alam ang season",
+    "NewVersionIsAvailable": "May bagong version ng Jellyfin Server na pwede i-download.",
+    "NameSeasonUnknown": "Hindi matukoy ang season",
     "NameSeasonNumber": "Season {0}",
     "NameSeasonNumber": "Season {0}",
     "NameInstallFailed": "Hindi nainstall ang {0}",
     "NameInstallFailed": "Hindi nainstall ang {0}",
-    "MusicVideos": "Music video",
-    "Music": "Kanta",
-    "Movies": "Pelikula",
+    "MusicVideos": "Mga Music video",
+    "Music": "Mga Kanta",
+    "Movies": "Mga Pelikula",
     "MixedContent": "Halo-halong content",
     "MixedContent": "Halo-halong content",
     "MessageServerConfigurationUpdated": "Naiupdate na ang server configuration",
     "MessageServerConfigurationUpdated": "Naiupdate na ang server configuration",
     "MessageNamedServerConfigurationUpdatedWithValue": "Naiupdate na ang server configuration section {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Naiupdate na ang server configuration section {0}",
-    "MessageApplicationUpdatedTo": "Ang Jellyfin Server ay naiupdate to {0}",
+    "MessageApplicationUpdatedTo": "Ang bersyon ng Jellyfin Server ay naiupdate sa {0}",
     "MessageApplicationUpdated": "Naiupdate na ang Jellyfin Server",
     "MessageApplicationUpdated": "Naiupdate na ang Jellyfin Server",
     "Latest": "Pinakabago",
     "Latest": "Pinakabago",
     "LabelRunningTimeValue": "Oras: {0}",
     "LabelRunningTimeValue": "Oras: {0}",
-    "LabelIpAddressValue": "Ang IP Address ay {0}",
+    "LabelIpAddressValue": "IP address: {0}",
     "ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
     "ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
     "ItemAddedWithName": "Naidagdag ang {0} sa librerya",
     "ItemAddedWithName": "Naidagdag ang {0} sa librerya",
     "Inherit": "Manahin",
     "Inherit": "Manahin",
     "HeaderRecordingGroups": "Pagtatalang Grupo",
     "HeaderRecordingGroups": "Pagtatalang Grupo",
     "HeaderNextUp": "Susunod",
     "HeaderNextUp": "Susunod",
     "HeaderLiveTV": "Live TV",
     "HeaderLiveTV": "Live TV",
-    "HeaderFavoriteSongs": "Paboritong Kanta",
-    "HeaderFavoriteShows": "Paboritong Pelikula",
-    "HeaderFavoriteEpisodes": "Paboritong Episodes",
-    "HeaderFavoriteArtists": "Paboritong Artista",
-    "HeaderFavoriteAlbums": "Paboritong Albums",
-    "HeaderContinueWatching": "Ituloy Manood",
-    "HeaderAlbumArtists": "Artista ng Album",
-    "Genres": "Kategorya",
-    "Folders": "Folders",
-    "Favorites": "Paborito",
-    "FailedLoginAttemptWithUserName": "maling login galing {0}",
-    "DeviceOnlineWithName": "nakakonekta si {0}",
-    "DeviceOfflineWithName": "nadiskonekta si {0}",
-    "Collections": "Koleksyon",
+    "HeaderFavoriteSongs": "Mga Paboritong Kanta",
+    "HeaderFavoriteShows": "Mga Paboritong Pelikula",
+    "HeaderFavoriteEpisodes": "Mga Paboritong Episode",
+    "HeaderFavoriteArtists": "Mga Paboritong Artista",
+    "HeaderFavoriteAlbums": "Mga Paboritong Album",
+    "HeaderContinueWatching": "Magpatuloy sa Panonood",
+    "HeaderAlbumArtists": "Mga Artista ng Album",
+    "Genres": "Mga Kategorya",
+    "Folders": "Mga Folder",
+    "Favorites": "Mga Paborito",
+    "FailedLoginAttemptWithUserName": "Maling login galing kay/sa {0}",
+    "DeviceOnlineWithName": "Nakakonekta si/ang {0}",
+    "DeviceOfflineWithName": "Nadiskonekta si/ang {0}",
+    "Collections": "Mga Koleksyon",
     "ChapterNameValue": "Kabanata {0}",
     "ChapterNameValue": "Kabanata {0}",
-    "Channels": "Channel",
-    "CameraImageUploadedFrom": "May bagong larawan na naupload galing {0}",
-    "Books": "Libro",
-    "AuthenticationSucceededWithUserName": "{0} na patunayan",
-    "Artists": "Artista",
+    "Channels": "Mga Channel",
+    "CameraImageUploadedFrom": "May bagong larawan na naupload galing sa/kay {0}",
+    "Books": "Mga Libro",
+    "AuthenticationSucceededWithUserName": "Napatunayan si/ang {0}",
+    "Artists": "Mga Artista",
     "Application": "Aplikasyon",
     "Application": "Aplikasyon",
     "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
     "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
-    "Albums": "Albums",
+    "Albums": "Mga Album",
     "TaskRefreshLibrary": "Suriin and Librerya ng Medya",
     "TaskRefreshLibrary": "Suriin and Librerya ng Medya",
     "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
     "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
     "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
     "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
-    "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
+    "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng sistema.",
     "TasksChannelsCategory": "Palabas sa internet",
     "TasksChannelsCategory": "Palabas sa internet",
     "TasksLibraryCategory": "Librerya",
     "TasksLibraryCategory": "Librerya",
     "TasksMaintenanceCategory": "Pagpapanatili",
     "TasksMaintenanceCategory": "Pagpapanatili",
-    "HomeVideos": "Sariling pelikula",
+    "HomeVideos": "Sariling video/pelikula",
     "TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
     "TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
     "TaskRefreshPeople": "I-refresh ang Tauhan",
     "TaskRefreshPeople": "I-refresh ang Tauhan",
     "TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
     "TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
@@ -105,14 +105,17 @@
     "TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
     "TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
     "TaskRefreshChannels": "I-refresh ang Channels",
     "TaskRefreshChannels": "I-refresh ang Channels",
     "TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
     "TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
-    "TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa automatikong pag update.",
+    "TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa awtomatikong pag-update.",
     "TaskUpdatePlugins": "I-update ang Plugins",
     "TaskUpdatePlugins": "I-update ang Plugins",
     "TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
     "TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
     "TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
     "TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
     "TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
     "TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
     "TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
     "TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
     "TaskCleanCache": "Linisin and Direktoryo ng Cache",
     "TaskCleanCache": "Linisin and Direktoryo ng Cache",
-    "TasksApplicationCategory": "Application",
+    "TasksApplicationCategory": "Aplikasyon",
     "TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
     "TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
-    "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas matanda sa naka configure na edad."
+    "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas luma sa nakatakda na edad.",
+    "Default": "Default",
+    "Undefined": "Hindi tiyak",
+    "Forced": "Sapilitan"
 }
 }

+ 27 - 2
Emby.Server.Implementations/Localization/Core/hi.json

@@ -1,5 +1,5 @@
 {
 {
-    "Albums": "संग्रह",
+    "Albums": "एल्बम",
     "HeaderRecordingGroups": "रिकॉर्डिंग समूह",
     "HeaderRecordingGroups": "रिकॉर्डिंग समूह",
     "HeaderNextUp": "इसके बाद",
     "HeaderNextUp": "इसके बाद",
     "HeaderLiveTV": "लाइव टीवी",
     "HeaderLiveTV": "लाइव टीवी",
@@ -26,5 +26,30 @@
     "AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत",
     "AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत",
     "Artists": "कलाकारों",
     "Artists": "कलाकारों",
     "Application": "एप्लिकेशन",
     "Application": "एप्लिकेशन",
-    "AppDeviceValues": "एप: {0}, मशीन: {1}"
+    "AppDeviceValues": "एप: {0}, उपकरण: {1}",
+    "NotificationOptionPluginUninstalled": "प्लगइन अनइंस्टाल हो गया",
+    "NotificationOptionPluginInstalled": "प्लगइन इनस्टॉल हो गया",
+    "NotificationOptionPluginError": "प्लगइन फ़ैल हो गया",
+    "NotificationOptionInstallationFailed": "इंस्टालेशन फ़ैल हो गया",
+    "NotificationOptionAudioPlaybackStopped": "संगीत बंद कर दिया गया",
+    "NotificationOptionAudioPlayback": "संगीत शुरू कर दिया गया",
+    "NotificationOptionCameraImageUploaded": "कैमरा फोटो अपलोड किया गया",
+    "NotificationOptionApplicationUpdateInstalled": "एप्लीकेशन अपडेट इनस्टॉल कर दिया है",
+    "NotificationOptionApplicationUpdateAvailable": "एप्लीकेशन अपडेट उपलभ्द है",
+    "NewVersionIsAvailable": "जेलीफिन सर्वर का एक नया वर्जन डाउनलोड के लिए उपलब्ध है।",
+    "NameSeasonUnknown": "अनजान भाग",
+    "NameSeasonNumber": "भाग {0}",
+    "NameInstallFailed": "{0} इनस्टॉल करते समय फेल हो गया है",
+    "MusicVideos": "संगीत वीडियो",
+    "Music": "संगीत",
+    "Movies": "फ़िल्म",
+    "MixedContent": "मिला-जुला कंटेंट",
+    "MessageServerConfigurationUpdated": "सर्वर कॉन्फ़िगरेशन अपडेट हो गया है",
+    "MessageNamedServerConfigurationUpdatedWithValue": "सर्वर कॉन्फ़िगरेशन भाग {0} अपडेट हो गया है",
+    "MessageApplicationUpdatedTo": "जैलीफिन सर्वर {0} में अपडेट हो गया है",
+    "MessageApplicationUpdated": "जैलीफिन सर्वर अपडेट हो गया है",
+    "Latest": "सबसे नया",
+    "LabelIpAddressValue": "आई पी एड्रेस: {0}",
+    "ItemRemovedWithName": "{0} लाइब्रेरी में से निकाल दिया है",
+    "HomeVideos": "होम वीडियोस"
 }
 }

+ 110 - 110
Emby.Server.Implementations/Localization/Core/kk.json

@@ -1,122 +1,122 @@
 {
 {
-    "Albums": "Álbomdar",
-    "AppDeviceValues": "Qoldanba: {0}, Qurylǵy: {1}",
+    "Albums": "Älbomdar",
+    "AppDeviceValues": "Qoldanba: {0}, Qūrylğy: {1}",
     "Application": "Qoldanba",
     "Application": "Qoldanba",
-    "Artists": "Oryndaýshylar",
-    "AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy",
-    "Books": "Kitaptar",
-    "CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep salyndy",
+    "Artists": "Oryndaylar",
+    "AuthenticationSucceededWithUserName": "{0} tüpnūsqalyq rastaluy sättı aiaqtaldy",
+    "Books": "Kıtaptar",
+    "CameraImageUploadedFrom": "{0} kamerasynan jaŋa suret jüktep salyndy",
     "Channels": "Arnalar",
     "Channels": "Arnalar",
     "ChapterNameValue": "{0}-sahna",
     "ChapterNameValue": "{0}-sahna",
-    "Collections": "Jıyntyqtar",
-    "DeviceOfflineWithName": "{0} ajyratylǵan",
-    "DeviceOnlineWithName": "{0} qosylǵan",
-    "FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz aıaqtaldy",
-    "Favorites": "Tańdaýlylar",
+    "Collections": "Jiyntyqtar",
+    "DeviceOfflineWithName": "{0} ajyratylğan",
+    "DeviceOnlineWithName": "{0} qosylğan",
+    "FailedLoginAttemptWithUserName": "{0} tarapynan kıru äreketı sätsız aiaqtaldy",
+    "Favorites": "Taŋdaulylar",
     "Folders": "Qaltalar",
     "Folders": "Qaltalar",
     "Genres": "Janrlar",
     "Genres": "Janrlar",
-    "HeaderAlbumArtists": "Álbom oryndaýshylary",
-    "HeaderContinueWatching": "Qaraýdy jalǵastyrý",
-    "HeaderFavoriteAlbums": "Tańdaýly álbomdar",
-    "HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",
-    "HeaderFavoriteEpisodes": "Tańdaýly bólimder",
-    "HeaderFavoriteShows": "Tańdaýly kórsetimder",
-    "HeaderFavoriteSongs": "Tańdaýly áýender",
-    "HeaderLiveTV": "Efır",
-    "HeaderNextUp": "Kezekti",
+    "HeaderAlbumArtists": "Älbom oryndauşylary",
+    "HeaderContinueWatching": "Qaraudy jalğastyru",
+    "HeaderFavoriteAlbums": "Taŋdauly älbomdar",
+    "HeaderFavoriteArtists": "Taŋdauly oryndauşylar",
+    "HeaderFavoriteEpisodes": "Taŋdauly telebölımder",
+    "HeaderFavoriteShows": "Taŋdauly körsetımder",
+    "HeaderFavoriteSongs": "Taŋdauly äuender",
+    "HeaderLiveTV": "Efir",
+    "HeaderNextUp": "Kezektı",
     "HeaderRecordingGroups": "Jazba toptary",
     "HeaderRecordingGroups": "Jazba toptary",
-    "HomeVideos": "Úılik beıneler",
-    "Inherit": "Muraǵa ıelený",
-    "ItemAddedWithName": "{0} tasyǵyshhanaǵa ústeldi",
-    "ItemRemovedWithName": "{0} tasyǵyshhanadan alastaldy",
-    "LabelIpAddressValue": "IP-mekenjaıy: {0}",
-    "LabelRunningTimeValue": "Oınatý ýaqyty: {0}",
-    "Latest": "Eń keıingi",
-    "MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
-    "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Server konfıgýrasýasynyń {0} bólimi jańartyldy",
-    "MessageServerConfigurationUpdated": "Server konfıgýrasıasy jańartyldy",
-    "MixedContent": "Aralas mazmun",
-    "Movies": "Fılmder",
-    "Music": "Mýzyka",
-    "MusicVideos": "Mýzykalyq beıneler",
-    "NameInstallFailed": "{0} ornatylýy sátsiz",
-    "NameSeasonNumber": "{0}-maýsym",
-    "NameSeasonUnknown": "Belgisiz maýsym",
-    "NewVersionIsAvailable": "Jańa Jellyfin Server nusqasy júktep alýǵa qoljetimdi.",
-    "NotificationOptionApplicationUpdateAvailable": "Qoldanba jańartýy qoljetimdi",
-    "NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy",
-    "NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy",
-    "NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy",
-    "NotificationOptionCameraImageUploaded": "Kameradan fotosýret júktep salynǵan",
-    "NotificationOptionInstallationFailed": "Ornatý sátsizdigi",
-    "NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen",
-    "NotificationOptionPluginError": "Plagın sátsizdigi",
-    "NotificationOptionPluginInstalled": "Plagın ornatyldy",
-    "NotificationOptionPluginUninstalled": "Plagın ornatýy boldyrylmady",
-    "NotificationOptionPluginUpdateInstalled": "Plagın jańartýy ornatyldy",
-    "NotificationOptionServerRestartRequired": "Serverdi qaıta iske qosý qajet",
-    "NotificationOptionTaskFailed": "Josparlaǵan tapsyrma sátsizdigi",
-    "NotificationOptionUserLockedOut": "Paıdalanýshy qursaýly",
-    "NotificationOptionVideoPlayback": "Beıne oınatýy bastaldy",
-    "NotificationOptionVideoPlaybackStopped": "Beıne oınatýy toqtatyldy",
-    "Photos": "Fotosýretter",
-    "Playlists": "Oınatý tizimderi",
-    "Plugin": "Plagın",
+    "HomeVideos": "Üilık beineler",
+    "Inherit": "İelenu",
+    "ItemAddedWithName": "{0} tasyğyşhanağa üstelindı",
+    "ItemRemovedWithName": "{0} tasyğyşhanadan alastaldy",
+    "LabelIpAddressValue": "IP-mekenjaiy: {0}",
+    "LabelRunningTimeValue": "Oinatu uaqyty: {0}",
+    "Latest": "Eŋ keiıngı",
+    "MessageApplicationUpdated": "Jellyfin Serverı jaŋartyldy",
+    "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jaŋartyldy",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Server teŋşelımderınıŋ {0} bölımı jaŋartyldy",
+    "MessageServerConfigurationUpdated": "Server teŋşelımderı jaŋartyldy",
+    "MixedContent": "Aralas mazmūn",
+    "Movies": "Filmder",
+    "Music": "Muzyka",
+    "MusicVideos": "Muzykalyq beineler",
+    "NameInstallFailed": "{0} ornatyluy sätsız",
+    "NameSeasonNumber": "{0}-mausym",
+    "NameSeasonUnknown": "Belgısız mausym",
+    "NewVersionIsAvailable": "Jaŋa Jellyfin Server nūsqasy jüktep aluğa qoljetımdı.",
+    "NotificationOptionApplicationUpdateAvailable": "Qoldanba jaŋartuy qoljetımdı",
+    "NotificationOptionApplicationUpdateInstalled": "Qoldanba jaŋartuy ornatyldy",
+    "NotificationOptionAudioPlayback": "Dybys oinatuy bastaldy",
+    "NotificationOptionAudioPlaybackStopped": "Dybys oinatuy toqtatyldy",
+    "NotificationOptionCameraImageUploaded": "Kameradan fotosuret jüktep salynğan",
+    "NotificationOptionInstallationFailed": "Ornatu sätsızdıgı",
+    "NotificationOptionNewLibraryContent": "Jaŋa mazmūn üstelıngen",
+    "NotificationOptionPluginError": "Plagin sätsızdıgı",
+    "NotificationOptionPluginInstalled": "Plagin ornatyldy",
+    "NotificationOptionPluginUninstalled": "Plagin ornatuy boldyrylmady",
+    "NotificationOptionPluginUpdateInstalled": "Plagin jaŋartuy ornatyldy",
+    "NotificationOptionServerRestartRequired": "Serverdı qaita ıske qosu qajet",
+    "NotificationOptionTaskFailed": "Josparlağan tapsyrma sätsızdıgı",
+    "NotificationOptionUserLockedOut": "Paidalanuşy qūrsauly",
+    "NotificationOptionVideoPlayback": "Beine oinatuy bastaldy",
+    "NotificationOptionVideoPlaybackStopped": "Beine oinatuy toqtatyldy",
+    "Photos": "Fotosuretter",
+    "Playlists": "Oinatu tızımderı",
+    "Plugin": "Plagin",
     "PluginInstalledWithName": "{0} ornatyldy",
     "PluginInstalledWithName": "{0} ornatyldy",
-    "PluginUninstalledWithName": "{0} joıyldy",
-    "PluginUpdatedWithName": "{0} jańartyldy",
-    "ProviderValue": "Jetkizýshi: {0}",
-    "ScheduledTaskFailedWithName": "{0} sátsiz",
-    "ScheduledTaskStartedWithName": "{0} iske qosyldy",
-    "ServerNameNeedsToBeRestarted": "{0} qaıta iske qosý qajet",
-    "Shows": "Kórsetimder",
-    "Songs": "Áýender",
-    "StartupEmbyServerIsLoading": "Jellyfin Server júktelýde. Áreketti kóp uzamaı qaıtalańyz.",
+    "PluginUninstalledWithName": "{0} joiyldy",
+    "PluginUpdatedWithName": "{0} jaŋartyldy",
+    "ProviderValue": "Jetkızuşı: {0}",
+    "ScheduledTaskFailedWithName": "{0} sätsız",
+    "ScheduledTaskStartedWithName": "{0} ıske qosyldy",
+    "ServerNameNeedsToBeRestarted": "{0} qaita ıske qosu qajet",
+    "Shows": "Körsetımder",
+    "Songs": "Äuender",
+    "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalaŋyz.",
     "SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз",
     "SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз",
-    "SubtitleDownloadFailureFromForItem": "{1} úshin sýbtıtrlerdi {0} kózinen júktep alý sátsiz",
-    "Sync": "Úndestirý",
-    "System": "Júıe",
-    "TvShows": "TD-kórsetimder",
-    "User": "Paıdalanýshy",
-    "UserCreatedWithName": "Paıdalanýshy {0} jasalǵan",
-    "UserDeletedWithName": "Paıdalanýshy {0} joıylǵan",
-    "UserDownloadingItemWithValues": "{0} mynany júktep alýda: {1}",
-    "UserLockedOutWithName": "Paıdalanýshy {0} qursaýly",
-    "UserOfflineFromDevice": "{0} - {1} tarapynan ajyratylǵan",
-    "UserOnlineFromDevice": "{0} - {1} arqyly qosylǵan",
-    "UserPasswordChangedWithName": "Paıdalanýshy {0} úshin paról ózgertildi",
-    "UserPolicyUpdatedWithName": "Paıdalanýshy {0} úshin saıasattary jańartyldy",
-    "UserStartedPlayingItemWithValues": "{0} - {1} oınatýyn {2} bastady",
-    "UserStoppedPlayingItemWithValues": "{0} - {1} oınatýyn {2} toqtatty",
-    "ValueHasBeenAddedToLibrary": "{0} (tasyǵyshhanaǵa ústelindi)",
-    "ValueSpecialEpisodeName": "Arnaıy - {0}",
-    "VersionNumber": "Nusqasy {0}",
-    "Default": "Ádepki",
-    "TaskDownloadMissingSubtitles": "Joq sýbtıtrlerdi júktep alý",
-    "TaskRefreshChannels": "Arnalardy jańartý",
-    "TaskCleanTranscode": "Qaıta kodtaý katalogyn tazalaý",
-    "TaskUpdatePlugins": "Plagınderdi jańartý",
-    "TaskRefreshPeople": "Adamdardy jańartý",
-    "TaskCleanLogs": "Jurnal katalogyn tazalaý",
-    "TaskRefreshLibrary": "Tasyǵyshhanany skanerleý",
-    "TaskRefreshChapterImages": "Sahna keskinderin shyǵaryp alý",
-    "TaskCleanCache": "Kesh katalogyn tazalaý",
-    "TaskCleanActivityLog": "Áreket jurnalyn tazalaý",
+    "SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız",
+    "Sync": "Ündestıru",
+    "System": "Jüie",
+    "TvShows": "TD-körsetımder",
+    "User": "Paidalanuşy",
+    "UserCreatedWithName": "Paidalanuşy {0} jasalğan",
+    "UserDeletedWithName": "Paidalanuşy {0} joiylğan",
+    "UserDownloadingItemWithValues": "{0} — {1} jüktep aluda",
+    "UserLockedOutWithName": "Paidalanuşy {0} qūrsaulanğan",
+    "UserOfflineFromDevice": "{0} — {1} tarapynan ajyratyldy",
+    "UserOnlineFromDevice": "{0} — {1} tarapynan qosyldy",
+    "UserPasswordChangedWithName": "Paidalanuşy {0} üşın paröl özgertıldı",
+    "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jaŋartyldy",
+    "UserStartedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuda",
+    "UserStoppedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuyn toqtatty",
+    "ValueHasBeenAddedToLibrary": "{0} tasyğyşhanağa üstelındı",
+    "ValueSpecialEpisodeName": "Arnaiy - {0}",
+    "VersionNumber": "Nūsqasy {0}",
+    "Default": "Ädepkı",
+    "TaskDownloadMissingSubtitles": "Joq subtitrlerdı jüktep alu",
+    "TaskRefreshChannels": "Arnalardy jaŋğyrtu",
+    "TaskCleanTranscode": "Qaita kodtau katalogyn tazalau",
+    "TaskUpdatePlugins": "Plaginderdı jaŋartu",
+    "TaskRefreshPeople": "Adamdardy jaŋğyrtu",
+    "TaskCleanLogs": "Jūrnal katalogyn tazalau",
+    "TaskRefreshLibrary": "Tasyğyşhanany skanerleu",
+    "TaskRefreshChapterImages": "Sahna suretterın şyğaryp alu",
+    "TaskCleanCache": "Keş katalogyn tazalau",
+    "TaskCleanActivityLog": "Äreket jūrnalyn tazalau",
     "TasksChannelsCategory": "Internet-arnalar",
     "TasksChannelsCategory": "Internet-arnalar",
     "TasksApplicationCategory": "Qoldanba",
     "TasksApplicationCategory": "Qoldanba",
-    "TasksLibraryCategory": "Tasyǵyshhana",
-    "TasksMaintenanceCategory": "Qyzmet kórsetý",
-    "Undefined": "Anyqtalmady",
-    "Forced": "Májbúrli",
-    "TaskDownloadMissingSubtitlesDescription": "Metaderekter teńshelimi negіzіnde joq sýbtıtrlerdі Internetten іzdeıdі.",
-    "TaskRefreshChannelsDescription": "Internet-arnalar málimetterin jańartady.",
-    "TaskCleanTranscodeDescription": "Bіr kúnnen asqan qaıta kodtaý faıldaryn joıady.",
-    "TaskUpdatePluginsDescription": "Avtomatty túrde jańartýǵa teńshelgen plagınder úshin jańartýlardy júktep alady jáne ornatady.",
-    "TaskRefreshPeopleDescription": "Tasyǵyshhanadaǵy aktórler men rejısórler metaderekterіn jańartady.",
-    "TaskCleanLogsDescription": "{0} kúnnen asqan jurnal faıldaryn joıady.",
-    "TaskRefreshLibraryDescription": "Tasyǵyshhanadaǵy jańa faıldardy skanerleıdі jáne metaderekterdі jańartady.",
-    "TaskRefreshChapterImagesDescription": "Sahnalarǵa bólіngen beıneler úshіn nobaılar jasaıdy.",
-    "TaskCleanCacheDescription": "Júıede qajet emes keshtelgen faıldardy joıady.",
-    "TaskCleanActivityLogDescription": "Áreketter jurnalyndaǵy teńshelgen jasynan asqan jazbalaly joıady."
+    "TasksLibraryCategory": "Tasyğyşhana",
+    "TasksMaintenanceCategory": "Qyzmet körsetu",
+    "Undefined": "Anyqtalmağan",
+    "Forced": "Mäjbürlı",
+    "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı Internetten ızdeidı.",
+    "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.",
+    "TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.",
+    "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.",
+    "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.",
+    "TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.",
+    "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.",
+    "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşіn nobailar jasaidy.",
+    "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
+    "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady."
 }
 }

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

@@ -117,5 +117,6 @@
     "TaskCleanActivityLog": "Tøm aktivitetslogg",
     "TaskCleanActivityLog": "Tøm aktivitetslogg",
     "Undefined": "Udefinert",
     "Undefined": "Udefinert",
     "Forced": "Tvungen",
     "Forced": "Tvungen",
-    "Default": "Standard"
+    "Default": "Standard",
+    "TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen."
 }
 }

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

@@ -89,8 +89,8 @@
     "UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены",
     "UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены",
     "UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}",
     "UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}",
     "UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}",
     "UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}",
-    "ValueHasBeenAddedToLibrary": "{0} (добавлено в медиатеку)",
-    "ValueSpecialEpisodeName": "Специальный эпизод - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} добавлено в медиатеку",
+    "ValueSpecialEpisodeName": "Спецэпизод - {0}",
     "VersionNumber": "Версия {0}",
     "VersionNumber": "Версия {0}",
     "TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров",
     "TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров",
     "TaskRefreshChannels": "Обновление каналов",
     "TaskRefreshChannels": "Обновление каналов",

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

@@ -117,5 +117,5 @@
     "TaskCleanActivityLogDescription": "Radera aktivitets logg inlägg som är äldre än definerad ålder.",
     "TaskCleanActivityLogDescription": "Radera aktivitets logg inlägg som är äldre än definerad ålder.",
     "TaskCleanActivityLog": "Rensa Aktivitets Logg",
     "TaskCleanActivityLog": "Rensa Aktivitets Logg",
     "Undefined": "odefinierad",
     "Undefined": "odefinierad",
-    "Forced": "Tvinga"
+    "Forced": "Tvingad"
 }
 }

+ 3 - 2
Emby.Server.Implementations/Localization/Core/tr.json

@@ -12,7 +12,7 @@
     "DeviceOfflineWithName": "{0} bağlantısı kesildi",
     "DeviceOfflineWithName": "{0} bağlantısı kesildi",
     "DeviceOnlineWithName": "{0} bağlı",
     "DeviceOnlineWithName": "{0} bağlı",
     "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
     "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
-    "Favorites": "Favorilerim",
+    "Favorites": "Favoriler",
     "Folders": "Klasörler",
     "Folders": "Klasörler",
     "Genres": "Türler",
     "Genres": "Türler",
     "HeaderAlbumArtists": "Albüm Sanatçıları",
     "HeaderAlbumArtists": "Albüm Sanatçıları",
@@ -117,5 +117,6 @@
     "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
     "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
     "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.",
     "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.",
     "Undefined": "Bilinmeyen",
     "Undefined": "Bilinmeyen",
-    "Default": "Varsayılan"
+    "Default": "Varsayılan",
+    "Forced": "Zorla"
 }
 }

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

@@ -3,7 +3,7 @@
     "Favorites": "Yêu Thích",
     "Favorites": "Yêu Thích",
     "Folders": "Thư Mục",
     "Folders": "Thư Mục",
     "Genres": "Thể Loại",
     "Genres": "Thể Loại",
-    "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+    "HeaderAlbumArtists": "Tuyển Tập Nghệ sĩ",
     "HeaderContinueWatching": "Xem Tiếp",
     "HeaderContinueWatching": "Xem Tiếp",
     "HeaderLiveTV": "TV Trực Tiếp",
     "HeaderLiveTV": "TV Trực Tiếp",
     "Movies": "Phim",
     "Movies": "Phim",
@@ -13,7 +13,7 @@
     "Songs": "Các Bài Hát",
     "Songs": "Các Bài Hát",
     "Sync": "Đồng Bộ",
     "Sync": "Đồng Bộ",
     "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
     "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
-    "Albums": "Albums",
+    "Albums": "Tuyển Tập",
     "Artists": "Các Nghệ Sĩ",
     "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 dữ liệu mô tả.",
     "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
     "TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
     "TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",

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

@@ -113,5 +113,9 @@
     "TaskCleanCache": "清理緩存目錄",
     "TaskCleanCache": "清理緩存目錄",
     "TasksChannelsCategory": "互聯網頻道",
     "TasksChannelsCategory": "互聯網頻道",
     "TasksLibraryCategory": "庫",
     "TasksLibraryCategory": "庫",
-    "TaskRefreshPeople": "刷新人物"
+    "TaskRefreshPeople": "刷新人物",
+    "TaskCleanActivityLog": "清理活動記錄",
+    "Undefined": "未定義",
+    "Forced": "強制",
+    "Default": "預設"
 }
 }

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

@@ -1,6 +1,6 @@
 {
 {
     "Albums": "專輯",
     "Albums": "專輯",
-    "AppDeviceValues": "軟體:{0},裝置:{1}",
+    "AppDeviceValues": "App:{0},裝置:{1}",
     "Application": "應用程式",
     "Application": "應用程式",
     "Artists": "演出者",
     "Artists": "演出者",
     "AuthenticationSucceededWithUserName": "{0} 成功授權",
     "AuthenticationSucceededWithUserName": "{0} 成功授權",

+ 2 - 0
Emby.Server.Implementations/Localization/iso6392.txt

@@ -77,6 +77,8 @@ chb|||Chibcha|chibcha
 che||ce|Chechen|tchétchène
 che||ce|Chechen|tchétchène
 chg|||Chagatai|djaghataï
 chg|||Chagatai|djaghataï
 chi|zho|zh|Chinese|chinois
 chi|zho|zh|Chinese|chinois
+chi|zho|zh-tw|Chinese; Traditional|chinois
+chi|zho|zh-hk|Chinese; Hong Kong|chinois
 chk|||Chuukese|chuuk
 chk|||Chuukese|chuuk
 chm|||Mari|mari
 chm|||Mari|mari
 chn|||Chinook jargon|chinook, jargon
 chn|||Chinook jargon|chinook, jargon

+ 1 - 1
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -166,7 +166,7 @@ namespace Emby.Server.Implementations.MediaEncoder
                         }
                         }
                         catch (Exception ex)
                         catch (Exception ex)
                         {
                         {
-                            _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(",", video.Path));
+                            _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
                             success = false;
                             success = false;
                             break;
                             break;
                         }
                         }

+ 41 - 23
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -112,8 +112,6 @@ namespace Emby.Server.Implementations.Plugins
                     {
                     {
                         assembly = Assembly.LoadFrom(file);
                         assembly = Assembly.LoadFrom(file);
 
 
-                        // This force loads all reference dll's that the plugin uses in the try..catch block.
-                        // Removing this will cause JF to bomb out if referenced dll's cause issues.
                         assembly.GetExportedTypes();
                         assembly.GetExportedTypes();
                     }
                     }
                     catch (FileLoadException ex)
                     catch (FileLoadException ex)
@@ -122,6 +120,20 @@ namespace Emby.Server.Implementations.Plugins
                         ChangePluginState(plugin, PluginStatus.Malfunctioned);
                         ChangePluginState(plugin, PluginStatus.Malfunctioned);
                         continue;
                         continue;
                     }
                     }
+                    catch (TypeLoadException ex) // Undocumented exception
+                    {
+                        _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
+                        ChangePluginState(plugin, PluginStatus.NotSupported);
+                        continue;
+                    }
+#pragma warning disable CA1031 // Do not catch general exception types
+                    catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+                    {
+                        _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
+                        ChangePluginState(plugin, PluginStatus.Malfunctioned);
+                        continue;
+                    }
 
 
                     _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
                     _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
                     yield return assembly;
                     yield return assembly;
@@ -336,7 +348,7 @@ namespace Emby.Server.Implementations.Plugins
             try
             try
             {
             {
                 var data = JsonSerializer.Serialize(manifest, _jsonOptions);
                 var data = JsonSerializer.Serialize(manifest, _jsonOptions);
-                File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
+                File.WriteAllText(Path.Combine(path, "meta.json"), data);
                 return true;
                 return true;
             }
             }
 #pragma warning disable CA1031 // Do not catch general exception types
 #pragma warning disable CA1031 // Do not catch general exception types
@@ -374,7 +386,7 @@ namespace Emby.Server.Implementations.Plugins
         private LocalPlugin? GetPluginByAssembly(Assembly assembly)
         private LocalPlugin? GetPluginByAssembly(Assembly assembly)
         {
         {
             // Find which plugin it is by the path.
             // Find which plugin it is by the path.
-            return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(assembly.Location), StringComparison.Ordinal));
+            return _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location, StringComparer.Ordinal));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -421,15 +433,17 @@ namespace Emby.Server.Implementations.Plugins
                 {
                 {
                     plugin.Instance = instance;
                     plugin.Instance = instance;
                     var manifest = plugin.Manifest;
                     var manifest = plugin.Manifest;
-                    var pluginStr = plugin.Instance.Version.ToString();
+                    var pluginStr = instance.Version.ToString();
                     bool changed = false;
                     bool changed = false;
-                    if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
+                    if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal)
+                        || manifest.Id != instance.Id)
                     {
                     {
                         // If a plugin without a manifest failed to load due to an external issue (eg config),
                         // If a plugin without a manifest failed to load due to an external issue (eg config),
                         // this updates the manifest to the actual plugin values.
                         // this updates the manifest to the actual plugin values.
                         manifest.Version = pluginStr;
                         manifest.Version = pluginStr;
                         manifest.Name = plugin.Instance.Name;
                         manifest.Name = plugin.Instance.Name;
                         manifest.Description = plugin.Instance.Description;
                         manifest.Description = plugin.Instance.Description;
+                        manifest.Id = plugin.Instance.Id;
                         changed = true;
                         changed = true;
                     }
                     }
 
 
@@ -505,39 +519,43 @@ namespace Emby.Server.Implementations.Plugins
             return _plugins.Remove(plugin);
             return _plugins.Remove(plugin);
         }
         }
 
 
-        private LocalPlugin LoadManifest(string dir)
+        internal LocalPlugin LoadManifest(string dir)
         {
         {
             Version? version;
             Version? version;
             PluginManifest? manifest = null;
             PluginManifest? manifest = null;
             var metafile = Path.Combine(dir, "meta.json");
             var metafile = Path.Combine(dir, "meta.json");
             if (File.Exists(metafile))
             if (File.Exists(metafile))
             {
             {
+                // Only path where this stays null is when File.ReadAllBytes throws an IOException
+                byte[] data = null!;
                 try
                 try
                 {
                 {
-                    var data = File.ReadAllText(metafile, Encoding.UTF8);
+                    data = File.ReadAllBytes(metafile);
                     manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions);
                     manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions);
                 }
                 }
-#pragma warning disable CA1031 // Do not catch general exception types
-                catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
+                catch (IOException ex)
                 {
                 {
-                    _logger.LogError(ex, "Error deserializing {Path}.", dir);
+                    _logger.LogError(ex, "Error reading file {Path}.", dir);
                 }
                 }
-            }
-
-            if (manifest != null)
-            {
-                if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+                catch (JsonException ex)
                 {
                 {
-                    targetAbi = _minimumVersion;
+                    _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!));
                 }
                 }
 
 
-                if (!Version.TryParse(manifest.Version, out version))
+                if (manifest != null)
                 {
                 {
-                    manifest.Version = _minimumVersion.ToString();
-                }
+                    if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+                    {
+                        targetAbi = _minimumVersion;
+                    }
 
 
-                return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
+                    if (!Version.TryParse(manifest.Version, out version))
+                    {
+                        manifest.Version = _minimumVersion.ToString();
+                    }
+
+                    return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
+                }
             }
             }
 
 
             // No metafile, so lets see if the folder is versioned.
             // No metafile, so lets see if the folder is versioned.
@@ -559,7 +577,7 @@ namespace Emby.Server.Implementations.Plugins
             // Auto-create a plugin manifest, so we can disable it, if it fails to load.
             // Auto-create a plugin manifest, so we can disable it, if it fails to load.
             manifest = new PluginManifest
             manifest = new PluginManifest
             {
             {
-                Status = PluginStatus.Restart,
+                Status = PluginStatus.Active,
                 Name = metafile,
                 Name = metafile,
                 AutoUpdate = false,
                 AutoUpdate = false,
                 Id = metafile.GetMD5(),
                 Id = metafile.GetMD5(),

+ 13 - 14
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -143,21 +143,21 @@ namespace Emby.Server.Implementations.ScheduledTasks
                     {
                     {
                         if (File.Exists(path))
                         if (File.Exists(path))
                         {
                         {
-                            try
+                            var bytes = File.ReadAllBytes(path);
+                            if (bytes.Length > 0)
                             {
                             {
-                                var jsonString = File.ReadAllText(path, Encoding.UTF8);
-                                if (!string.IsNullOrWhiteSpace(jsonString))
+                                try
                                 {
                                 {
-                                    _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(jsonString, _jsonOptions);
+                                    _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(bytes, _jsonOptions);
                                 }
                                 }
-                                else
+                                catch (JsonException ex)
                                 {
                                 {
-                                    _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path);
+                                    _logger.LogError(ex, "Error deserializing {File}", path);
                                 }
                                 }
                             }
                             }
-                            catch (Exception ex)
+                            else
                             {
                             {
-                                _logger.LogError(ex, "Error deserializing {File}", path);
+                                _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path);
                             }
                             }
                         }
                         }
 
 
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
 
                 lock (_lastExecutionResultSyncLock)
                 lock (_lastExecutionResultSyncLock)
                 {
                 {
-                    using FileStream createStream = File.OpenWrite(path);
+                    using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
                     JsonSerializer.SerializeAsync(createStream, value, _jsonOptions);
                     JsonSerializer.SerializeAsync(createStream, value, _jsonOptions);
                 }
                 }
             }
             }
@@ -541,8 +541,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
             TaskTriggerInfo[] list = null;
             TaskTriggerInfo[] list = null;
             if (File.Exists(path))
             if (File.Exists(path))
             {
             {
-                var jsonString = File.ReadAllText(path, Encoding.UTF8);
-                list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(jsonString, _jsonOptions);
+                var bytes = File.ReadAllBytes(path);
+                list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(bytes, _jsonOptions);
             }
             }
 
 
             // Return defaults if file doesn't exist.
             // Return defaults if file doesn't exist.
@@ -577,9 +577,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
             var path = GetConfigurationFilePath();
             var path = GetConfigurationFilePath();
 
 
             Directory.CreateDirectory(Path.GetDirectoryName(path));
             Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            var json = JsonSerializer.Serialize(triggers, _jsonOptions);
-            File.WriteAllText(path, json, Encoding.UTF8);
+            using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+            JsonSerializer.SerializeAsync(createStream, triggers, _jsonOptions);
         }
         }
 
 
         /// <summary>
         /// <summary>

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

@@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
 
                         Directory.CreateDirectory(parentPath);
                         Directory.CreateDirectory(parentPath);
 
 
-                        string text = string.Join("|", previouslyFailedImages);
+                        string text = string.Join('|', previouslyFailedImages);
                         File.WriteAllText(failHistoryPath, text);
                         File.WriteAllText(failHistoryPath, text);
                     }
                     }
 
 

+ 5 - 4
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs

@@ -80,10 +80,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             // Delete log files more than n days old
             // Delete log files more than n days old
             var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
             var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
 
 
-            // Only delete the .txt log files, the *.log files created by serilog get managed by itself
-            var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
-                          .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
-                          .ToList();
+            // Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
+            var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
+                .Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
+                            && _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+                .ToList();
 
 
             var index = 0;
             var index = 0;
 
 

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

@@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
 
             var dueTime = triggerDate - now;
             var dueTime = triggerDate - now;
 
 
-            logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:g}, which is {DueTime:g} from now.", taskName, triggerDate, dueTime);
+            logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
 
 
             Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
             Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
         }
         }

+ 6 - 1
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1456,7 +1456,12 @@ namespace Emby.Server.Implementations.Session
                 throw new SecurityException("Unknown quick connect token");
                 throw new SecurityException("Unknown quick connect token");
             }
             }
 
 
-            request.UserId = result.Items[0].UserId;
+            var info = result.Items[0];
+            request.UserId = info.UserId;
+
+            // There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
+            _authRepo.Delete(info);
+
             return AuthenticateNewSessionInternal(request, false);
             return AuthenticateNewSessionInternal(request, false);
         }
         }
 
 

+ 22 - 1
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -143,10 +143,31 @@ namespace Emby.Server.Implementations.TV
             var allNextUp = seriesKeys
             var allNextUp = seriesKeys
                 .Select(i => GetNextUp(i, currentUser, dtoOptions));
                 .Select(i => GetNextUp(i, currentUser, dtoOptions));
 
 
+            // If viewing all next up for all series, remove first episodes
+            // But if that returns empty, keep those first episodes (avoid completely empty view)
+            var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
+            var anyFound = false;
+
             return allNextUp
             return allNextUp
                 .Where(i =>
                 .Where(i =>
                 {
                 {
-                    return i.Item1 != DateTime.MinValue;
+                    if (request.DisableFirstEpisode)
+                    {
+                        return i.Item1 != DateTime.MinValue;
+                    }
+
+                    if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
+                    {
+                        anyFound = true;
+                        return true;
+                    }
+
+                    if (!anyFound && i.Item1 == DateTime.MinValue)
+                    {
+                        return true;
+                    }
+
+                    return false;
                 })
                 })
                 .Select(i => i.Item2())
                 .Select(i => i.Item2())
                 .Where(i => i != null);
                 .Where(i => i != null);

+ 28 - 0
Jellyfin.Api/Attributes/AcceptsFileAttribute.cs

@@ -0,0 +1,28 @@
+using System;
+
+namespace Jellyfin.Api.Attributes
+{
+    /// <summary>
+    /// Internal produces image attribute.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Method)]
+    public class AcceptsFileAttribute : Attribute
+    {
+        private readonly string[] _contentTypes;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
+        /// </summary>
+        /// <param name="contentTypes">Content types this endpoint produces.</param>
+        public AcceptsFileAttribute(params string[] contentTypes)
+        {
+            _contentTypes = contentTypes;
+        }
+
+        /// <summary>
+        /// Gets the configured content types.
+        /// </summary>
+        /// <returns>the configured content types.</returns>
+        public string[] GetContentTypes() => _contentTypes;
+    }
+}

+ 18 - 0
Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs

@@ -0,0 +1,18 @@
+namespace Jellyfin.Api.Attributes
+{
+    /// <summary>
+    /// Produces file attribute of "image/*".
+    /// </summary>
+    public class AcceptsImageFileAttribute : AcceptsFileAttribute
+    {
+        private const string ContentType = "image/*";
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
+        /// </summary>
+        public AcceptsImageFileAttribute()
+            : base(ContentType)
+        {
+        }
+    }
+}

+ 12 - 0
Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs

@@ -0,0 +1,12 @@
+using System;
+
+namespace Jellyfin.Api.Attributes
+{
+    /// <summary>
+    /// Attribute to mark a parameter as obsolete.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Parameter)]
+    public class ParameterObsoleteAttribute : Attribute
+    {
+    }
+}

+ 10 - 8
Jellyfin.Api/Controllers/ArtistsController.cs

@@ -3,8 +3,10 @@ using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -88,8 +90,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
@@ -127,8 +129,8 @@ namespace Jellyfin.Api.Controllers
 
 
             var query = new InternalItemsQuery(user)
             var query = new InternalItemsQuery(user)
             {
             {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
+                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
                 MediaTypes = mediaTypes,
                 MediaTypes = mediaTypes,
                 StartIndex = startIndex,
                 StartIndex = startIndex,
                 Limit = limit,
                 Limit = limit,
@@ -287,8 +289,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
@@ -326,8 +328,8 @@ namespace Jellyfin.Api.Controllers
 
 
             var query = new InternalItemsQuery(user)
             var query = new InternalItemsQuery(user)
             {
             {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
+                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
                 MediaTypes = mediaTypes,
                 MediaTypes = mediaTypes,
                 StartIndex = startIndex,
                 StartIndex = startIndex,
                 Limit = limit,
                 Limit = limit,

+ 3 - 4
Jellyfin.Api/Controllers/ChannelsController.cs

@@ -1,13 +1,12 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
-using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
-using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -121,9 +120,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? sortOrder,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
-            [FromQuery] string? sortBy,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)

+ 0 - 1
Jellyfin.Api/Controllers/CollectionController.cs

@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 8 - 56
Jellyfin.Api/Controllers/DashboardController.cs

@@ -7,17 +7,11 @@ using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Models;
 using Jellyfin.Api.Models;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Plugins;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
 
 
 namespace Jellyfin.Api.Controllers
 namespace Jellyfin.Api.Controllers
 {
 {
@@ -51,7 +45,6 @@ namespace Jellyfin.Api.Controllers
         /// Gets the configuration pages.
         /// Gets the configuration pages.
         /// </summary>
         /// </summary>
         /// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
         /// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
-        /// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param>
         /// <response code="200">ConfigurationPages returned.</response>
         /// <response code="200">ConfigurationPages returned.</response>
         /// <response code="404">Server still loading.</response>
         /// <response code="404">Server still loading.</response>
         /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
         /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
@@ -59,40 +52,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
         public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
-            [FromQuery] bool? enableInMainMenu,
-            [FromQuery] ConfigurationPageType? pageType)
+            [FromQuery] bool? enableInMainMenu)
         {
         {
-            const string unavailableMessage = "The server is still loading. Please try again momentarily.";
-
-            var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList();
-
-            if (pages == null)
-            {
-                return NotFound(unavailableMessage);
-            }
-
-            // Don't allow a failing plugin to fail them all
-            var configPages = pages.Select(p =>
-                {
-                    try
-                    {
-                        return new ConfigurationPageInfo(p);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name);
-                        return null;
-                    }
-                })
-                .Where(i => i != null)
-                .ToList();
-
-            configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
-
-            if (pageType.HasValue)
-            {
-                configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList();
-            }
+            var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
 
 
             if (enableInMainMenu.HasValue)
             if (enableInMainMenu.HasValue)
             {
             {
@@ -121,24 +83,14 @@ namespace Jellyfin.Api.Controllers
             var isJs = false;
             var isJs = false;
             var isTemplate = false;
             var isTemplate = false;
 
 
-            var page = _appHost.GetExports<IPluginConfigurationPage>().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
-            if (page != null)
-            {
-                plugin = page.Plugin;
-                stream = page.GetHtmlStream();
-            }
-
-            if (plugin == null)
+            var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
+            if (altPage != null)
             {
             {
-                var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
-                if (altPage != null)
-                {
-                    plugin = altPage.Item2;
-                    stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
+                plugin = altPage.Item2;
+                stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
 
 
-                    isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
-                    isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
-                }
+                isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
+                isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
             }
             }
 
 
             if (plugin != null && stream != null)
             if (plugin != null && stream != null)

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

@@ -15,7 +15,6 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;

+ 18 - 19
Jellyfin.Api/Controllers/FilterController.cs

@@ -1,13 +1,12 @@
 using System;
 using System;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
@@ -51,7 +50,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
         public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -60,10 +59,10 @@ namespace Jellyfin.Api.Controllers
 
 
             BaseItem? item = null;
             BaseItem? item = null;
             if (includeItemTypes.Length != 1
             if (includeItemTypes.Length != 1
-                || !(string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
-                     || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
-                     || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
-                     || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
+                || !(includeItemTypes[0] == BaseItemKind.BoxSet
+                     || includeItemTypes[0] == BaseItemKind.Playlist
+                     || includeItemTypes[0] == BaseItemKind.Trailer
+                     || includeItemTypes[0] == BaseItemKind.Program))
             {
             {
                 item = _libraryManager.GetParentItem(parentId, user?.Id);
                 item = _libraryManager.GetParentItem(parentId, user?.Id);
             }
             }
@@ -72,7 +71,7 @@ namespace Jellyfin.Api.Controllers
             {
             {
                 User = user,
                 User = user,
                 MediaTypes = mediaTypes,
                 MediaTypes = mediaTypes,
-                IncludeItemTypes = includeItemTypes,
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
                 Recursive = true,
                 Recursive = true,
                 EnableTotalRecordCount = false,
                 EnableTotalRecordCount = false,
                 DtoOptions = new DtoOptions
                 DtoOptions = new DtoOptions
@@ -137,7 +136,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryFilters> GetQueryFilters(
         public ActionResult<QueryFilters> GetQueryFilters(
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery] bool? isAiring,
             [FromQuery] bool? isAiring,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isSports,
             [FromQuery] bool? isSports,
@@ -152,10 +151,10 @@ namespace Jellyfin.Api.Controllers
 
 
             BaseItem? parentItem = null;
             BaseItem? parentItem = null;
             if (includeItemTypes.Length == 1
             if (includeItemTypes.Length == 1
-                && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
+                && (includeItemTypes[0] == BaseItemKind.BoxSet
+                    || includeItemTypes[0] == BaseItemKind.Playlist
+                    || includeItemTypes[0] == BaseItemKind.Trailer
+                    || includeItemTypes[0] == BaseItemKind.Program))
             {
             {
                 parentItem = null;
                 parentItem = null;
             }
             }
@@ -167,7 +166,7 @@ namespace Jellyfin.Api.Controllers
             var filters = new QueryFilters();
             var filters = new QueryFilters();
             var genreQuery = new InternalItemsQuery(user)
             var genreQuery = new InternalItemsQuery(user)
             {
             {
-                IncludeItemTypes = includeItemTypes,
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
                 DtoOptions = new DtoOptions
                 DtoOptions = new DtoOptions
                 {
                 {
                     Fields = Array.Empty<ItemFields>(),
                     Fields = Array.Empty<ItemFields>(),
@@ -192,10 +191,10 @@ namespace Jellyfin.Api.Controllers
             }
             }
 
 
             if (includeItemTypes.Length == 1
             if (includeItemTypes.Length == 1
-                && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase)))
+                && (includeItemTypes[0] == BaseItemKind.MusicAlbum
+                    || includeItemTypes[0] == BaseItemKind.MusicVideo
+                    || includeItemTypes[0] == BaseItemKind.MusicArtist
+                    || includeItemTypes[0] == BaseItemKind.Audio))
             {
             {
                 filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
                 filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
                 {
                 {

+ 5 - 4
Jellyfin.Api/Controllers/GenresController.cs

@@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -74,8 +75,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -96,8 +97,8 @@ namespace Jellyfin.Api.Controllers
 
 
             var query = new InternalItemsQuery(user)
             var query = new InternalItemsQuery(user)
             {
             {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
+                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
                 StartIndex = startIndex,
                 StartIndex = startIndex,
                 Limit = limit,
                 Limit = limit,
                 IsFavorite = isFavorite,
                 IsFavorite = isFavorite,

+ 0 - 2
Jellyfin.Api/Controllers/HlsSegmentController.cs

@@ -2,13 +2,11 @@ using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.IO;
-using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;

+ 4 - 0
Jellyfin.Api/Controllers/ImageController.cs

@@ -87,6 +87,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Users/{userId}/Images/{imageType}")]
         [HttpPost("Users/{userId}/Images/{imageType}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [AcceptsImageFile]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
         [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
+        [AcceptsImageFile]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -312,6 +314,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpPost("Items/{itemId}/Images/{imageType}")]
         [HttpPost("Items/{itemId}/Images/{imageType}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [AcceptsImageFile]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -346,6 +349,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
         [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [Authorize(Policy = Policies.RequiresElevation)]
+        [AcceptsImageFile]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]

+ 0 - 1
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -1,7 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
-using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;

+ 0 - 2
Jellyfin.Api/Controllers/ItemLookupController.cs

@@ -2,8 +2,6 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.IO;
-using System.Linq;
-using System.Net.Mime;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;

+ 17 - 18
Jellyfin.Api/Controllers/ItemsController.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
-using System.Globalization;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
@@ -175,16 +174,16 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] bool? recursive,
             [FromQuery] bool? recursive,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
-            [FromQuery] string? sortOrder,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
-            [FromQuery] string? sortBy,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? isPlayed,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -233,8 +232,8 @@ namespace Jellyfin.Api.Controllers
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 
             if (includeItemTypes.Length == 1
             if (includeItemTypes.Length == 1
-                && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
-                    || includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
+                && (includeItemTypes[0] == BaseItemKind.Playlist
+                    || includeItemTypes[0] == BaseItemKind.BoxSet))
             {
             {
                 parentId = null;
                 parentId = null;
             }
             }
@@ -251,7 +250,7 @@ namespace Jellyfin.Api.Controllers
                 && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
                 && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
             {
             {
                 recursive = true;
                 recursive = true;
-                includeItemTypes = new[] { "Playlist" };
+                includeItemTypes = new[] { BaseItemKind.Playlist };
             }
             }
 
 
             var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
             var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
@@ -286,8 +285,8 @@ namespace Jellyfin.Api.Controllers
                 {
                 {
                     IsPlayed = isPlayed,
                     IsPlayed = isPlayed,
                     MediaTypes = mediaTypes,
                     MediaTypes = mediaTypes,
-                    IncludeItemTypes = includeItemTypes,
-                    ExcludeItemTypes = excludeItemTypes,
+                    IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                    ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
                     Recursive = recursive ?? false,
                     Recursive = recursive ?? false,
                     OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
                     OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
                     IsFavorite = isFavorite,
                     IsFavorite = isFavorite,
@@ -608,16 +607,16 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] bool? recursive,
             [FromQuery] bool? recursive,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
-            [FromQuery] string? sortOrder,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
-            [FromQuery] string? sortBy,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? isPlayed,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -773,8 +772,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery] bool enableTotalRecordCount = true,
             [FromQuery] bool enableTotalRecordCount = true,
             [FromQuery] bool? enableImages = true)
             [FromQuery] bool? enableImages = true)
         {
         {
@@ -810,8 +809,8 @@ namespace Jellyfin.Api.Controllers
                 CollapseBoxSetItems = false,
                 CollapseBoxSetItems = false,
                 EnableTotalRecordCount = enableTotalRecordCount,
                 EnableTotalRecordCount = enableTotalRecordCount,
                 AncestorIds = ancestorIds,
                 AncestorIds = ancestorIds,
-                IncludeItemTypes = includeItemTypes,
-                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
                 SearchTerm = searchTerm
                 SearchTerm = searchTerm
             });
             });
 
 

+ 0 - 1
Jellyfin.Api/Controllers/LibraryController.cs

@@ -11,7 +11,6 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LibraryDtos;
 using Jellyfin.Api.Models.LibraryDtos;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;

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

@@ -553,8 +553,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isSports,
             [FromQuery] bool? isSports,
             [FromQuery] int? startIndex,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? sortBy,
-            [FromQuery] string? sortOrder,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
             [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,

+ 15 - 14
Jellyfin.Api/Controllers/MediaInfoController.cs

@@ -83,6 +83,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
         /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+        /// Query parameters are obsolete.
         /// </remarks>
         /// </remarks>
         /// <param name="itemId">The item id.</param>
         /// <param name="itemId">The item id.</param>
         /// <param name="userId">The user id.</param>
         /// <param name="userId">The user id.</param>
@@ -106,20 +107,20 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
         public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] Guid itemId,
-            [FromQuery] Guid? userId,
-            [FromQuery] int? maxStreamingBitrate,
-            [FromQuery] long? startTimeTicks,
-            [FromQuery] int? audioStreamIndex,
-            [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] int? maxAudioChannels,
-            [FromQuery] string? mediaSourceId,
-            [FromQuery] string? liveStreamId,
-            [FromQuery] bool? autoOpenLiveStream,
-            [FromQuery] bool? enableDirectPlay,
-            [FromQuery] bool? enableDirectStream,
-            [FromQuery] bool? enableTranscoding,
-            [FromQuery] bool? allowVideoStreamCopy,
-            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery, ParameterObsolete] Guid? userId,
+            [FromQuery, ParameterObsolete] int? maxStreamingBitrate,
+            [FromQuery, ParameterObsolete] long? startTimeTicks,
+            [FromQuery, ParameterObsolete] int? audioStreamIndex,
+            [FromQuery, ParameterObsolete] int? subtitleStreamIndex,
+            [FromQuery, ParameterObsolete] int? maxAudioChannels,
+            [FromQuery, ParameterObsolete] string? mediaSourceId,
+            [FromQuery, ParameterObsolete] string? liveStreamId,
+            [FromQuery, ParameterObsolete] bool? autoOpenLiveStream,
+            [FromQuery, ParameterObsolete] bool? enableDirectPlay,
+            [FromQuery, ParameterObsolete] bool? enableDirectStream,
+            [FromQuery, ParameterObsolete] bool? enableTranscoding,
+            [FromQuery, ParameterObsolete] bool? allowVideoStreamCopy,
+            [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
             [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
             [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
         {
         {
             var authInfo = _authContext.GetAuthorizationInfo(Request);
             var authInfo = _authContext.GetAuthorizationInfo(Request);

+ 5 - 4
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -74,8 +75,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -96,8 +97,8 @@ namespace Jellyfin.Api.Controllers
 
 
             var query = new InternalItemsQuery(user)
             var query = new InternalItemsQuery(user)
             {
             {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
+                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
                 StartIndex = startIndex,
                 StartIndex = startIndex,
                 Limit = limit,
                 Limit = limit,
                 IsFavorite = isFavorite,
                 IsFavorite = isFavorite,

+ 1 - 2
Jellyfin.Api/Controllers/PackageController.cs

@@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Updates;
 using MediaBrowser.Model.Updates;
@@ -158,7 +157,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Repositories")]
         [HttpPost("Repositories")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
+        public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
         {
         {
             _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
             _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
             _serverConfigurationManager.SaveConfiguration();
             _serverConfigurationManager.SaveConfiguration();

+ 0 - 1
Jellyfin.Api/Controllers/PersonsController.cs

@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 6 - 5
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -3,9 +3,9 @@ using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.PlaylistDtos;
 using Jellyfin.Api.Models.PlaylistDtos;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
@@ -57,6 +57,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
         /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+        /// Query parameters are obsolete.
         /// </remarks>
         /// </remarks>
         /// <param name="name">The playlist name.</param>
         /// <param name="name">The playlist name.</param>
         /// <param name="ids">The item ids.</param>
         /// <param name="ids">The item ids.</param>
@@ -70,10 +71,10 @@ namespace Jellyfin.Api.Controllers
         [HttpPost]
         [HttpPost]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
         public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
-            [FromQuery] string? name,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids,
-            [FromQuery] Guid? userId,
-            [FromQuery] string? mediaType,
+            [FromQuery, ParameterObsolete] string? name,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
+            [FromQuery, ParameterObsolete] Guid? userId,
+            [FromQuery, ParameterObsolete] string? mediaType,
             [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
             [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
         {
         {
             if (ids.Count == 0)
             if (ids.Count == 0)

+ 1 - 5
Jellyfin.Api/Controllers/PluginsController.cs

@@ -1,10 +1,8 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
-using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using System.Net.Mime;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
@@ -300,9 +298,7 @@ namespace Jellyfin.Api.Controllers
             }
             }
 
 
             var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
             var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
-            if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
-                || plugin.Manifest.ImagePath == null
-                || !System.IO.File.Exists(imagePath))
+            if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath))
             {
             {
                 return NotFound();
                 return NotFound();
             }
             }

+ 5 - 4
Jellyfin.Api/Controllers/SearchController.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -83,8 +84,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery, Required] string searchTerm,
             [FromQuery, Required] string searchTerm,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isMovie,
@@ -109,8 +110,8 @@ namespace Jellyfin.Api.Controllers
                 IncludeStudios = includeStudios,
                 IncludeStudios = includeStudios,
                 StartIndex = startIndex,
                 StartIndex = startIndex,
                 UserId = userId ?? Guid.Empty,
                 UserId = userId ?? Guid.Empty,
-                IncludeItemTypes = includeItemTypes,
-                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
                 MediaTypes = mediaTypes,
                 MediaTypes = mediaTypes,
                 ParentId = parentId,
                 ParentId = parentId,
 
 

+ 5 - 4
Jellyfin.Api/Controllers/StudiosController.cs

@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -73,8 +74,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
@@ -96,8 +97,8 @@ namespace Jellyfin.Api.Controllers
 
 
             var query = new InternalItemsQuery(user)
             var query = new InternalItemsQuery(user)
             {
             {
-                ExcludeItemTypes = excludeItemTypes,
-                IncludeItemTypes = includeItemTypes,
+                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
                 StartIndex = startIndex,
                 StartIndex = startIndex,
                 Limit = limit,
                 Limit = limit,
                 IsFavorite = isFavorite,
                 IsFavorite = isFavorite,

部分文件因为文件数量过多而无法显示