< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.PluginsController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/PluginsController.cs
Line coverage
35%
Covered lines: 24
Uncovered lines: 43
Coverable lines: 67
Total lines: 282
Line coverage: 35.8%
Branch coverage
27%
Covered branches: 10
Total branches: 36
Branch coverage: 27.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 4/15/2026 - 12:14:34 AM Line coverage: 23.8% (10/42) Branch coverage: 0% (0/20) Total lines: 2634/19/2026 - 12:14:27 AM Line coverage: 19.6% (10/51) Branch coverage: 0% (0/26) Total lines: 2634/30/2026 - 12:14:58 AM Line coverage: 17.5% (10/57) Branch coverage: 0% (0/30) Total lines: 2625/22/2026 - 12:15:17 AM Line coverage: 15.3% (10/65) Branch coverage: 0% (0/34) Total lines: 2787/18/2026 - 12:15:19 AM Line coverage: 35.8% (24/67) Branch coverage: 27.7% (10/36) Total lines: 2817/22/2026 - 12:16:22 AM Line coverage: 35.8% (24/67) Branch coverage: 27.7% (10/36) Total lines: 282 4/15/2026 - 12:14:34 AM Line coverage: 23.8% (10/42) Branch coverage: 0% (0/20) Total lines: 2634/19/2026 - 12:14:27 AM Line coverage: 19.6% (10/51) Branch coverage: 0% (0/26) Total lines: 2634/30/2026 - 12:14:58 AM Line coverage: 17.5% (10/57) Branch coverage: 0% (0/30) Total lines: 2625/22/2026 - 12:15:17 AM Line coverage: 15.3% (10/65) Branch coverage: 0% (0/34) Total lines: 2787/18/2026 - 12:15:19 AM Line coverage: 35.8% (24/67) Branch coverage: 27.7% (10/36) Total lines: 2817/22/2026 - 12:16:22 AM Line coverage: 35.8% (24/67) Branch coverage: 27.7% (10/36) Total lines: 282

Coverage delta

Coverage delta 28 -28

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetPlugins()100%11100%
EnablePlugin(...)0%620%
DisablePlugin(...)0%620%
UninstallPluginByVersion(...)0%620%
UninstallPlugin(...)0%2040%
GetPluginConfiguration(...)0%2040%
UpdatePluginConfiguration()0%4260%
GetPluginImage(...)71.42%181473.68%
GetPluginManifest(...)0%620%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Controllers/PluginsController.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.ComponentModel.DataAnnotations;
 4using System.IO;
 5using System.Linq;
 6using System.Text.Json;
 7using System.Threading.Tasks;
 8using Jellyfin.Api.Attributes;
 9using Jellyfin.Extensions;
 10using Jellyfin.Extensions.Json;
 11using MediaBrowser.Common.Api;
 12using MediaBrowser.Common.Plugins;
 13using MediaBrowser.Common.Updates;
 14using MediaBrowser.Model.Net;
 15using MediaBrowser.Model.Plugins;
 16using Microsoft.AspNetCore.Authorization;
 17using Microsoft.AspNetCore.Http;
 18using Microsoft.AspNetCore.Mvc;
 19
 20namespace Jellyfin.Api.Controllers;
 21
 22/// <summary>
 23/// Plugins controller.
 24/// </summary>
 25[Authorize(Policy = Policies.RequiresElevation)]
 26[Tags("Plugin")]
 27public class PluginsController : BaseJellyfinApiController
 28{
 29    private readonly IInstallationManager _installationManager;
 30    private readonly IPluginManager _pluginManager;
 31    private readonly JsonSerializerOptions _serializerOptions;
 32
 33    /// <summary>
 34    /// Initializes a new instance of the <see cref="PluginsController"/> class.
 35    /// </summary>
 36    /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
 37    /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
 1138    public PluginsController(
 1139        IInstallationManager installationManager,
 1140        IPluginManager pluginManager)
 41    {
 1142        _installationManager = installationManager;
 1143        _pluginManager = pluginManager;
 1144        _serializerOptions = JsonDefaults.Options;
 1145    }
 46
 47    /// <summary>
 48    /// Gets a list of currently installed plugins.
 49    /// </summary>
 50    /// <response code="200">Installed plugins returned.</response>
 51    /// <returns>List of currently installed plugins.</returns>
 52    [HttpGet]
 53    [ProducesResponseType(StatusCodes.Status200OK)]
 54    public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
 55    {
 156        return Ok(_pluginManager.Plugins
 157            .OrderBy(p => p.Name)
 158            .Select(p => p.GetPluginInfo()));
 59    }
 60
 61    /// <summary>
 62    /// Enables a disabled plugin.
 63    /// </summary>
 64    /// <param name="pluginId">Plugin id.</param>
 65    /// <param name="version">Plugin version.</param>
 66    /// <response code="204">Plugin enabled.</response>
 67    /// <response code="404">Plugin not found.</response>
 68    /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not 
 69    [HttpPost("{pluginId}/{version}/Enable")]
 70    [ProducesResponseType(StatusCodes.Status204NoContent)]
 71    [ProducesResponseType(StatusCodes.Status404NotFound)]
 72    public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
 73    {
 074        var plugin = _pluginManager.GetPlugin(pluginId, version);
 075        if (plugin is null)
 76        {
 077            return NotFound();
 78        }
 79
 080        _pluginManager.EnablePlugin(plugin);
 081        return NoContent();
 82    }
 83
 84    /// <summary>
 85    /// Disable a plugin.
 86    /// </summary>
 87    /// <param name="pluginId">Plugin id.</param>
 88    /// <param name="version">Plugin version.</param>
 89    /// <response code="204">Plugin disabled.</response>
 90    /// <response code="404">Plugin not found.</response>
 91    /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not 
 92    [HttpPost("{pluginId}/{version}/Disable")]
 93    [ProducesResponseType(StatusCodes.Status204NoContent)]
 94    [ProducesResponseType(StatusCodes.Status404NotFound)]
 95    public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
 96    {
 097        var plugin = _pluginManager.GetPlugin(pluginId, version);
 098        if (plugin is null)
 99        {
 0100            return NotFound();
 101        }
 102
 0103        _pluginManager.DisablePlugin(plugin);
 0104        return NoContent();
 105    }
 106
 107    /// <summary>
 108    /// Uninstalls a plugin by version.
 109    /// </summary>
 110    /// <param name="pluginId">Plugin id.</param>
 111    /// <param name="version">Plugin version.</param>
 112    /// <response code="204">Plugin uninstalled.</response>
 113    /// <response code="404">Plugin not found.</response>
 114    /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not 
 115    [HttpDelete("{pluginId}/{version}")]
 116    [ProducesResponseType(StatusCodes.Status204NoContent)]
 117    [ProducesResponseType(StatusCodes.Status404NotFound)]
 118    public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version vers
 119    {
 0120        var plugin = _pluginManager.GetPlugin(pluginId, version);
 0121        if (plugin is null)
 122        {
 0123            return NotFound();
 124        }
 125
 0126        _installationManager.UninstallPlugin(plugin);
 0127        return NoContent();
 128    }
 129
 130    /// <summary>
 131    /// Uninstalls a plugin.
 132    /// </summary>
 133    /// <param name="pluginId">Plugin id.</param>
 134    /// <response code="204">Plugin uninstalled.</response>
 135    /// <response code="404">Plugin not found.</response>
 136    /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not 
 137    [HttpDelete("{pluginId}")]
 138    [ProducesResponseType(StatusCodes.Status204NoContent)]
 139    [ProducesResponseType(StatusCodes.Status404NotFound)]
 140    public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
 141    {
 142        // If no version is given, return the current instance.
 0143        var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList();
 144
 145        // Select the un-instanced one first.
 0146        var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.MinBy(p => p.Manifest.Status);
 147
 0148        if (plugin is not null)
 149        {
 0150            _installationManager.UninstallPlugin(plugin);
 0151            return NoContent();
 152        }
 153
 0154        return NotFound();
 155    }
 156
 157    /// <summary>
 158    /// Gets plugin configuration.
 159    /// </summary>
 160    /// <param name="pluginId">Plugin id.</param>
 161    /// <response code="200">Plugin configuration returned.</response>
 162    /// <response code="404">Plugin not found or plugin configuration not found.</response>
 163    /// <returns>Plugin configuration.</returns>
 164    [HttpGet("{pluginId}/Configuration")]
 165    [ProducesResponseType(StatusCodes.Status200OK)]
 166    [ProducesResponseType(StatusCodes.Status404NotFound)]
 167    public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
 168    {
 0169        var plugin = _pluginManager.GetPlugin(pluginId);
 0170        if (plugin?.Instance is IHasPluginConfiguration configPlugin)
 171        {
 0172            return configPlugin.Configuration;
 173        }
 174
 0175        return NotFound();
 176    }
 177
 178    /// <summary>
 179    /// Updates plugin configuration.
 180    /// </summary>
 181    /// <remarks>
 182    /// Accepts plugin configuration as JSON body.
 183    /// </remarks>
 184    /// <param name="pluginId">Plugin id.</param>
 185    /// <response code="204">Plugin configuration updated.</response>
 186    /// <response code="404">Plugin not found or plugin does not have configuration.</response>
 187    /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not 
 188    [HttpPost("{pluginId}/Configuration")]
 189    [ProducesResponseType(StatusCodes.Status204NoContent)]
 190    [ProducesResponseType(StatusCodes.Status404NotFound)]
 191    public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
 192    {
 0193        var plugin = _pluginManager.GetPlugin(pluginId);
 0194        if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
 195        {
 0196            return NotFound();
 197        }
 198
 0199        var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.C
 0200            .ConfigureAwait(false);
 201
 0202        if (configuration is not null)
 203        {
 0204            configPlugin.UpdateConfiguration(configuration);
 205        }
 206
 0207        return NoContent();
 0208    }
 209
 210    /// <summary>
 211    /// Gets a plugin's image.
 212    /// </summary>
 213    /// <param name="pluginId">Plugin id.</param>
 214    /// <param name="version">Plugin version.</param>
 215    /// <response code="200">Plugin image returned.</response>
 216    /// <returns>Plugin's image.</returns>
 217    [HttpGet("{pluginId}/{version}/Image")]
 218    [ProducesResponseType(StatusCodes.Status200OK)]
 219    [ProducesResponseType(StatusCodes.Status404NotFound)]
 220    [ProducesImageFile]
 221    [AllowAnonymous]
 222    public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
 223    {
 10224        var plugin = _pluginManager.GetPlugin(pluginId, version);
 10225        if (plugin is null)
 226        {
 1227            return NotFound();
 228        }
 229
 9230        string? imagePath = plugin.Manifest.ImagePath;
 9231        if (!string.IsNullOrWhiteSpace(imagePath))
 232        {
 6233            var pluginPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(plugin.Path));
 6234            imagePath = Path.GetFullPath(imagePath, pluginPath);
 235            // Require a separator after the plugin path so a sibling like "<pluginPath>-evil" can't pass.
 6236            if (imagePath.StartsWith(pluginPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) is fa
 237            {
 5238                return NotFound();
 239            }
 240
 1241            Response.Headers.ContentDisposition = "attachment";
 1242            return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
 243        }
 244
 3245        var resourceName = plugin.Manifest.ImageResourceName;
 3246        if (!string.IsNullOrEmpty(resourceName) && plugin.Instance is not null)
 247        {
 0248            var stream = plugin.Instance.GetType().Assembly.GetManifestResourceStream(resourceName);
 0249            if (stream is null)
 250            {
 0251                return NotFound();
 252            }
 253
 0254            Response.Headers.ContentDisposition = "attachment";
 0255            return File(stream, MimeTypes.GetMimeType(resourceName));
 256        }
 257
 3258        return NotFound();
 259    }
 260
 261    /// <summary>
 262    /// Gets a plugin's manifest.
 263    /// </summary>
 264    /// <param name="pluginId">Plugin id.</param>
 265    /// <response code="204">Plugin manifest returned.</response>
 266    /// <response code="404">Plugin not found.</response>
 267    /// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be
 268    [HttpPost("{pluginId}/Manifest")]
 269    [ProducesResponseType(StatusCodes.Status204NoContent)]
 270    [ProducesResponseType(StatusCodes.Status404NotFound)]
 271    public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId)
 272    {
 0273        var plugin = _pluginManager.GetPlugin(pluginId);
 274
 0275        if (plugin is not null)
 276        {
 0277            return plugin.Manifest;
 278        }
 279
 0280        return NotFound();
 281    }
 282}