using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Api; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; 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.RequiresElevation)] public class PluginsController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; private readonly IPluginManager _pluginManager; private readonly JsonSerializerOptions _serializerOptions; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. public PluginsController( IInstallationManager installationManager, IPluginManager pluginManager) { _installationManager = installationManager; _pluginManager = pluginManager; _serializerOptions = JsonDefaults.Options; } /// /// Gets a list of currently installed plugins. /// /// Installed plugins returned. /// List of currently installed plugins. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPlugins() { return Ok(_pluginManager.Plugins .OrderBy(p => p.Name) .Select(p => p.GetPluginInfo())); } /// /// Enables a disabled plugin. /// /// Plugin id. /// Plugin version. /// Plugin enabled. /// Plugin not found. /// An on success, or a if the plugin could not be found. [HttpPost("{pluginId}/{version}/Enable")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { var plugin = _pluginManager.GetPlugin(pluginId, version); if (plugin is null) { return NotFound(); } _pluginManager.EnablePlugin(plugin); return NoContent(); } /// /// Disable a plugin. /// /// Plugin id. /// Plugin version. /// Plugin disabled. /// Plugin not found. /// An on success, or a if the plugin could not be found. [HttpPost("{pluginId}/{version}/Disable")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { var plugin = _pluginManager.GetPlugin(pluginId, version); if (plugin is null) { return NotFound(); } _pluginManager.DisablePlugin(plugin); return NoContent(); } /// /// Uninstalls a plugin by version. /// /// Plugin id. /// Plugin version. /// Plugin uninstalled. /// Plugin not found. /// An on success, or a if the plugin could not be found. [HttpDelete("{pluginId}/{version}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { var plugin = _pluginManager.GetPlugin(pluginId, version); if (plugin is null) { return NotFound(); } _installationManager.UninstallPlugin(plugin); return NoContent(); } /// /// Uninstalls a plugin. /// /// Plugin id. /// Plugin uninstalled. /// Plugin not found. /// An on success, or a if the plugin could not be found. [HttpDelete("{pluginId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("Please use the UninstallPluginByVersion API.")] public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) { // If no version is given, return the current instance. var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList(); // Select the un-instanced one first. var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.MinBy(p => p.Manifest.Status); if (plugin is not null) { _installationManager.UninstallPlugin(plugin); return NoContent(); } return NotFound(); } /// /// 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) { var plugin = _pluginManager.GetPlugin(pluginId); if (plugin?.Instance is IHasPluginConfiguration configPlugin) { return configPlugin.Configuration; } return NotFound(); } /// /// Updates plugin configuration. /// /// /// Accepts plugin configuration as JSON body. /// /// Plugin id. /// Plugin configuration updated. /// Plugin not found or plugin does not have configuration. /// An on success, or a if the plugin could not be found. [HttpPost("{pluginId}/Configuration")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) { var plugin = _pluginManager.GetPlugin(pluginId); if (plugin?.Instance is not IHasPluginConfiguration configPlugin) { return NotFound(); } var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions) .ConfigureAwait(false); if (configuration is not null) { configPlugin.UpdateConfiguration(configuration); } return NoContent(); } /// /// Gets a plugin's image. /// /// Plugin id. /// Plugin version. /// Plugin image returned. /// Plugin's image. [HttpGet("{pluginId}/{version}/Image")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] [AllowAnonymous] public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { var plugin = _pluginManager.GetPlugin(pluginId, version); if (plugin is null) { return NotFound(); } var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath)) { return NotFound(); } Response.Headers.ContentDisposition = "attachment"; imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); } /// /// Gets a plugin's manifest. /// /// Plugin id. /// Plugin manifest returned. /// Plugin not found. /// A on success, or a if the plugin could not be found. [HttpPost("{pluginId}/Manifest")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPluginManifest([FromRoute, Required] Guid pluginId) { var plugin = _pluginManager.GetPlugin(pluginId); if (plugin is not null) { return plugin.Manifest; } return NotFound(); } }