瀏覽代碼

Merge remote-tracking branch 'upstream/master' into query-fields

crobibero 4 年之前
父節點
當前提交
4f7c13ecf4
共有 100 個文件被更改,包括 427 次插入424 次删除
  1. 24 8
      .ci/azure-pipelines-api-client.yml
  2. 3 0
      .npmrc
  3. 16 2
      Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
  4. 10 3
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
  5. 66 56
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
  6. 41 7
      Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
  7. 5 11
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  8. 25 19
      Emby.Dlna/Service/BaseControlHandler.cs
  9. 3 3
      Emby.Drawing/ImageProcessor.cs
  10. 1 1
      Emby.Drawing/NullImageEncoder.cs
  11. 4 13
      Emby.Naming/AudioBook/AudioBookResolver.cs
  12. 5 40
      Emby.Server.Implementations/ApplicationHost.cs
  13. 2 1
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  14. 23 12
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  15. 6 3
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  16. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  17. 5 12
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  18. 12 1
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  19. 17 34
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  20. 4 2
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  21. 33 34
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  22. 13 1
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  23. 2 1
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  24. 1 1
      Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
  25. 0 1
      Emby.Server.Implementations/Localization/Core/af.json
  26. 0 1
      Emby.Server.Implementations/Localization/Core/ar.json
  27. 0 1
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  28. 0 1
      Emby.Server.Implementations/Localization/Core/bn.json
  29. 0 1
      Emby.Server.Implementations/Localization/Core/ca.json
  30. 0 1
      Emby.Server.Implementations/Localization/Core/cs.json
  31. 0 1
      Emby.Server.Implementations/Localization/Core/da.json
  32. 0 1
      Emby.Server.Implementations/Localization/Core/de.json
  33. 0 1
      Emby.Server.Implementations/Localization/Core/el.json
  34. 0 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  35. 0 1
      Emby.Server.Implementations/Localization/Core/en-US.json
  36. 0 1
      Emby.Server.Implementations/Localization/Core/es-AR.json
  37. 0 1
      Emby.Server.Implementations/Localization/Core/es-MX.json
  38. 0 1
      Emby.Server.Implementations/Localization/Core/es.json
  39. 0 1
      Emby.Server.Implementations/Localization/Core/es_419.json
  40. 0 1
      Emby.Server.Implementations/Localization/Core/es_DO.json
  41. 0 1
      Emby.Server.Implementations/Localization/Core/fa.json
  42. 20 21
      Emby.Server.Implementations/Localization/Core/fi.json
  43. 0 1
      Emby.Server.Implementations/Localization/Core/fil.json
  44. 0 1
      Emby.Server.Implementations/Localization/Core/fr-CA.json
  45. 0 1
      Emby.Server.Implementations/Localization/Core/fr.json
  46. 0 1
      Emby.Server.Implementations/Localization/Core/gsw.json
  47. 0 1
      Emby.Server.Implementations/Localization/Core/he.json
  48. 3 0
      Emby.Server.Implementations/Localization/Core/hi.json
  49. 0 1
      Emby.Server.Implementations/Localization/Core/hr.json
  50. 0 1
      Emby.Server.Implementations/Localization/Core/hu.json
  51. 0 1
      Emby.Server.Implementations/Localization/Core/id.json
  52. 0 1
      Emby.Server.Implementations/Localization/Core/is.json
  53. 0 1
      Emby.Server.Implementations/Localization/Core/it.json
  54. 0 1
      Emby.Server.Implementations/Localization/Core/ja.json
  55. 0 1
      Emby.Server.Implementations/Localization/Core/kk.json
  56. 0 1
      Emby.Server.Implementations/Localization/Core/ko.json
  57. 0 1
      Emby.Server.Implementations/Localization/Core/lt-LT.json
  58. 0 1
      Emby.Server.Implementations/Localization/Core/lv.json
  59. 0 1
      Emby.Server.Implementations/Localization/Core/mk.json
  60. 0 1
      Emby.Server.Implementations/Localization/Core/mr.json
  61. 0 1
      Emby.Server.Implementations/Localization/Core/ms.json
  62. 0 1
      Emby.Server.Implementations/Localization/Core/nb.json
  63. 0 1
      Emby.Server.Implementations/Localization/Core/ne.json
  64. 0 1
      Emby.Server.Implementations/Localization/Core/nl.json
  65. 0 1
      Emby.Server.Implementations/Localization/Core/nn.json
  66. 0 1
      Emby.Server.Implementations/Localization/Core/pl.json
  67. 0 1
      Emby.Server.Implementations/Localization/Core/pt-BR.json
  68. 1 2
      Emby.Server.Implementations/Localization/Core/pt-PT.json
  69. 0 1
      Emby.Server.Implementations/Localization/Core/pt.json
  70. 0 1
      Emby.Server.Implementations/Localization/Core/ro.json
  71. 0 1
      Emby.Server.Implementations/Localization/Core/ru.json
  72. 0 1
      Emby.Server.Implementations/Localization/Core/sk.json
  73. 0 1
      Emby.Server.Implementations/Localization/Core/sl-SI.json
  74. 1 2
      Emby.Server.Implementations/Localization/Core/sq.json
  75. 0 1
      Emby.Server.Implementations/Localization/Core/sr.json
  76. 1 2
      Emby.Server.Implementations/Localization/Core/sv.json
  77. 0 1
      Emby.Server.Implementations/Localization/Core/ta.json
  78. 0 1
      Emby.Server.Implementations/Localization/Core/th.json
  79. 0 1
      Emby.Server.Implementations/Localization/Core/tr.json
  80. 0 1
      Emby.Server.Implementations/Localization/Core/uk.json
  81. 0 1
      Emby.Server.Implementations/Localization/Core/ur_PK.json
  82. 0 1
      Emby.Server.Implementations/Localization/Core/vi.json
  83. 0 1
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  84. 0 1
      Emby.Server.Implementations/Localization/Core/zh-HK.json
  85. 0 1
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  86. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  87. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
  88. 12 6
      Emby.Server.Implementations/Security/AuthenticationRepository.cs
  89. 1 1
      Emby.Server.Implementations/Session/SessionManager.cs
  90. 10 10
      Jellyfin.Api/Controllers/ActivityLogController.cs
  91. 6 6
      Jellyfin.Api/Controllers/ArtistsController.cs
  92. 5 5
      Jellyfin.Api/Controllers/ChannelsController.cs
  93. 3 3
      Jellyfin.Api/Controllers/GenresController.cs
  94. 19 19
      Jellyfin.Api/Controllers/ImageController.cs
  95. 5 8
      Jellyfin.Api/Controllers/ItemsController.cs
  96. 3 3
      Jellyfin.Api/Controllers/MusicGenresController.cs
  97. 2 2
      Jellyfin.Api/Controllers/PersonsController.cs
  98. 2 2
      Jellyfin.Api/Controllers/ScheduledTasksController.cs
  99. 3 3
      Jellyfin.Api/Controllers/StudiosController.cs
  100. 3 2
      Jellyfin.Api/Controllers/TrailersController.cs

+ 24 - 8
.ci/azure-pipelines-api-client.yml

@@ -28,7 +28,13 @@ jobs:
       inputs:
         script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
 
-# Generate npm api client
+## Authenticate with npm registry
+    - task: npmAuthenticate@0
+      inputs:
+        workingFile: ./.npmrc
+        customEndpoint: 'jellyfin-bot for NPM'
+
+## Generate npm api client
 # Unstable
     - task: CmdLine@2
       displayName: 'Build unstable typescript axios client'
@@ -36,22 +42,32 @@ jobs:
       inputs:
         script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
 
+# Stable
+    - task: CmdLine@2
+      displayName: 'Build stable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+      inputs:
+        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
+
+## Run npm install
+    - task: Npm@1
+      displayName: 'Install npm dependencies'
+      inputs:
+        command: install
+        workingDir: ./apiclient/generated/typescript/axios
+
+## Publish npm packages
+# Unstable
     - task: Npm@1
       displayName: 'Publish unstable typescript axios client'
       condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
       inputs:
         command: publish
         publishRegistry: useFeed
-        publishFeed: 'unstable@Local'
+        publishFeed: 'jellyfin/unstable'
         workingDir: ./apiclient/generated/typescript/axios
 
 # Stable
-    - task: CmdLine@2
-      displayName: 'Build stable typescript axios client'
-      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-      inputs:
-        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
-
     - task: Npm@1
       displayName: 'Publish stable typescript axios client'
       condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')

+ 3 - 0
.npmrc

@@ -0,0 +1,3 @@
+registry=https://registry.npmjs.org/
+@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
+always-auth=true

+ 16 - 2
Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Xml;
@@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
+    /// <summary>
+    /// Defines the <see cref="ControlHandler" />.
+    /// </summary>
     public class ControlHandler : BaseControlHandler
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ControlHandler"/> class.
+        /// </summary>
+        /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
+        /// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
         public ControlHandler(IServerConfigurationManager config, ILogger logger)
             : base(config, logger)
         {
@@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
         }
 
+        /// <summary>
+        /// Records that the handle is authorized in the xml stream.
+        /// </summary>
+        /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
         private static void HandleIsAuthorized(XmlWriter xmlWriter)
             => xmlWriter.WriteElementString("Result", "1");
 
+        /// <summary>
+        /// Records that the handle is validated in the xml stream.
+        /// </summary>
+        /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
         private static void HandleIsValidated(XmlWriter xmlWriter)
             => xmlWriter.WriteElementString("Result", "1");
     }

+ 10 - 3
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System.Net.Http;
 using System.Threading.Tasks;
 using Emby.Dlna.Service;
@@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
+    /// <summary>
+    /// Defines the <see cref="MediaReceiverRegistrarService" />.
+    /// </summary>
     public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
     {
         private readonly IServerConfigurationManager _config;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
+        /// </summary>
+        /// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
+        /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
+        /// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
         public MediaReceiverRegistrarService(
             ILogger<MediaReceiverRegistrarService> logger,
             IHttpClientFactory httpClientFactory,
@@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
         /// <inheritdoc />
         public string GetServiceXml()
         {
-            return new MediaReceiverRegistrarXmlBuilder().GetXml();
+            return MediaReceiverRegistrarXmlBuilder.GetXml();
         }
 
         /// <inheritdoc />

+ 66 - 56
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs

@@ -1,79 +1,89 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 using Emby.Dlna.Service;
+using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
-    public class MediaReceiverRegistrarXmlBuilder
+    /// <summary>
+    /// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
+    /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
+    /// </summary>
+    public static class MediaReceiverRegistrarXmlBuilder
     {
-        public string GetXml()
+        /// <summary>
+        /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
+        /// </summary>
+        /// <returns>An XML representation of this service.</returns>
+        public static string GetXml()
         {
-            return new ServiceXmlBuilder().GetXml(
-                new ServiceActionListBuilder().GetActions(),
-                GetStateVariables());
+            return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
         }
 
+        /// <summary>
+        /// The a list of all the state variables for this invocation.
+        /// </summary>
+        /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
         private static IEnumerable<StateVariable> GetStateVariables()
         {
-            var list = new List<StateVariable>();
-
-            list.Add(new StateVariable
+            var list = new List<StateVariable>
             {
-                Name = "AuthorizationGrantedUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "AuthorizationGrantedUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_DeviceID",
-                DataType = "string",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_DeviceID",
+                    DataType = "string",
+                    SendsEvents = false
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "AuthorizationDeniedUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "AuthorizationDeniedUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "ValidationSucceededUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "ValidationSucceededUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_RegistrationRespMsg",
-                DataType = "bin.base64",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_RegistrationRespMsg",
+                    DataType = "bin.base64",
+                    SendsEvents = false
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_RegistrationReqMsg",
-                DataType = "bin.base64",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_RegistrationReqMsg",
+                    DataType = "bin.base64",
+                    SendsEvents = false
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "ValidationRevokedUpdateID",
-                DataType = "ui4",
-                SendsEvents = true
-            });
+                new StateVariable
+                {
+                    Name = "ValidationRevokedUpdateID",
+                    DataType = "ui4",
+                    SendsEvents = true
+                },
 
-            list.Add(new StateVariable
-            {
-                Name = "A_ARG_TYPE_Result",
-                DataType = "int",
-                SendsEvents = false
-            });
+                new StateVariable
+                {
+                    Name = "A_ARG_TYPE_Result",
+                    DataType = "int",
+                    SendsEvents = false
+                }
+            };
 
             return list;
         }

+ 41 - 7
Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs

@@ -1,13 +1,19 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using Emby.Dlna.Common;
+using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {
-    public class ServiceActionListBuilder
+    /// <summary>
+    /// Defines the <see cref="ServiceActionListBuilder" />.
+    /// </summary>
+    public static class ServiceActionListBuilder
     {
-        public IEnumerable<ServiceAction> GetActions()
+        /// <summary>
+        /// Returns a list of services that this instance provides.
+        /// </summary>
+        /// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
+        public static IEnumerable<ServiceAction> GetActions()
         {
             return new[]
             {
@@ -21,6 +27,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             };
         }
 
+        /// <summary>
+        /// Returns the action details for "IsValidated".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetIsValidated()
         {
             var action = new ServiceAction
@@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
+        /// <summary>
+        /// Returns the action details for "IsAuthorized".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetIsAuthorized()
         {
             var action = new ServiceAction
@@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
+        /// <summary>
+        /// Returns the action details for "RegisterDevice".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetRegisterDevice()
         {
             var action = new ServiceAction
@@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
+        /// <summary>
+        /// Returns the action details for "GetValidationSucceededUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
         private static ServiceAction GetGetValidationSucceededUpdateID()
         {
             var action = new ServiceAction
@@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
-        private ServiceAction GetGetAuthorizationDeniedUpdateID()
+        /// <summary>
+        /// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
+        private static ServiceAction GetGetAuthorizationDeniedUpdateID()
         {
             var action = new ServiceAction
             {
@@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
-        private ServiceAction GetGetValidationRevokedUpdateID()
+        /// <summary>
+        /// Returns the action details for "GetValidationRevokedUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
+        private static ServiceAction GetGetValidationRevokedUpdateID()
         {
             var action = new ServiceAction
             {
@@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             return action;
         }
 
-        private ServiceAction GetGetAuthorizationGrantedUpdateID()
+        /// <summary>
+        /// Returns the action details for "GetAuthorizationGrantedUpdateID".
+        /// </summary>
+        /// <returns>The <see cref="ServiceAction"/>.</returns>
+        private static ServiceAction GetGetAuthorizationGrantedUpdateID()
         {
             var action = new ServiceAction
             {

+ 5 - 11
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -235,13 +235,13 @@ namespace Emby.Dlna.Server
                     .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
                     .Append("</serviceId>");
                 builder.Append("<SCPDURL>")
-                    .Append(BuildUrl(service.ScpdUrl, true))
+                    .Append(BuildUrl(service.ScpdUrl))
                     .Append("</SCPDURL>");
                 builder.Append("<controlURL>")
-                    .Append(BuildUrl(service.ControlUrl, true))
+                    .Append(BuildUrl(service.ControlUrl))
                     .Append("</controlURL>");
                 builder.Append("<eventSubURL>")
-                    .Append(BuildUrl(service.EventSubUrl, true))
+                    .Append(BuildUrl(service.EventSubUrl))
                     .Append("</eventSubURL>");
 
                 builder.Append("</service>");
@@ -250,13 +250,7 @@ namespace Emby.Dlna.Server
             builder.Append("</serviceList>");
         }
 
-        /// <summary>
-        /// Builds a valid url for inclusion in the xml.
-        /// </summary>
-        /// <param name="url">Url to include.</param>
-        /// <param name="absoluteUrl">Optional. When set to true, the absolute url is always used.</param>
-        /// <returns>The url to use for the element.</returns>
-        private string BuildUrl(string url, bool absoluteUrl = false)
+        private string BuildUrl(string url)
         {
             if (string.IsNullOrEmpty(url))
             {
@@ -267,7 +261,7 @@ namespace Emby.Dlna.Server
 
             url = "/dlna/" + _serverUdn + "/" + url;
 
-            if (EnableAbsoluteUrls || absoluteUrl)
+            if (EnableAbsoluteUrls)
             {
                 url = _serverAddress.TrimEnd('/') + url;
             }

+ 25 - 19
Emby.Dlna/Service/BaseControlHandler.cs

@@ -60,10 +60,8 @@ namespace Emby.Dlna.Service
                     Async = true
                 };
 
-                using (var reader = XmlReader.Create(streamReader, readerSettings))
-                {
-                    requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
-                }
+                using var reader = XmlReader.Create(streamReader, readerSettings);
+                requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
             }
 
             Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
@@ -124,10 +122,8 @@ namespace Emby.Dlna.Service
                             {
                                 if (!reader.IsEmptyElement)
                                 {
-                                    using (var subReader = reader.ReadSubtree())
-                                    {
-                                        return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
-                                    }
+                                    using var subReader = reader.ReadSubtree();
+                                    return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
                                 }
                                 else
                                 {
@@ -150,12 +146,12 @@ namespace Emby.Dlna.Service
                 }
             }
 
-            return new ControlRequestInfo();
+            throw new EndOfStreamException("Stream ended but no body tag found.");
         }
 
         private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
         {
-            var result = new ControlRequestInfo();
+            string namespaceURI = null, localName = null;
 
             await reader.MoveToContentAsync().ConfigureAwait(false);
             await reader.ReadAsync().ConfigureAwait(false);
@@ -165,16 +161,14 @@ namespace Emby.Dlna.Service
             {
                 if (reader.NodeType == XmlNodeType.Element)
                 {
-                    result.LocalName = reader.LocalName;
-                    result.NamespaceURI = reader.NamespaceURI;
+                    localName = reader.LocalName;
+                    namespaceURI = reader.NamespaceURI;
 
                     if (!reader.IsEmptyElement)
                     {
-                        using (var subReader = reader.ReadSubtree())
-                        {
-                            await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
-                            return result;
-                        }
+                        var result = new ControlRequestInfo(localName, namespaceURI);
+                        using var subReader = reader.ReadSubtree();
+                        await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
                     }
                     else
                     {
@@ -187,7 +181,12 @@ namespace Emby.Dlna.Service
                 }
             }
 
-            return result;
+            if (localName != null && namespaceURI != null)
+            {
+                return new ControlRequestInfo(localName, namespaceURI);
+            }
+
+            throw new EndOfStreamException("Stream ended but no control found.");
         }
 
         private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
@@ -234,11 +233,18 @@ namespace Emby.Dlna.Service
 
         private class ControlRequestInfo
         {
+            public ControlRequestInfo(string localName, string namespaceUri)
+            {
+                LocalName = localName;
+                NamespaceURI = namespaceUri;
+                Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            }
+
             public string LocalName { get; set; }
 
             public string NamespaceURI { get; set; }
 
-            public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            public Dictionary<string, string> Headers { get; }
         }
     }
 }

+ 3 - 3
Emby.Drawing/ImageProcessor.cs

@@ -36,7 +36,7 @@ namespace Emby.Drawing
         private readonly IImageEncoder _imageEncoder;
         private readonly IMediaEncoder _mediaEncoder;
 
-        private bool _disposed = false;
+        private bool _disposed;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
@@ -466,11 +466,11 @@ namespace Emby.Drawing
         }
 
         /// <inheritdoc />
-        public void CreateImageCollage(ImageCollageOptions options)
+        public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
         {
             _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
 
-            _imageEncoder.CreateImageCollage(options);
+            _imageEncoder.CreateImageCollage(options, libraryName);
 
             _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
         }

+ 1 - 1
Emby.Drawing/NullImageEncoder.cs

@@ -38,7 +38,7 @@ namespace Emby.Drawing
         }
 
         /// <inheritdoc />
-        public void CreateImageCollage(ImageCollageOptions options)
+        public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
         {
             throw new NotImplementedException();
         }

+ 4 - 13
Emby.Naming/AudioBook/AudioBookResolver.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
             _options = options;
         }
 
-        public AudioBookFileInfo ParseFile(string path)
+        public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
         {
-            return Resolve(path, false);
-        }
-
-        public AudioBookFileInfo ParseDirectory(string path)
-        {
-            return Resolve(path, true);
-        }
-
-        public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
-        {
-            if (string.IsNullOrEmpty(path))
+            if (path.Length == 0)
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("String can't be empty.", nameof(path));
             }
 
             // TODO

+ 5 - 40
Emby.Server.Implementations/ApplicationHost.cs

@@ -128,7 +128,6 @@ namespace Emby.Server.Implementations
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private IHttpClientFactory _httpClientFactory;
-        private IWebSocketManager _webSocketManager;
 
         private string[] _urlPrefixes;
 
@@ -259,8 +258,8 @@ namespace Emby.Server.Implementations
             IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
-            _jsonSerializer = new JsonSerializer();            
-            
+            _jsonSerializer = new JsonSerializer();
+
             ServiceCollection = serviceCollection;
 
             _networkManager = networkManager;
@@ -667,7 +666,6 @@ namespace Emby.Server.Implementations
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
             _httpClientFactory = Resolve<IHttpClientFactory>();
-            _webSocketManager = Resolve<IWebSocketManager>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
@@ -788,7 +786,6 @@ namespace Emby.Server.Implementations
                         .ToArray();
 
             _urlPrefixes = GetUrlPrefixes().ToArray();
-            _webSocketManager.Init(GetExports<IWebSocketListener>());
 
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
@@ -821,38 +818,6 @@ namespace Emby.Server.Implementations
         {
             try
             {
-                if (plugin is IPluginAssembly assemblyPlugin)
-                {
-                    var assembly = plugin.GetType().Assembly;
-                    var assemblyName = assembly.GetName();
-                    var assemblyFilePath = assembly.Location;
-
-                    var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
-
-                    assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
-
-                    try
-                    {
-                        var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
-                        if (idAttributes.Length > 0)
-                        {
-                            var attribute = (GuidAttribute)idAttributes[0];
-                            var assemblyId = new Guid(attribute.Value);
-
-                            assemblyPlugin.SetId(assemblyId);
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName);
-                    }
-                }
-
-                if (plugin is IHasPluginConfiguration hasPluginConfiguration)
-                {
-                    hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
-                }
-
                 plugin.RegisterServices(ServiceCollection);
             }
             catch (Exception ex)
@@ -1090,7 +1055,7 @@ namespace Emby.Server.Implementations
                     {
                         // No metafile, so lets see if the folder is versioned.
                         metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
-                        
+
                         int versionIndex = dir.LastIndexOf('_');
                         if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
                         {
@@ -1099,9 +1064,9 @@ namespace Emby.Server.Implementations
                         }
                         else
                         {
-                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.                        
+                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.
                             versions.Add((new Version(0, 0, 0, 1), metafile, dir));
-                        }   
+                        }
                     }
                 }
                 catch

+ 2 - 1
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data
 
         protected bool TableExists(ManagedConnection connection, string name)
         {
-            return connection.RunInTransaction(db =>
+            return connection.RunInTransaction(
+            db =>
             {
                 using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
                 {

+ 23 - 12
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
             {
                 connection.RunQueries(queries);
 
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var existingColumnNames = GetColumnNames(db, "AncestorIds");
                     AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
                     {
@@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     SaveItemsInTranscation(db, tuples);
                 }, TransactionMode);
@@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     // First delete chapters
                     db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
@@ -2921,7 +2925,8 @@ namespace Emby.Server.Implementations.Data
             var result = new QueryResult<BaseItem>();
             using (var connection = GetConnection(true))
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var statements = PrepareAll(db, statementTexts);
 
@@ -3324,7 +3329,8 @@ namespace Emby.Server.Implementations.Data
             var result = new QueryResult<Guid>();
             using (var connection = GetConnection(true))
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var statements = PrepareAll(db, statementTexts);
 
@@ -4899,7 +4905,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     connection.ExecuteAll(sql);
                 }, TransactionMode);
@@ -4950,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var idBlob = id.ToByteArray();
 
@@ -5357,7 +5365,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                 itemCountColumns = new Dictionary<string, string>()
                 {
-                    { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"}
+                    { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
                 };
             }
 
@@ -5744,7 +5752,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var itemIdBlob = itemId.ToByteArray();
 
@@ -5898,7 +5907,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var itemIdBlob = id.ToByteArray();
 
@@ -6232,7 +6242,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     var itemIdBlob = id.ToByteArray();
 

+ 6 - 3
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data
 
                 var users = userDatasTableExists ? null : userManager.Users;
 
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     db.ExecuteAll(string.Join(";", new[] {
 
@@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     SaveUserData(db, internalUserId, key, userData);
                 }, TransactionMode);
@@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     foreach (var userItemData in userDataList)
                     {

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

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

+ 5 - 12
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -2,7 +2,6 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Net.WebSockets;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
@@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
 {
     public class WebSocketManager : IWebSocketManager
     {
+        private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
         private readonly ILogger<WebSocketManager> _logger;
         private readonly ILoggerFactory _loggerFactory;
 
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
         private bool _disposed = false;
 
         public WebSocketManager(
+            Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
             ILogger<WebSocketManager> logger,
             ILoggerFactory loggerFactory)
         {
+            _webSocketListeners = webSocketListeners;
             _logger = logger;
             _loggerFactory = loggerFactory;
         }
@@ -68,15 +69,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        /// <summary>
-        /// Adds the rest handlers.
-        /// </summary>
-        /// <param name="listeners">The web socket listeners.</param>
-        public void Init(IEnumerable<IWebSocketListener> listeners)
-        {
-            _webSocketListeners = listeners.ToArray();
-        }
-
         /// <summary>
         /// Processes the web socket message received.
         /// </summary>
@@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
 
             IEnumerable<Task> GetTasks()
             {
-                foreach (var x in _webSocketListeners)
+                var listeners = _webSocketListeners.Value;
+                foreach (var x in listeners)
                 {
                     yield return x.ProcessMessageAsync(result);
                 }

+ 12 - 1
Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs

@@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
 
         protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
         {
+            var useBackdrop = primaryItem is CollectionFolder;
             return items
                 .Select(i =>
                 {
+                    // Use Backdrop instead of Primary image for Library images.
+                    if (useBackdrop)
+                    {
+                        var backdrop = i.GetImageInfo(ImageType.Backdrop, 0);
+                        if (backdrop != null && backdrop.IsLocalFile)
+                        {
+                            return backdrop.Path;
+                        }
+                    }
+
                     var image = i.GetImageInfo(ImageType.Primary, 0);
                     if (image != null && image.IsLocalFile)
                     {
@@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images
                 return null;
             }
 
-            ImageProcessor.CreateImageCollage(options);
+            ImageProcessor.CreateImageCollage(options, primaryItem.Name);
             return outputPath;
         }
 

+ 17 - 34
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
 
-        private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
+        private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
 
         private IMediaSourceProvider[] _providers;
@@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library
             mediaSource.InferTotalBitrate();
         }
 
-        public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
+        public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
         {
-            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
+            var info = _openStreams.Values.FirstOrDefault(i =>
             {
-                var info = _openStreams.Values.FirstOrDefault(i =>
+                var liveStream = i as ILiveStream;
+                if (liveStream != null)
                 {
-                    var liveStream = i as ILiveStream;
-                    if (liveStream != null)
-                    {
-                        return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
-                    }
+                    return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
+                }
 
-                    return false;
-                });
+                return false;
+            });
 
-                return info as IDirectStreamProvider;
-            }
-            finally
-            {
-                _liveStreamSemaphore.Release();
-            }
+            return Task.FromResult(info as IDirectStreamProvider);
         }
 
         public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library
             return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
         }
 
-        private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+        private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
         {
             if (string.IsNullOrEmpty(id))
             {
                 throw new ArgumentNullException(nameof(id));
             }
 
-            await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
+            if (_openStreams.TryGetValue(id, out ILiveStream info))
             {
-                if (_openStreams.TryGetValue(id, out ILiveStream info))
-                {
-                    return info;
-                }
-                else
-                {
-                    throw new ResourceNotFoundException();
-                }
+                return Task.FromResult(info);
             }
-            finally
+            else
             {
-                _liveStreamSemaphore.Release();
+                return Task.FromException<ILiveStream>(new ResourceNotFoundException());
             }
         }
 
@@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library
 
                     if (liveStream.ConsumerCount <= 0)
                     {
-                        _openStreams.Remove(id);
+                        _openStreams.TryRemove(id, out _);
 
                         _logger.LogInformation("Closing live stream {0}", id);
 

+ 4 - 2
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <value>The priority.</value>
         public override ResolverPriority Priority => ResolverPriority.Fourth;
 
-        public MultiItemResolverResult ResolveMultiple(Folder parent,
+        public MultiItemResolverResult ResolveMultiple(
+            Folder parent,
             List<FileSystemMetadata> files,
             string collectionType,
             IDirectoryService directoryService)
@@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             return result;
         }
 
-        private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
+        private MultiItemResolverResult ResolveMultipleInternal(
+            Folder parent,
             List<FileSystemMetadata> files,
             string collectionType,
             IDirectoryService directoryService)

+ 33 - 34
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -1,5 +1,8 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
 using Emby.Naming.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -113,52 +116,48 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             IFileSystem fileSystem,
             ILibraryManager libraryManager)
         {
+            // check for audio files before digging down into directories
+            var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
+            if (foundAudioFile)
+            {
+                // at least one audio file exists
+                return true;
+            }
+
+            if (!allowSubfolders)
+            {
+                // not music since no audio file exists and we're not looking into subfolders
+                return false;
+            }
+
             var discSubfolderCount = 0;
-            var notMultiDisc = false;
 
             var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
             var parser = new AlbumParser(namingOptions);
-            foreach (var fileSystemInfo in list)
+
+            var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
+
+            var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
             {
-                if (fileSystemInfo.IsDirectory)
+                var path = fileSystemInfo.FullName;
+                var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
+
+                if (hasMusic)
                 {
-                    if (allowSubfolders)
+                    if (parser.IsMultiPart(path))
                     {
-                        if (notMultiDisc)
-                        {
-                            continue;
-                        }
-
-                        var path = fileSystemInfo.FullName;
-                        var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
-
-                        if (hasMusic)
-                        {
-                            if (parser.IsMultiPart(path))
-                            {
-                                logger.LogDebug("Found multi-disc folder: " + path);
-                                discSubfolderCount++;
-                            }
-                            else
-                            {
-                                // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
-                                notMultiDisc = true;
-                            }
-                        }
+                        logger.LogDebug("Found multi-disc folder: " + path);
+                        Interlocked.Increment(ref discSubfolderCount);
                     }
-                }
-                else
-                {
-                    var fullName = fileSystemInfo.FullName;
-
-                    if (libraryManager.IsAudioFile(fullName))
+                    else
                     {
-                        return true;
+                        // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
+                        state.Stop();
                     }
                 }
-            }
+            });
 
-            if (notMultiDisc)
+            if (!result.IsCompleted)
             {
                 return false;
             }

+ 13 - 1
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using System.Threading.Tasks;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
 
             // If we contain an album assume we are an artist folder
-            return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
+            var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
+
+            var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
+            {
+                if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+                {
+                    // stop once we see a music album
+                    state.Stop();
+                }
+            });
+
+            return !result.IsCompleted ? new MusicArtist() : null;
         }
     }
 }

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs

@@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
                 var fileExtension = Path.GetExtension(f.FullName) ??
                                     string.Empty;
 
-                return _validExtensions.Contains(fileExtension,
+                return _validExtensions.Contains(
+                    fileExtension,
                                                 StringComparer
                                                     .OrdinalIgnoreCase);
             }).ToList();

+ 1 - 1
Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs

@@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv
             return new[]
             {
                 // Every so often
-                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
             };
         }
 

+ 0 - 1
Emby.Server.Implementations/Localization/Core/af.json

@@ -85,7 +85,6 @@
     "ItemAddedWithName": "{0} is in die versameling",
     "HomeVideos": "Tuis opnames",
     "HeaderRecordingGroups": "Groep Opnames",
-    "HeaderCameraUploads": "Kamera Oplaai",
     "Genres": "Genres",
     "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
     "ChapterNameValue": "Hoofstuk",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ar.json

@@ -16,7 +16,6 @@
     "Folders": "المجلدات",
     "Genres": "التضنيفات",
     "HeaderAlbumArtists": "فناني الألبومات",
-    "HeaderCameraUploads": "تحميلات الكاميرا",
     "HeaderContinueWatching": "استئناف",
     "HeaderFavoriteAlbums": "الألبومات المفضلة",
     "HeaderFavoriteArtists": "الفنانون المفضلون",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/bg-BG.json

@@ -16,7 +16,6 @@
     "Folders": "Папки",
     "Genres": "Жанрове",
     "HeaderAlbumArtists": "Изпълнители на албуми",
-    "HeaderCameraUploads": "Качени от камера",
     "HeaderContinueWatching": "Продължаване на гледането",
     "HeaderFavoriteAlbums": "Любими албуми",
     "HeaderFavoriteArtists": "Любими изпълнители",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/bn.json

@@ -14,7 +14,6 @@
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderContinueWatching": "দেখতে থাকুন",
-    "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
     "HeaderAlbumArtists": "এলবাম শিল্পী",
     "Genres": "জেনার",
     "Folders": "ফোল্ডারগুলো",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ca.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetes",
     "Genres": "Gèneres",
     "HeaderAlbumArtists": "Artistes del Àlbum",
-    "HeaderCameraUploads": "Pujades de Càmera",
     "HeaderContinueWatching": "Continua Veient",
     "HeaderFavoriteAlbums": "Àlbums Preferits",
     "HeaderFavoriteArtists": "Artistes Preferits",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/cs.json

@@ -16,7 +16,6 @@
     "Folders": "Složky",
     "Genres": "Žánry",
     "HeaderAlbumArtists": "Umělci alba",
-    "HeaderCameraUploads": "Nahrané fotografie",
     "HeaderContinueWatching": "Pokračovat ve sledování",
     "HeaderFavoriteAlbums": "Oblíbená alba",
     "HeaderFavoriteArtists": "Oblíbení interpreti",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/da.json

@@ -16,7 +16,6 @@
     "Folders": "Mapper",
     "Genres": "Genrer",
     "HeaderAlbumArtists": "Albumkunstnere",
-    "HeaderCameraUploads": "Kamera Uploads",
     "HeaderContinueWatching": "Fortsæt Afspilning",
     "HeaderFavoriteAlbums": "Favoritalbummer",
     "HeaderFavoriteArtists": "Favoritkunstnere",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/de.json

@@ -16,7 +16,6 @@
     "Folders": "Verzeichnisse",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album-Interpreten",
-    "HeaderCameraUploads": "Kamera-Uploads",
     "HeaderContinueWatching": "Fortsetzen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
     "HeaderFavoriteArtists": "Lieblings-Interpreten",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/el.json

@@ -16,7 +16,6 @@
     "Folders": "Φάκελοι",
     "Genres": "Είδη",
     "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
-    "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
     "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
     "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
     "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/en-GB.json

@@ -16,7 +16,6 @@
     "Folders": "Folders",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album Artists",
-    "HeaderCameraUploads": "Camera Uploads",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favourite Albums",
     "HeaderFavoriteArtists": "Favourite Artists",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -16,7 +16,6 @@
     "Folders": "Folders",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album Artists",
-    "HeaderCameraUploads": "Camera Uploads",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favorite Albums",
     "HeaderFavoriteArtists": "Favorite Artists",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es-AR.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetas",
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas de álbum",
-    "HeaderCameraUploads": "Subidas de cámara",
     "HeaderContinueWatching": "Seguir viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetas",
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas del álbum",
-    "HeaderCameraUploads": "Subidas desde la cámara",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es.json

@@ -16,7 +16,6 @@
     "Folders": "Carpetas",
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas del álbum",
-    "HeaderCameraUploads": "Subidas desde la cámara",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

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

@@ -105,7 +105,6 @@
     "Inherit": "Heredar",
     "HomeVideos": "Videos caseros",
     "HeaderRecordingGroups": "Grupos de grabación",
-    "HeaderCameraUploads": "Subidas desde la cámara",
     "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
     "DeviceOnlineWithName": "{0} está conectado",
     "DeviceOfflineWithName": "{0} se ha desconectado",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/es_DO.json

@@ -12,7 +12,6 @@
     "Application": "Aplicación",
     "AppDeviceValues": "App: {0}, Dispositivo: {1}",
     "HeaderContinueWatching": "Continuar Viendo",
-    "HeaderCameraUploads": "Subidas de Cámara",
     "HeaderAlbumArtists": "Artistas del Álbum",
     "Genres": "Géneros",
     "Folders": "Carpetas",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/fa.json

@@ -16,7 +16,6 @@
     "Folders": "پوشه‌ها",
     "Genres": "ژانرها",
     "HeaderAlbumArtists": "هنرمندان آلبوم",
-    "HeaderCameraUploads": "آپلودهای دوربین",
     "HeaderContinueWatching": "ادامه تماشا",
     "HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه",
     "HeaderFavoriteArtists": "هنرمندان مورد علاقه",

+ 20 - 21
Emby.Server.Implementations/Localization/Core/fi.json

@@ -1,7 +1,7 @@
 {
     "HeaderLiveTV": "Live-TV",
     "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
-    "NameSeasonUnknown": "Tuntematon Kausi",
+    "NameSeasonUnknown": "Tuntematon kausi",
     "NameSeasonNumber": "Kausi {0}",
     "NameInstallFailed": "{0} asennus epäonnistui",
     "MusicVideos": "Musiikkivideot",
@@ -19,24 +19,23 @@
     "ItemAddedWithName": "{0} lisättiin kirjastoon",
     "Inherit": "Periytyä",
     "HomeVideos": "Kotivideot",
-    "HeaderRecordingGroups": "Nauhoiteryhmät",
+    "HeaderRecordingGroups": "Tallennusryhmät",
     "HeaderNextUp": "Seuraavaksi",
-    "HeaderFavoriteSongs": "Lempikappaleet",
-    "HeaderFavoriteShows": "Lempisarjat",
-    "HeaderFavoriteEpisodes": "Lempijaksot",
-    "HeaderCameraUploads": "Kamerasta Lähetetyt",
-    "HeaderFavoriteArtists": "Lempiartistit",
-    "HeaderFavoriteAlbums": "Lempialbumit",
+    "HeaderFavoriteSongs": "Suosikkikappaleet",
+    "HeaderFavoriteShows": "Suosikkisarjat",
+    "HeaderFavoriteEpisodes": "Suosikkijaksot",
+    "HeaderFavoriteArtists": "Suosikkiartistit",
+    "HeaderFavoriteAlbums": "Suosikkialbumit",
     "HeaderContinueWatching": "Jatka katsomista",
-    "HeaderAlbumArtists": "Albumin esittäjä",
+    "HeaderAlbumArtists": "Albumin artistit",
     "Genres": "Tyylilajit",
     "Folders": "Kansiot",
     "Favorites": "Suosikit",
     "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
     "DeviceOnlineWithName": "{0} on yhdistetty",
-    "DeviceOfflineWithName": "{0} on katkaissut yhteytensä",
+    "DeviceOfflineWithName": "{0} yhteys on katkaistu",
     "Collections": "Kokoelmat",
-    "ChapterNameValue": "Luku: {0}",
+    "ChapterNameValue": "Jakso: {0}",
     "Channels": "Kanavat",
     "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
     "Books": "Kirjat",
@@ -62,25 +61,25 @@
     "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 {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-sarjat",
+    "TvShows": "TV-ohjelmat",
     "Sync": "Synkronoi",
-    "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry",
-    "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.",
+    "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
+    "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
     "Songs": "Kappaleet",
-    "Shows": "Sarjat",
-    "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
+    "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 pitää käynnistää uudelleen",
+    "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
     "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
     "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
     "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
@@ -105,10 +104,10 @@
     "TaskRefreshPeople": "Päivitä henkilöt",
     "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
     "TaskCleanLogs": "Puhdista lokihakemisto",
-    "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.",
+    "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
     "TaskRefreshLibrary": "Skannaa mediakirjasto",
-    "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.",
-    "TaskRefreshChapterImages": "Eristä lukujen kuvat",
+    "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",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/fil.json

@@ -73,7 +73,6 @@
     "HeaderFavoriteArtists": "Paboritong Artista",
     "HeaderFavoriteAlbums": "Paboritong Albums",
     "HeaderContinueWatching": "Ituloy Manood",
-    "HeaderCameraUploads": "Camera Uploads",
     "HeaderAlbumArtists": "Artista ng Album",
     "Genres": "Kategorya",
     "Folders": "Folders",

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

@@ -16,7 +16,6 @@
     "Folders": "Dossiers",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Artistes de l'album",
-    "HeaderCameraUploads": "Photos transférées",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteArtists": "Artistes favoris",

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

@@ -16,7 +16,6 @@
     "Folders": "Dossiers",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Artistes",
-    "HeaderCameraUploads": "Photos transférées",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteArtists": "Artistes préférés",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/gsw.json

@@ -16,7 +16,6 @@
     "Folders": "Ordner",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album-Künstler",
-    "HeaderCameraUploads": "Kamera-Uploads",
     "HeaderContinueWatching": "weiter schauen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
     "HeaderFavoriteArtists": "Lieblings-Künstler",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/he.json

@@ -16,7 +16,6 @@
     "Folders": "תיקיות",
     "Genres": "ז'אנרים",
     "HeaderAlbumArtists": "אמני האלבום",
-    "HeaderCameraUploads": "העלאות ממצלמה",
     "HeaderContinueWatching": "המשך לצפות",
     "HeaderFavoriteAlbums": "אלבומים מועדפים",
     "HeaderFavoriteArtists": "אמנים מועדפים",

+ 3 - 0
Emby.Server.Implementations/Localization/Core/hi.json

@@ -0,0 +1,3 @@
+{
+    "Albums": "आल्बुम्"
+}

+ 0 - 1
Emby.Server.Implementations/Localization/Core/hr.json

@@ -16,7 +16,6 @@
     "Folders": "Mape",
     "Genres": "Žanrovi",
     "HeaderAlbumArtists": "Izvođači na albumu",
-    "HeaderCameraUploads": "Uvoz sa kamere",
     "HeaderContinueWatching": "Nastavi gledati",
     "HeaderFavoriteAlbums": "Omiljeni albumi",
     "HeaderFavoriteArtists": "Omiljeni izvođači",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/hu.json

@@ -16,7 +16,6 @@
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
     "HeaderAlbumArtists": "Album előadók",
-    "HeaderCameraUploads": "Kamera feltöltések",
     "HeaderContinueWatching": "Megtekintés folytatása",
     "HeaderFavoriteAlbums": "Kedvenc albumok",
     "HeaderFavoriteArtists": "Kedvenc előadók",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/id.json

@@ -20,7 +20,6 @@
     "HeaderFavoriteArtists": "Artis Favorit",
     "HeaderFavoriteAlbums": "Album Favorit",
     "HeaderContinueWatching": "Lanjut Menonton",
-    "HeaderCameraUploads": "Unggahan Kamera",
     "HeaderAlbumArtists": "Album Artis",
     "Genres": "Aliran",
     "Folders": "Folder",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/is.json

@@ -13,7 +13,6 @@
     "HeaderFavoriteArtists": "Uppáhalds Listamenn",
     "HeaderFavoriteAlbums": "Uppáhalds Plötur",
     "HeaderContinueWatching": "Halda áfram að horfa",
-    "HeaderCameraUploads": "Myndavéla upphal",
     "HeaderAlbumArtists": "Höfundur plötu",
     "Genres": "Tegundir",
     "Folders": "Möppur",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/it.json

@@ -16,7 +16,6 @@
     "Folders": "Cartelle",
     "Genres": "Generi",
     "HeaderAlbumArtists": "Artisti degli Album",
-    "HeaderCameraUploads": "Caricamenti Fotocamera",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderFavoriteAlbums": "Album Preferiti",
     "HeaderFavoriteArtists": "Artisti Preferiti",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ja.json

@@ -16,7 +16,6 @@
     "Folders": "フォルダー",
     "Genres": "ジャンル",
     "HeaderAlbumArtists": "アルバムアーティスト",
-    "HeaderCameraUploads": "カメラアップロード",
     "HeaderContinueWatching": "視聴を続ける",
     "HeaderFavoriteAlbums": "お気に入りのアルバム",
     "HeaderFavoriteArtists": "お気に入りのアーティスト",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/kk.json

@@ -16,7 +16,6 @@
     "Folders": "Qaltalar",
     "Genres": "Janrlar",
     "HeaderAlbumArtists": "Álbom oryndaýshylary",
-    "HeaderCameraUploads": "Kameradan júktelgender",
     "HeaderContinueWatching": "Qaraýdy jalǵastyrý",
     "HeaderFavoriteAlbums": "Tańdaýly álbomdar",
     "HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ko.json

@@ -16,7 +16,6 @@
     "Folders": "폴더",
     "Genres": "장르",
     "HeaderAlbumArtists": "앨범 아티스트",
-    "HeaderCameraUploads": "카메라 업로드",
     "HeaderContinueWatching": "계속 시청하기",
     "HeaderFavoriteAlbums": "즐겨찾는 앨범",
     "HeaderFavoriteArtists": "즐겨찾는 아티스트",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/lt-LT.json

@@ -16,7 +16,6 @@
     "Folders": "Katalogai",
     "Genres": "Žanrai",
     "HeaderAlbumArtists": "Albumo atlikėjai",
-    "HeaderCameraUploads": "Kameros",
     "HeaderContinueWatching": "Žiūrėti toliau",
     "HeaderFavoriteAlbums": "Mėgstami Albumai",
     "HeaderFavoriteArtists": "Mėgstami Atlikėjai",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/lv.json

@@ -72,7 +72,6 @@
     "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
     "HeaderLiveTV": "Tiešraides TV",
     "HeaderContinueWatching": "Turpināt Skatīšanos",
-    "HeaderCameraUploads": "Kameras augšupielādes",
     "HeaderAlbumArtists": "Albumu Izpildītāji",
     "Genres": "Žanri",
     "Folders": "Mapes",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/mk.json

@@ -51,7 +51,6 @@
     "HeaderFavoriteArtists": "Омилени Изведувачи",
     "HeaderFavoriteAlbums": "Омилени Албуми",
     "HeaderContinueWatching": "Продолжи со гледање",
-    "HeaderCameraUploads": "Поставувања од камера",
     "HeaderAlbumArtists": "Изведувачи од Албуми",
     "Genres": "Жанрови",
     "Folders": "Папки",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/mr.json

@@ -54,7 +54,6 @@
     "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
     "HomeVideos": "घरचे व्हिडीयो",
     "HeaderRecordingGroups": "रेकॉर्डिंग गट",
-    "HeaderCameraUploads": "कॅमेरा अपलोड",
     "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
     "Application": "अ‍ॅप्लिकेशन",
     "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ms.json

@@ -16,7 +16,6 @@
     "Folders": "Fail-fail",
     "Genres": "Genre-genre",
     "HeaderAlbumArtists": "Album Artis-artis",
-    "HeaderCameraUploads": "Muatnaik Kamera",
     "HeaderContinueWatching": "Terus Menonton",
     "HeaderFavoriteAlbums": "Album-album Kegemaran",
     "HeaderFavoriteArtists": "Artis-artis Kegemaran",

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

@@ -16,7 +16,6 @@
     "Folders": "Mapper",
     "Genres": "Sjangre",
     "HeaderAlbumArtists": "Albumartister",
-    "HeaderCameraUploads": "Kameraopplastinger",
     "HeaderContinueWatching": "Fortsett å se",
     "HeaderFavoriteAlbums": "Favorittalbum",
     "HeaderFavoriteArtists": "Favorittartister",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ne.json

@@ -41,7 +41,6 @@
     "HeaderFavoriteArtists": "मनपर्ने कलाकारहरू",
     "HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू",
     "HeaderContinueWatching": "हेर्न जारी राख्नुहोस्",
-    "HeaderCameraUploads": "क्यामेरा अपलोडहरू",
     "HeaderAlbumArtists": "एल्बमका कलाकारहरू",
     "Genres": "विधाहरू",
     "Folders": "फोल्डरहरू",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/nl.json

@@ -16,7 +16,6 @@
     "Folders": "Mappen",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Albumartiesten",
-    "HeaderCameraUploads": "Camera-uploads",
     "HeaderContinueWatching": "Kijken hervatten",
     "HeaderFavoriteAlbums": "Favoriete albums",
     "HeaderFavoriteArtists": "Favoriete artiesten",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/nn.json

@@ -19,7 +19,6 @@
     "HeaderFavoriteArtists": "Favoritt Artistar",
     "HeaderFavoriteAlbums": "Favoritt Album",
     "HeaderContinueWatching": "Fortsett å sjå",
-    "HeaderCameraUploads": "Kamera Opplastingar",
     "HeaderAlbumArtists": "Album Artist",
     "Genres": "Sjangrar",
     "Folders": "Mapper",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/pl.json

@@ -16,7 +16,6 @@
     "Folders": "Foldery",
     "Genres": "Gatunki",
     "HeaderAlbumArtists": "Wykonawcy albumów",
-    "HeaderCameraUploads": "Przekazane obrazy",
     "HeaderContinueWatching": "Kontynuuj odtwarzanie",
     "HeaderFavoriteAlbums": "Ulubione albumy",
     "HeaderFavoriteArtists": "Ulubieni wykonawcy",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/pt-BR.json

@@ -16,7 +16,6 @@
     "Folders": "Pastas",
     "Genres": "Gêneros",
     "HeaderAlbumArtists": "Artistas do Álbum",
-    "HeaderCameraUploads": "Envios da Câmera",
     "HeaderContinueWatching": "Continuar Assistindo",
     "HeaderFavoriteAlbums": "Álbuns Favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",

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

@@ -16,7 +16,6 @@
     "Folders": "Pastas",
     "Genres": "Géneros",
     "HeaderAlbumArtists": "Artistas do Álbum",
-    "HeaderCameraUploads": "Envios a partir da câmara",
     "HeaderContinueWatching": "Continuar a Ver",
     "HeaderFavoriteAlbums": "Álbuns Favoritos",
     "HeaderFavoriteArtists": "Artistas Favoritos",
@@ -26,7 +25,7 @@
     "HeaderLiveTV": "TV em Direto",
     "HeaderNextUp": "A Seguir",
     "HeaderRecordingGroups": "Grupos de Gravação",
-    "HomeVideos": "Videos caseiros",
+    "HomeVideos": "Vídeos Caseiros",
     "Inherit": "Herdar",
     "ItemAddedWithName": "{0} foi adicionado à biblioteca",
     "ItemRemovedWithName": "{0} foi removido da biblioteca",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/pt.json

@@ -83,7 +83,6 @@
     "Playlists": "Listas de Reprodução",
     "Photos": "Fotografias",
     "Movies": "Filmes",
-    "HeaderCameraUploads": "Carregamentos a partir da câmara",
     "FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
     "DeviceOnlineWithName": "{0} está connectado",
     "DeviceOfflineWithName": "{0} desconectou-se",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ro.json

@@ -74,7 +74,6 @@
     "HeaderFavoriteArtists": "Artiști Favoriți",
     "HeaderFavoriteAlbums": "Albume Favorite",
     "HeaderContinueWatching": "Vizionează în continuare",
-    "HeaderCameraUploads": "Incărcări Cameră Foto",
     "HeaderAlbumArtists": "Album Artiști",
     "Genres": "Genuri",
     "Folders": "Dosare",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ru.json

@@ -16,7 +16,6 @@
     "Folders": "Папки",
     "Genres": "Жанры",
     "HeaderAlbumArtists": "Исполнители альбома",
-    "HeaderCameraUploads": "Камеры",
     "HeaderContinueWatching": "Продолжение просмотра",
     "HeaderFavoriteAlbums": "Избранные альбомы",
     "HeaderFavoriteArtists": "Избранные исполнители",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/sk.json

@@ -16,7 +16,6 @@
     "Folders": "Priečinky",
     "Genres": "Žánre",
     "HeaderAlbumArtists": "Umelci albumu",
-    "HeaderCameraUploads": "Nahrané fotografie",
     "HeaderContinueWatching": "Pokračovať v pozeraní",
     "HeaderFavoriteAlbums": "Obľúbené albumy",
     "HeaderFavoriteArtists": "Obľúbení umelci",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/sl-SI.json

@@ -16,7 +16,6 @@
     "Folders": "Mape",
     "Genres": "Zvrsti",
     "HeaderAlbumArtists": "Izvajalci albuma",
-    "HeaderCameraUploads": "Posnetki kamere",
     "HeaderContinueWatching": "Nadaljuj gledanje",
     "HeaderFavoriteAlbums": "Priljubljeni albumi",
     "HeaderFavoriteArtists": "Priljubljeni izvajalci",

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

@@ -96,7 +96,6 @@
     "HeaderFavoriteArtists": "Artistët e preferuar",
     "HeaderFavoriteAlbums": "Albumet e preferuar",
     "HeaderContinueWatching": "Vazhdo të shikosh",
-    "HeaderCameraUploads": "Ngarkimet nga Kamera",
     "HeaderAlbumArtists": "Artistët e albumeve",
     "Genres": "Zhanre",
     "Folders": "Dosje",
@@ -113,5 +112,5 @@
     "Artists": "Artistë",
     "Application": "Aplikacioni",
     "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
-    "Albums": "Albumet"
+    "Albums": "Albume"
 }

+ 0 - 1
Emby.Server.Implementations/Localization/Core/sr.json

@@ -74,7 +74,6 @@
     "HeaderFavoriteArtists": "Омиљени извођачи",
     "HeaderFavoriteAlbums": "Омиљени албуми",
     "HeaderContinueWatching": "Настави гледање",
-    "HeaderCameraUploads": "Слања са камере",
     "HeaderAlbumArtists": "Извођачи албума",
     "Genres": "Жанрови",
     "Folders": "Фасцикле",

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

@@ -9,14 +9,13 @@
     "Channels": "Kanaler",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Samlingar",
-    "DeviceOfflineWithName": "{0} har kopplat från",
+    "DeviceOfflineWithName": "{0} har kopplat ner",
     "DeviceOnlineWithName": "{0} är ansluten",
     "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
     "Favorites": "Favoriter",
     "Folders": "Mappar",
     "Genres": "Genrer",
     "HeaderAlbumArtists": "Albumartister",
-    "HeaderCameraUploads": "Kamerauppladdningar",
     "HeaderContinueWatching": "Fortsätt kolla",
     "HeaderFavoriteAlbums": "Favoritalbum",
     "HeaderFavoriteArtists": "Favoritartister",

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

@@ -20,7 +20,6 @@
     "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
     "Inherit": "மரபுரிமையாகப் பெறு",
     "HeaderRecordingGroups": "பதிவு குழுக்கள்",
-    "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
     "Folders": "கோப்புறைகள்",
     "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
     "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/th.json

@@ -50,7 +50,6 @@
     "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
     "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
     "HeaderContinueWatching": "ดูต่อ",
-    "HeaderCameraUploads": "อัปโหลดรูปถ่าย",
     "HeaderAlbumArtists": "อัลบั้มศิลปิน",
     "Genres": "ประเภท",
     "Folders": "โฟลเดอร์",

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

@@ -16,7 +16,6 @@
     "Folders": "Klasörler",
     "Genres": "Türler",
     "HeaderAlbumArtists": "Albüm Sanatçıları",
-    "HeaderCameraUploads": "Kamera Yüklemeleri",
     "HeaderContinueWatching": "İzlemeye Devam Et",
     "HeaderFavoriteAlbums": "Favori Albümler",
     "HeaderFavoriteArtists": "Favori Sanatçılar",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/uk.json

@@ -16,7 +16,6 @@
     "HeaderFavoriteArtists": "Улюблені виконавці",
     "HeaderFavoriteAlbums": "Улюблені альбоми",
     "HeaderContinueWatching": "Продовжити перегляд",
-    "HeaderCameraUploads": "Завантажено з камери",
     "HeaderAlbumArtists": "Виконавці альбому",
     "Genres": "Жанри",
     "Folders": "Каталоги",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/ur_PK.json

@@ -105,7 +105,6 @@
     "Inherit": "وراثت میں",
     "HomeVideos": "ہوم ویڈیو",
     "HeaderRecordingGroups": "ریکارڈنگ گروپس",
-    "HeaderCameraUploads": "کیمرہ اپلوڈز",
     "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
     "DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
     "DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",

+ 0 - 1
Emby.Server.Implementations/Localization/Core/vi.json

@@ -103,7 +103,6 @@
     "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
     "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
     "HeaderFavoriteAlbums": "Album Ưa Thích",
-    "HeaderCameraUploads": "Máy Ảnh Tải Lên",
     "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
     "DeviceOnlineWithName": "{0} đã kết nối",
     "DeviceOfflineWithName": "{0} đã ngắt kết nối",

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

@@ -16,7 +16,6 @@
     "Folders": "文件夹",
     "Genres": "风格",
     "HeaderAlbumArtists": "专辑作家",
-    "HeaderCameraUploads": "相机上传",
     "HeaderContinueWatching": "继续观影",
     "HeaderFavoriteAlbums": "收藏的专辑",
     "HeaderFavoriteArtists": "最爱的艺术家",

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

@@ -16,7 +16,6 @@
     "Folders": "檔案夾",
     "Genres": "風格",
     "HeaderAlbumArtists": "專輯藝人",
-    "HeaderCameraUploads": "相機上載",
     "HeaderContinueWatching": "繼續觀看",
     "HeaderFavoriteAlbums": "最愛專輯",
     "HeaderFavoriteArtists": "最愛的藝人",

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

@@ -16,7 +16,6 @@
     "Folders": "資料夾",
     "Genres": "風格",
     "HeaderAlbumArtists": "專輯演出者",
-    "HeaderCameraUploads": "相機上傳",
     "HeaderContinueWatching": "繼續觀賞",
     "HeaderFavoriteAlbums": "最愛專輯",
     "HeaderFavoriteArtists": "最愛演出者",

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

@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             return new[]
             {
                 // Every so often
-                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
             };
         }
 

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

@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         {
             return new[]
             {
-                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+                new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
             };
         }
 

+ 12 - 6
Emby.Server.Implementations/Security/AuthenticationRepository.cs

@@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Security
             {
                 if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
                 {
-                    connection.RunInTransaction(db =>
+                    connection.RunInTransaction(
+                    db =>
                     {
                         var existingColumnNames = GetColumnNames(db, "AccessTokens");
 
@@ -88,7 +89,8 @@ namespace Emby.Server.Implementations.Security
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
                     {
@@ -119,7 +121,8 @@ namespace Emby.Server.Implementations.Security
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
                     {
@@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Security
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
                     {
@@ -346,7 +350,8 @@ namespace Emby.Server.Implementations.Security
         {
             using (var connection = GetConnection(true))
             {
-                return connection.RunInTransaction(db =>
+                return connection.RunInTransaction(
+                db =>
                 {
                     using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
                     {
@@ -377,7 +382,8 @@ namespace Emby.Server.Implementations.Security
 
             using (var connection = GetConnection())
             {
-                connection.RunInTransaction(db =>
+                connection.RunInTransaction(
+                db =>
                 {
                     using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
                     {

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

@@ -666,7 +666,7 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
-            var eventArgs = new PlaybackProgressEventArgs
+            var eventArgs = new PlaybackStartEventArgs
             {
                 Item = libraryItem,
                 Users = users,

+ 10 - 10
Jellyfin.Api/Controllers/ActivityLogController.cs

@@ -1,7 +1,7 @@
 using System;
-using System.Linq;
+using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data.Queries;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
@@ -39,19 +39,19 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
         [HttpGet("Entries")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
+        public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] DateTime? minDate,
             [FromQuery] bool? hasUserId)
         {
-            var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
-                entries => entries.Where(entry => entry.DateCreated >= minDate
-                                                  && (!hasUserId.HasValue || (hasUserId.Value
-                                                      ? entry.UserId != Guid.Empty
-                                                      : entry.UserId == Guid.Empty))));
-
-            return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
+            return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
+            {
+                StartIndex = startIndex,
+                Limit = limit,
+                MinDate = minDate,
+                HasUserId = hasUserId
+            }).ConfigureAwait(false);
         }
     }
 }

+ 6 - 6
Jellyfin.Api/Controllers/ArtistsController.cs

@@ -54,7 +54,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
@@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
                 }).Where(i => i != null).Select(i => i!.Id).ToArray();
             }
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {
@@ -263,7 +263,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
@@ -298,7 +298,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -397,7 +397,7 @@ namespace Jellyfin.Api.Controllers
                 }).Where(i => i != null).Select(i => i!.Id).ToArray();
             }
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {

+ 5 - 5
Jellyfin.Api/Controllers/ChannelsController.cs

@@ -105,7 +105,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <response code="200">Channel items returned.</response>
@@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] string? sortOrder,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] string? sortBy,
             [FromQuery] ItemFields[] fields)
         {
@@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
                     .AddItemFields(fields)
             };
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? channelIds)
         {
@@ -217,7 +217,7 @@ namespace Jellyfin.Api.Controllers
                     .AddItemFields(fields)
             };
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {

+ 3 - 3
Jellyfin.Api/Controllers/GenresController.cs

@@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
@@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
                     .ToArray();
             }
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {

+ 19 - 19
Jellyfin.Api/Controllers/ImageController.cs

@@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
         /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
         /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
-        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
         /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
         /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
         /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@@ -364,7 +364,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? quality,
             [FromQuery] string? tag,
             [FromQuery] bool? cropWhitespace,
-            [FromQuery] string? format,
+            [FromQuery] ImageFormat? format,
             [FromQuery] bool? addPlayedIndicator,
             [FromQuery] double? percentPlayed,
             [FromQuery] int? unplayedCount,
@@ -443,7 +443,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? quality,
             [FromRoute, Required] string tag,
             [FromQuery] bool? cropWhitespace,
-            [FromRoute, Required] string format,
+            [FromRoute, Required] ImageFormat format,
             [FromQuery] bool? addPlayedIndicator,
             [FromRoute, Required] double percentPlayed,
             [FromRoute, Required] int unplayedCount,
@@ -516,7 +516,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromQuery] string tag,
-            [FromQuery] string format,
+            [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] double? percentPlayed,
@@ -595,7 +595,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromQuery] string tag,
-            [FromQuery] string format,
+            [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] double? percentPlayed,
@@ -674,7 +674,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromQuery] string tag,
-            [FromQuery] string format,
+            [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] double? percentPlayed,
@@ -753,7 +753,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromQuery] string tag,
-            [FromQuery] string format,
+            [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] double? percentPlayed,
@@ -832,7 +832,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] string tag,
-            [FromRoute, Required] string format,
+            [FromRoute, Required] ImageFormat format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] double? percentPlayed,
@@ -911,7 +911,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid userId,
             [FromRoute, Required] ImageType imageType,
             [FromQuery] string? tag,
-            [FromQuery] string? format,
+            [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] double? percentPlayed,
@@ -1038,7 +1038,7 @@ namespace Jellyfin.Api.Controllers
             ImageType imageType,
             int? imageIndex,
             string? tag,
-            string? format,
+            ImageFormat? format,
             int? maxWidth,
             int? maxHeight,
             double? percentPlayed,
@@ -1128,12 +1128,11 @@ namespace Jellyfin.Api.Controllers
                 isHeadRequest).ConfigureAwait(false);
         }
 
-        private ImageFormat[] GetOutputFormats(string? format)
+        private ImageFormat[] GetOutputFormats(ImageFormat? format)
         {
-            if (!string.IsNullOrWhiteSpace(format)
-                && Enum.TryParse(format, true, out ImageFormat parsedFormat))
+            if (format.HasValue)
             {
-                return new[] { parsedFormat };
+                return new[] { format.Value };
             }
 
             return GetClientSupportedFormats();
@@ -1157,7 +1156,7 @@ namespace Jellyfin.Api.Controllers
 
             var acceptParam = Request.Query[HeaderNames.Accept];
 
-            var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
+            var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
 
             if (!supportsWebP)
             {
@@ -1179,7 +1178,7 @@ namespace Jellyfin.Api.Controllers
             formats.Add(ImageFormat.Jpg);
             formats.Add(ImageFormat.Png);
 
-            if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
+            if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
             {
                 formats.Add(ImageFormat.Gif);
             }
@@ -1187,9 +1186,10 @@ namespace Jellyfin.Api.Controllers
             return formats.ToArray();
         }
 
-        private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+        private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll)
         {
-            var mimeType = "image/" + format;
+            var normalized = format.ToString().ToLowerInvariant();
+            var mimeType = "image/" + normalized;
 
             if (requestAcceptTypes.Contains(mimeType))
             {
@@ -1201,7 +1201,7 @@ namespace Jellyfin.Api.Controllers
                 return true;
             }
 
-            return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
+            return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
         }
 
         private async Task<ActionResult> GetImageResult(

+ 5 - 8
Jellyfin.Api/Controllers/ItemsController.cs

@@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isHd,
             [FromQuery] bool? is4K,
             [FromQuery] string? locationTypes,
-            [FromQuery] string? excludeLocationTypes,
+            [FromQuery] LocationType[] excludeLocationTypes,
             [FromQuery] bool? isMissing,
             [FromQuery] bool? isUnaired,
             [FromQuery] double? minCommunityRating,
@@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? imageTypes,
@@ -365,7 +365,7 @@ namespace Jellyfin.Api.Controllers
                     query.CollapseBoxSetItems = false;
                 }
 
-                foreach (var filter in RequestHelpers.GetFilters(filters!))
+                foreach (var filter in filters)
                 {
                     switch (filter)
                     {
@@ -406,12 +406,9 @@ namespace Jellyfin.Api.Controllers
                 }
 
                 // ExcludeLocationTypes
-                if (!string.IsNullOrEmpty(excludeLocationTypes))
+                if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
                 {
-                    if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual))
-                    {
-                        query.IsVirtualItem = false;
-                    }
+                    query.IsVirtualItem = false;
                 }
 
                 if (!string.IsNullOrEmpty(locationTypes))

+ 3 - 3
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
@@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
                     .ToArray();
             }
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {

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

@@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
                     .ToArray();
             }
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {

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

@@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>The list of scheduled tasks.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public IEnumerable<IScheduledTaskWorker> GetTasks(
+        public IEnumerable<TaskInfo> GetTasks(
             [FromQuery] bool? isHidden,
             [FromQuery] bool? isEnabled)
         {
@@ -57,7 +57,7 @@ namespace Jellyfin.Api.Controllers
                     }
                 }
 
-                yield return task;
+                yield return ScheduledTaskHelpers.GetTaskInfo(task);
             }
         }
 

+ 3 - 3
Jellyfin.Api/Controllers/StudiosController.cs

@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? genres,
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
                     .ToArray();
             }
 
-            foreach (var filter in RequestHelpers.GetFilters(filters))
+            foreach (var filter in filters)
             {
                 switch (filter)
                 {

+ 3 - 2
Jellyfin.Api/Controllers/TrailersController.cs

@@ -1,6 +1,7 @@
 using System;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
@@ -124,7 +125,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isHd,
             [FromQuery] bool? is4K,
             [FromQuery] string? locationTypes,
-            [FromQuery] string? excludeLocationTypes,
+            [FromQuery] LocationType[] excludeLocationTypes,
             [FromQuery] bool? isMissing,
             [FromQuery] bool? isUnaired,
             [FromQuery] double? minCommunityRating,
@@ -146,7 +147,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? parentId,
             [FromQuery] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
-            [FromQuery] string? filters,
+            [FromQuery] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? imageTypes,

部分文件因文件數量過多而無法顯示