using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; 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.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers { /// /// Plugins controller. /// [Authorize(Policy = Policies.DefaultAuthorization)] public class PluginsController : BaseJellyfinApiController { private readonly IApplicationHost _appHost; private readonly IInstallationManager _installationManager; private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); /// /// 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. /// /// Installed plugins returned. /// List of currently installed plugins. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPlugins() { 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)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) { var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); if (plugin == null) { return NotFound(); } _installationManager.UninstallPlugin(plugin); return NoContent(); } /// /// Gets plugin configuration. /// /// Plugin id. /// Plugin configuration returned. /// Plugin not found or plugin configuration not found. /// Plugin configuration. [HttpGet("{pluginId}/Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPluginConfiguration([FromRoute, Required] 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")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdatePluginConfiguration([FromRoute, Required] 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, _serializerOptions) .ConfigureAwait(false); if (configuration != null) { plugin.UpdateConfiguration(configuration); } return NoContent(); } /// /// Get plugin security info. /// /// Plugin security info returned. /// Plugin security info. [Obsolete("This endpoint should not be used.")] [HttpGet("SecurityInfo")] [ProducesResponseType(StatusCodes.Status200OK)] 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)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo) { return NoContent(); } /// /// Gets registration status for a feature. /// /// Feature name. /// Registration status returned. /// Mb registration record. [Obsolete("This endpoint should not be used.")] [HttpPost("RegistrationRecords/{name}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetRegistrationStatus([FromRoute, Required] 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}")] [ProducesResponseType(StatusCodes.Status501NotImplemented)] public ActionResult GetRegistration([FromRoute, Required] 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(); } } }