diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
| -rw-r--r-- | Jellyfin.Api/Controllers/DashboardController.cs | 20 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 1 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/DlnaServerController.cs | 121 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LibraryController.cs | 4 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/MediaInfoController.cs | 28 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PackageController.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PluginsController.cs | 293 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SystemController.cs | 7 |
8 files changed, 360 insertions, 117 deletions
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index ccc81dfc5..b77d79209 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers { private readonly ILogger<DashboardController> _logger; private readonly IServerApplicationHost _appHost; + private readonly IPluginManager _pluginManager; /// <summary> /// Initializes a new instance of the <see cref="DashboardController"/> class. /// </summary> /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> + /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param> public DashboardController( ILogger<DashboardController> logger, - IServerApplicationHost appHost) + IServerApplicationHost appHost, + IPluginManager pluginManager) { _logger = logger; _appHost = appHost; + _pluginManager = pluginManager; } /// <summary> @@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers .Where(i => i != null) .ToList(); - configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); + configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages)); if (pageType.HasValue) { @@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin) + private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin) { - return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); + return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); } - private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin) + private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin) { - if (!(plugin is IHasWebPages hasWebPages)) + if (plugin?.Instance is not IHasWebPages hasWebPages) { return new List<Tuple<PluginPageInfo, IPlugin>>(); } - return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin)); + return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance)); } private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() { - return _appHost.Plugins.SelectMany(GetPluginPages); + return _pluginManager.Plugins.SelectMany(GetPluginPages); } } } diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index f7bb968f0..87b4577b6 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -85,6 +85,7 @@ namespace Jellyfin.Api.Controllers dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["tvhome"] = displayPreferences.TvHome; + dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme; // Load all custom display preferences var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 4fd9c2fbf..694d16ad9 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Description xml returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>An <see cref="OkResult"/> containing the description xml.</returns> [HttpGet("{serverId}/description")] [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) { - var url = GetAbsoluteUri(); - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); - return Ok(xml); + if (DlnaEntryPoint.Enabled) + { + var url = GetAbsoluteUri(); + var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); + return Ok(xml); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Dlna content directory returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> [HttpGet("{serverId}/ContentDirectory")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetContentDirectory([FromRoute, Required] string serverId) { - return Ok(_contentDirectory.GetServiceXml()); + if (DlnaEntryPoint.Enabled) + { + return Ok(_contentDirectory.GetServiceXml()); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Dlna media receiver registrar xml returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Dlna media receiver registrar xml.</returns> [HttpGet("{serverId}/MediaReceiverRegistrar")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) { - return Ok(_mediaReceiverRegistrar.GetServiceXml()); + if (DlnaEntryPoint.Enabled) + { + return Ok(_mediaReceiverRegistrar.GetServiceXml()); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Dlna media receiver registrar xml returned.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Dlna media receiver registrar xml.</returns> [HttpGet("{serverId}/ConnectionManager")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetConnectionManager([FromRoute, Required] string serverId) { - return Ok(_connectionManager.GetServiceXml()); + if (DlnaEntryPoint.Enabled) + { + return Ok(_connectionManager.GetServiceXml()); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Control response.</returns> [HttpPost("{serverId}/ContentDirectory/Control")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); + if (DlnaEntryPoint.Enabled) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Control response.</returns> [HttpPost("{serverId}/ConnectionManager/Control")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); + if (DlnaEntryPoint.Enabled) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Control response.</returns> [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); + if (DlnaEntryPoint.Enabled) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Event subscription response.</returns> [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) { - return ProcessEventRequest(_mediaReceiverRegistrar); + if (DlnaEntryPoint.Enabled) + { + return ProcessEventRequest(_mediaReceiverRegistrar); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Event subscription response.</returns> [HttpSubscribe("{serverId}/ContentDirectory/Events")] [HttpUnsubscribe("{serverId}/ContentDirectory/Events")] [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) { - return ProcessEventRequest(_contentDirectory); + if (DlnaEntryPoint.Enabled) + { + return ProcessEventRequest(_contentDirectory); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <response code="200">Request processed.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Event subscription response.</returns> [HttpSubscribe("{serverId}/ConnectionManager/Events")] [HttpUnsubscribe("{serverId}/ConnectionManager/Events")] [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) { - return ProcessEventRequest(_connectionManager); + if (DlnaEntryPoint.Enabled) + { + return ProcessEventRequest(_connectionManager); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="serverId">Server UUID.</param> /// <param name="fileName">The icon filename.</param> + /// <response code="200">Request processed.</response> + /// <response code="404">Not Found.</response> + /// <response code="503">DLNA is disabled.</response> /// <returns>Icon stream.</returns> [HttpGet("{serverId}/icons/{fileName}")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [ProducesImageFile] public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) { - return GetIconInternal(fileName); + if (DlnaEntryPoint.Enabled) + { + return GetIconInternal(fileName); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } /// <summary> @@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="fileName">The icon filename.</param> /// <returns>Icon stream.</returns> + /// <response code="200">Request processed.</response> + /// <response code="404">Not Found.</response> + /// <response code="503">DLNA is disabled.</response> [HttpGet("icons/{fileName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [ProducesImageFile] public ActionResult GetIcon([FromRoute, Required] string fileName) { - return GetIconInternal(fileName); + if (DlnaEntryPoint.Enabled) + { + return GetIconInternal(fileName); + } + + return StatusCode(StatusCodes.Status503ServiceUnavailable); } private ActionResult GetIconInternal(string fileName) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 184843b39..28d359ac3 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers } // TODO determine non-ASCII validity. - return PhysicalFile(path, MimeTypes.GetMimeType(path)); + return PhysicalFile(path, MimeTypes.GetMimeType(path), filename); } /// <summary> @@ -742,8 +742,6 @@ namespace Jellyfin.Api.Controllers { Limit = limit, IncludeItemTypes = includeItemTypes.ToArray(), - IsMovie = isMovie, - IsSeries = isSeries, SimilarTo = item, DtoOptions = dtoOptions, EnableTotalRecordCount = !isMovie ?? true, diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 2a1da31c9..baa2e0636 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -259,24 +259,24 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? subtitleStreamIndex, [FromQuery] int? maxAudioChannels, [FromQuery] Guid? itemId, - [FromBody] OpenLiveStreamDto openLiveStreamDto, - [FromQuery] bool enableDirectPlay = true, - [FromQuery] bool enableDirectStream = true) + [FromBody] OpenLiveStreamDto? openLiveStreamDto, + [FromQuery] bool? enableDirectPlay, + [FromQuery] bool? enableDirectStream) { var request = new LiveStreamRequest { - OpenToken = openToken, - UserId = userId ?? Guid.Empty, - PlaySessionId = playSessionId, - MaxStreamingBitrate = maxStreamingBitrate, - StartTimeTicks = startTimeTicks, - AudioStreamIndex = audioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex, - MaxAudioChannels = maxAudioChannels, - ItemId = itemId ?? Guid.Empty, + OpenToken = openToken ?? openLiveStreamDto?.OpenToken, + UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, + PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, + MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, + StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, + AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex, + MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels, + ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty, DeviceProfile = openLiveStreamDto?.DeviceProfile, - EnableDirectPlay = enableDirectPlay, - EnableDirectStream = enableDirectStream, + EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true, + EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true, DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } }; return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 6295dfc05..9ab8e0bdc 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using MediaBrowser.Common.Json; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Updates; @@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); if (!string.IsNullOrEmpty(repositoryUrl)) { - packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any()) + packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))) .ToList(); } diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 98f1bc2d2..b73611c97 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,15 +1,21 @@ 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; +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; @@ -23,22 +29,81 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public class PluginsController : BaseJellyfinApiController { - private readonly IApplicationHost _appHost; private readonly IInstallationManager _installationManager; - - private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); + private readonly IPluginManager _pluginManager; + private readonly IConfigurationManager _config; + private readonly JsonSerializerOptions _serializerOptions; /// <summary> /// Initializes a new instance of the <see cref="PluginsController"/> class. /// </summary> - /// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param> /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> + /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param> + /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> public PluginsController( - IApplicationHost appHost, - IInstallationManager installationManager) + IInstallationManager installationManager, + IPluginManager pluginManager, + IConfigurationManager config) { - _appHost = appHost; _installationManager = installationManager; + _pluginManager = pluginManager; + _serializerOptions = JsonDefaults.GetOptions(); + _config = config; + } + + /// <summary> + /// Get plugin security info. + /// </summary> + /// <response code="200">Plugin security info returned.</response> + /// <returns>Plugin security info.</returns> + [Obsolete("This endpoint should not be used.")] + [HttpGet("SecurityInfo")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() + { + return new PluginSecurityInfo + { + IsMbSupporter = true, + SupporterKey = "IAmTotallyLegit" + }; + } + + /// <summary> + /// Gets registration status for a feature. + /// </summary> + /// <param name="name">Feature name.</param> + /// <response code="200">Registration status returned.</response> + /// <returns>Mb registration record.</returns> + [Obsolete("This endpoint should not be used.")] + [HttpPost("RegistrationRecords/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name) + { + return new MBRegistrationRecord + { + IsRegistered = true, + RegChecked = true, + TrialVersion = false, + IsValid = true, + RegError = false + }; + } + + /// <summary> + /// Gets registration status for a feature. + /// </summary> + /// <param name="name">Feature name.</param> + /// <response code="501">Not implemented.</response> + /// <returns>Not Implemented.</returns> + /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception> + [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(); } /// <summary> @@ -50,23 +115,74 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<PluginInfo>> GetPlugins() { - return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo())); + return Ok(_pluginManager.Plugins + .OrderBy(p => p.Name) + .Select(p => p.GetPluginInfo())); } /// <summary> - /// Uninstalls a plugin. + /// Enables a disabled plugin. /// </summary> /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> + /// <response code="204">Plugin enabled.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [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(); + } + + /// <summary> + /// Disable a plugin. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> + /// <response code="204">Plugin disabled.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [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(); + } + + /// <summary> + /// Uninstalls a plugin by version. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> /// <response code="204">Plugin uninstalled.</response> /// <response code="404">Plugin not found.</response> - /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> - [HttpDelete("{pluginId}")] + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [HttpDelete("{pluginId}/{version}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) + public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { - var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId); + var plugin = _pluginManager.GetPlugin(pluginId, version); if (plugin == null) { return NotFound(); @@ -77,6 +193,40 @@ namespace Jellyfin.Api.Controllers } /// <summary> + /// Uninstalls a plugin. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <response code="204">Plugin uninstalled.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [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(); + } + + if (plugin != null) + { + _installationManager.UninstallPlugin(plugin); + return NoContent(); + } + + return NotFound(); + } + + /// <summary> /// Gets plugin configuration. /// </summary> /// <param name="pluginId">Plugin id.</param> @@ -88,12 +238,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId) { - if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + var plugin = _pluginManager.GetPlugin(pluginId); + if (plugin?.Instance is IHasPluginConfiguration configPlugin) { - return NotFound(); + return configPlugin.Configuration; } - return plugin.Configuration; + return NotFound(); } /// <summary> @@ -105,47 +256,81 @@ namespace Jellyfin.Api.Controllers /// <param name="pluginId">Plugin id.</param> /// <response code="204">Plugin configuration updated.</response> /// <response code="404">Plugin not found or plugin does not have configuration.</response> - /// <returns> - /// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration. - /// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/> - /// when plugin not found or plugin doesn't have configuration. - /// </returns> + /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> [HttpPost("{pluginId}/Configuration")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) { - if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin)) + var plugin = _pluginManager.GetPlugin(pluginId); + if (plugin?.Instance is not IHasPluginConfiguration configPlugin) { return NotFound(); } - var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions) + var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions) .ConfigureAwait(false); if (configuration != null) { - plugin.UpdateConfiguration(configuration); + configPlugin.UpdateConfiguration(configuration); } return NoContent(); } /// <summary> - /// Get plugin security info. + /// Gets a plugin's image. /// </summary> - /// <response code="200">Plugin security info returned.</response> - /// <returns>Plugin security info.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpGet("SecurityInfo")] + /// <param name="pluginId">Plugin id.</param> + /// <param name="version">Plugin version.</param> + /// <response code="200">Plugin image returned.</response> + /// <returns>Plugin's image.</returns> + [HttpGet("{pluginId}/{version}/Image")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + [AllowAnonymous] + public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) { - return new PluginSecurityInfo + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin == null) { - IsMbSupporter = true, - SupporterKey = "IAmTotallyLegit" - }; + return NotFound(); + } + + var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); + if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages + || plugin.Manifest.ImagePath == null + || !System.IO.File.Exists(imagePath)) + { + return NotFound(); + } + + imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); + return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); + } + + /// <summary> + /// Gets a plugin's manifest. + /// </summary> + /// <param name="pluginId">Plugin id.</param> + /// <response code="204">Plugin manifest returned.</response> + /// <response code="404">Plugin not found.</response> + /// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> + [HttpPost("{pluginId}/Manifest")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId) + { + var plugin = _pluginManager.GetPlugin(pluginId); + + if (plugin != null) + { + return plugin.Manifest; + } + + return NotFound(); } /// <summary> @@ -162,43 +347,5 @@ namespace Jellyfin.Api.Controllers { return NoContent(); } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="200">Registration status returned.</response> - /// <returns>Mb registration record.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpPost("RegistrationRecords/{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name) - { - return new MBRegistrationRecord - { - IsRegistered = true, - RegChecked = true, - TrialVersion = false, - IsValid = true, - RegError = false - }; - } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="501">Not implemented.</response> - /// <returns>Not Implemented.</returns> - /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception> - [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(); - } } } diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 7784e8a11..e67a27ae3 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; +using System.Net; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; @@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<SystemInfo> GetSystemInfo() { - return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); + return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// <summary> @@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<PublicSystemInfo> GetPublicSystemInfo() { - return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); + return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// <summary> @@ -202,7 +203,7 @@ namespace Jellyfin.Api.Controllers // For older files, assume fully static var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); - return File(stream, "text/plain"); + return File(stream, "text/plain; charset=utf-8"); } /// <summary> |
