diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
new file mode 100644
index 000000000..59196a41a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -0,0 +1,189 @@
+#nullable enable
+#pragma warning disable CA1801
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.PluginDtos;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Plugins;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers
+{
+ ///
+ /// Plugins controller.
+ ///
+ [Authorize]
+ public class PluginsController : BaseJellyfinApiController
+ {
+ private readonly IApplicationHost _appHost;
+ private readonly IInstallationManager _installationManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public PluginsController(
+ IApplicationHost appHost,
+ IInstallationManager installationManager)
+ {
+ _appHost = appHost;
+ _installationManager = installationManager;
+ }
+
+ ///
+ /// Gets a list of currently installed plugins.
+ ///
+ /// Optional. Unused.
+ /// Installed plugins returned.
+ /// List of currently installed plugins.
+ [HttpGet]
+ public ActionResult> GetPlugins([FromRoute] bool? isAppStoreEnabled)
+ {
+ return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
+ }
+
+ ///
+ /// Uninstalls a plugin.
+ ///
+ /// Plugin id.
+ /// Plugin uninstalled.
+ /// Plugin not found.
+ /// An on success, or a if the file could not be found.
+ [HttpDelete("{pluginId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public ActionResult UninstallPlugin([FromRoute] Guid pluginId)
+ {
+ var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
+ if (plugin == null)
+ {
+ return NotFound();
+ }
+
+ _installationManager.UninstallPlugin(plugin);
+ return Ok();
+ }
+
+ ///
+ /// Gets plugin configuration.
+ ///
+ /// Plugin id.
+ /// Plugin configuration returned.
+ /// Plugin not found or plugin configuration not found.
+ /// Plugin configuration.
+ [HttpGet("{pluginId}/Configuration")]
+ public ActionResult GetPluginConfiguration([FromRoute] Guid pluginId)
+ {
+ if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+ {
+ return NotFound();
+ }
+
+ return plugin.Configuration;
+ }
+
+ ///
+ /// Updates plugin configuration.
+ ///
+ ///
+ /// Accepts plugin configuration as JSON body.
+ ///
+ /// Plugin id.
+ /// Plugin configuration updated.
+ /// Plugin not found or plugin does not have configuration.
+ ///
+ /// A that represents the asynchronous operation to update plugin configuration.
+ /// The task result contains an indicating success, or
+ /// when plugin not found or plugin doesn't have configuration.
+ ///
+ [HttpPost("{pluginId}/Configuration")]
+ public async Task UpdatePluginConfiguration([FromRoute] Guid pluginId)
+ {
+ if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
+ {
+ return NotFound();
+ }
+
+ var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType)
+ .ConfigureAwait(false);
+
+ plugin.UpdateConfiguration(configuration);
+ return Ok();
+ }
+
+ ///
+ /// Get plugin security info.
+ ///
+ /// Plugin security info returned.
+ /// Plugin security info.
+ [Obsolete("This endpoint should not be used.")]
+ [HttpGet("SecurityInfo")]
+ public ActionResult GetPluginSecurityInfo()
+ {
+ return new PluginSecurityInfo
+ {
+ IsMbSupporter = true,
+ SupporterKey = "IAmTotallyLegit"
+ };
+ }
+
+ ///
+ /// Updates plugin security info.
+ ///
+ /// Plugin security info.
+ /// Plugin security info updated.
+ /// An .
+ [Obsolete("This endpoint should not be used.")]
+ [HttpPost("SecurityInfo")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo)
+ {
+ return Ok();
+ }
+
+ ///
+ /// Gets registration status for a feature.
+ ///
+ /// Feature name.
+ /// Registration status returned.
+ /// Mb registration record.
+ [Obsolete("This endpoint should not be used.")]
+ [HttpPost("RegistrationRecords/{name}")]
+ public ActionResult GetRegistrationStatus([FromRoute] string name)
+ {
+ return new MBRegistrationRecord
+ {
+ IsRegistered = true,
+ RegChecked = true,
+ TrialVersion = false,
+ IsValid = true,
+ RegError = false
+ };
+ }
+
+ ///
+ /// Gets registration status for a feature.
+ ///
+ /// Feature name.
+ /// Not implemented.
+ /// Not Implemented.
+ /// This endpoint is not implemented.
+ [Obsolete("Paid plugins are not supported")]
+ [HttpGet("/Registrations/{name}")]
+ public ActionResult GetRegistration([FromRoute] string name)
+ {
+ // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
+ // delete all these registration endpoints. They are only kept for compatibility.
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
new file mode 100644
index 000000000..aaaf54267
--- /dev/null
+++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
@@ -0,0 +1,42 @@
+#nullable enable
+
+using System;
+
+namespace Jellyfin.Api.Models.PluginDtos
+{
+ ///
+ /// MB Registration Record.
+ ///
+ public class MBRegistrationRecord
+ {
+ ///
+ /// Gets or sets expiration date.
+ ///
+ public DateTime ExpirationDate { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether is registered.
+ ///
+ public bool IsRegistered { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether reg checked.
+ ///
+ public bool RegChecked { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether reg error.
+ ///
+ public bool RegError { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether trial version.
+ ///
+ public bool TrialVersion { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether is valid.
+ ///
+ public bool IsValid { get; set; }
+ }
+}
diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
new file mode 100644
index 000000000..793002a6c
--- /dev/null
+++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
@@ -0,0 +1,20 @@
+#nullable enable
+
+namespace Jellyfin.Api.Models.PluginDtos
+{
+ ///
+ /// Plugin security info.
+ ///
+ public class PluginSecurityInfo
+ {
+ ///
+ /// Gets or sets the supporter key.
+ ///
+ public string? SupporterKey { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether is mb supporter.
+ ///
+ public bool IsMbSupporter { get; set; }
+ }
+}
diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs
deleted file mode 100644
index 7f74511ee..000000000
--- a/MediaBrowser.Api/PluginService.cs
+++ /dev/null
@@ -1,268 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Plugins;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api
-{
- ///
- /// Class Plugins
- ///
- [Route("/Plugins", "GET", Summary = "Gets a list of currently installed plugins")]
- [Authenticated]
- public class GetPlugins : IReturn
- {
- public bool? IsAppStoreEnabled { get; set; }
- }
-
- ///
- /// Class UninstallPlugin
- ///
- [Route("/Plugins/{Id}", "DELETE", Summary = "Uninstalls a plugin")]
- [Authenticated(Roles = "Admin")]
- public class UninstallPlugin : IReturnVoid
- {
- ///
- /// Gets or sets the id.
- ///
- /// The id.
- [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
- public string Id { get; set; }
- }
-
- ///
- /// Class GetPluginConfiguration
- ///
- [Route("/Plugins/{Id}/Configuration", "GET", Summary = "Gets a plugin's configuration")]
- [Authenticated]
- public class GetPluginConfiguration
- {
- ///
- /// Gets or sets the id.
- ///
- /// The id.
- [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
-
- ///
- /// Class UpdatePluginConfiguration
- ///
- [Route("/Plugins/{Id}/Configuration", "POST", Summary = "Updates a plugin's configuration")]
- [Authenticated]
- public class UpdatePluginConfiguration : IRequiresRequestStream, IReturnVoid
- {
- ///
- /// Gets or sets the id.
- ///
- /// The id.
- [ApiMember(Name = "Id", Description = "Plugin Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string Id { get; set; }
-
- ///
- /// The raw Http Request Input Stream
- ///
- /// The request stream.
- public Stream RequestStream { get; set; }
- }
-
- //TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
- // delete all these registration endpoints. They are only kept for compatibility.
- [Route("/Registrations/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
- [Authenticated]
- public class GetRegistration : IReturn
- {
- [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
- }
-
- ///
- /// Class GetPluginSecurityInfo
- ///
- [Route("/Plugins/SecurityInfo", "GET", Summary = "Gets plugin registration information", IsHidden = true)]
- [Authenticated]
- public class GetPluginSecurityInfo : IReturn
- {
- }
-
- ///
- /// Class UpdatePluginSecurityInfo
- ///
- [Route("/Plugins/SecurityInfo", "POST", Summary = "Updates plugin registration information", IsHidden = true)]
- [Authenticated(Roles = "Admin")]
- public class UpdatePluginSecurityInfo : PluginSecurityInfo, IReturnVoid
- {
- }
-
- [Route("/Plugins/RegistrationRecords/{Name}", "GET", Summary = "Gets registration status for a feature", IsHidden = true)]
- [Authenticated]
- public class GetRegistrationStatus
- {
- [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Name { get; set; }
- }
-
- // TODO these two classes are only kept for compability with paid plugins and should be removed
- public class RegistrationInfo
- {
- public string Name { get; set; }
- public DateTime ExpirationDate { get; set; }
- public bool IsTrial { get; set; }
- public bool IsRegistered { get; set; }
- }
-
- public class MBRegistrationRecord
- {
- public DateTime ExpirationDate { get; set; }
- public bool IsRegistered { get; set; }
- public bool RegChecked { get; set; }
- public bool RegError { get; set; }
- public bool TrialVersion { get; set; }
- public bool IsValid { get; set; }
- }
-
- public class PluginSecurityInfo
- {
- public string SupporterKey { get; set; }
- public bool IsMBSupporter { get; set; }
- }
- ///
- /// Class PluginsService
- ///
- public class PluginService : BaseApiService
- {
- ///
- /// The _json serializer
- ///
- private readonly IJsonSerializer _jsonSerializer;
-
- ///
- /// The _app host
- ///
- private readonly IApplicationHost _appHost;
- private readonly IInstallationManager _installationManager;
-
- public PluginService(
- ILogger logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IJsonSerializer jsonSerializer,
- IApplicationHost appHost,
- IInstallationManager installationManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _appHost = appHost;
- _installationManager = installationManager;
- _jsonSerializer = jsonSerializer;
- }
-
- ///
- /// Gets the specified request.
- ///
- /// The request.
- /// System.Object.
- public object Get(GetRegistrationStatus request)
- {
- var record = new MBRegistrationRecord
- {
- IsRegistered = true,
- RegChecked = true,
- TrialVersion = false,
- IsValid = true,
- RegError = false
- };
-
- return ToOptimizedResult(record);
- }
-
- ///
- /// Gets the specified request.
- ///
- /// The request.
- /// System.Object.
- public object Get(GetPlugins request)
- {
- var result = _appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToArray();
- return ToOptimizedResult(result);
- }
-
- ///
- /// Gets the specified request.
- ///
- /// The request.
- /// System.Object.
- public object Get(GetPluginConfiguration request)
- {
- var guid = new Guid(request.Id);
- var plugin = _appHost.Plugins.First(p => p.Id == guid) as IHasPluginConfiguration;
-
- return ToOptimizedResult(plugin.Configuration);
- }
-
- ///
- /// Gets the specified request.
- ///
- /// The request.
- /// System.Object.
- public object Get(GetPluginSecurityInfo request)
- {
- var result = new PluginSecurityInfo
- {
- IsMBSupporter = true,
- SupporterKey = "IAmTotallyLegit"
- };
-
- return ToOptimizedResult(result);
- }
-
- ///
- /// Posts the specified request.
- ///
- /// The request.
- public Task Post(UpdatePluginSecurityInfo request)
- {
- return Task.CompletedTask;
- }
-
- ///
- /// Posts the specified request.
- ///
- /// The request.
- public async Task Post(UpdatePluginConfiguration request)
- {
- // We need to parse this manually because we told service stack not to with IRequiresRequestStream
- // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
- var id = Guid.Parse(GetPathValue(1));
-
- if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin))
- {
- throw new FileNotFoundException();
- }
-
- var configuration = (await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, plugin.ConfigurationType).ConfigureAwait(false)) as BasePluginConfiguration;
-
- plugin.UpdateConfiguration(configuration);
- }
-
- ///
- /// Deletes the specified request.
- ///
- /// The request.
- public void Delete(UninstallPlugin request)
- {
- var guid = new Guid(request.Id);
- var plugin = _appHost.Plugins.First(p => p.Id == guid);
-
- _installationManager.UninstallPlugin(plugin);
- }
- }
-}