using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Configuration;
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.DefaultAuthorization)]
public class PluginsController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
private readonly IPluginManager _pluginManager;
private readonly IConfigurationManager _config;
private readonly JsonSerializerOptions _serializerOptions;
///
/// Initializes a new instance of the class.
///
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
public PluginsController(
IInstallationManager installationManager,
IPluginManager pluginManager,
IConfigurationManager config)
{
_installationManager = installationManager;
_pluginManager = pluginManager;
_serializerOptions = JsonDefaults.GetCamelCaseOptions();
_config = config;
}
///
/// Get plugin security info.
///
/// Plugin security info returned.
/// Plugin security info.
[Obsolete("This endpoint should not be used.")]
[HttpGet("SecurityInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public static ActionResult GetPluginSecurityInfo()
{
return new PluginSecurityInfo
{
IsMbSupporter = true,
SupporterKey = "IAmTotallyLegit"
};
}
///
/// 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 static 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 static 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();
}
///
/// 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")]
[Authorize(Policy = Policies.RequiresElevation)]
[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 == 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")]
[Authorize(Policy = Policies.RequiresElevation)]
[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 == 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}")]
[Authorize(Policy = Policies.RequiresElevation)]
[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 == 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 file could not be found.
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[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));
// Select the un-instanced one first.
var plugin = plugins.FirstOrDefault(p => p.Instance != null);
if (plugin == null)
{
// Then by the status.
plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
}
_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)
{
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.
///
/// 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)
{
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 != 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 == null)
{
return NotFound();
}
var imgPath = Path.Combine(plugin.Path, plugin.Manifest.ImageUrl ?? string.Empty);
if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
|| plugin.Manifest.ImageUrl == null
|| !System.IO.File.Exists(imgPath))
{
// Use a blank image.
var type = GetType();
var stream = type.Assembly.GetManifestResourceStream(type.Namespace + ".Plugins.blank.png");
return File(stream, "image/png");
}
imgPath = Path.Combine(plugin.Path, plugin.Manifest.ImageUrl);
return PhysicalFile(imgPath, MimeTypes.GetMimeType(imgPath));
}
///
/// Gets a plugin's manifest.
///
/// Plugin id.
/// Plugin manifest returned.
/// Plugin not found.
///
/// A that represents the asynchronous operation to get the plugin's manifest.
/// The task result contains an indicating success, or
/// when plugin not found.
///
[HttpPost("{pluginId}/Manifest")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult GetPluginManifest([FromRoute, Required] Guid pluginId)
{
var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin != null)
{
return Ok(plugin.Manifest);
}
return NotFound();
}
///
/// 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();
}
}
}