using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Jellyfin.Api.Models; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { /// /// The dashboard controller. /// public class DashboardController : BaseJellyfinApiController { private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; private readonly IConfiguration _appConfig; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IResourceFileManager _resourceFileManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. /// Instance of interface. /// Instance of interface. /// Instance of interface. public DashboardController( ILogger logger, IServerApplicationHost appHost, IConfiguration appConfig, IResourceFileManager resourceFileManager, IServerConfigurationManager serverConfigurationManager) { _logger = logger; _appHost = appHost; _appConfig = appConfig; _resourceFileManager = resourceFileManager; _serverConfigurationManager = serverConfigurationManager; } /// /// Gets the path of the directory containing the static web interface content, or null if the server is not /// hosting the web client. /// private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager); /// /// Gets the configuration pages. /// /// Whether to enable in the main menu. /// The . /// ConfigurationPages returned. /// Server still loading. /// An with infos about the plugins. [HttpGet("/web/ConfigurationPages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetConfigurationPages( [FromQuery] bool? enableInMainMenu, [FromQuery] ConfigurationPageType? pageType) { const string unavailableMessage = "The server is still loading. Please try again momentarily."; var pages = _appHost.GetExports().ToList(); if (pages == null) { return NotFound(unavailableMessage); } // Don't allow a failing plugin to fail them all var configPages = pages.Select(p => { try { return new ConfigurationPageInfo(p); } catch (Exception ex) { _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name); return null; } }) .Where(i => i != null) .ToList(); configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); if (pageType.HasValue) { configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList(); } if (enableInMainMenu.HasValue) { configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList(); } return configPages; } /// /// Gets a dashboard configuration page. /// /// The name of the page. /// ConfigurationPage returned. /// Plugin configuration page not found. /// The configuration page. [HttpGet("/web/ConfigurationPage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetDashboardConfigurationPage([FromQuery] string name) { IPlugin? plugin = null; Stream? stream = null; var isJs = false; var isTemplate = false; var page = _appHost.GetExports().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); if (page != null) { plugin = page.Plugin; stream = page.GetHtmlStream(); } if (plugin == null) { var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase)); if (altPage != null) { plugin = altPage.Item2; stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); } } if (plugin != null && stream != null) { if (isJs) { return File(stream, MimeTypes.GetMimeType("page.js")); } if (isTemplate) { return File(stream, MimeTypes.GetMimeType("page.html")); } return File(stream, MimeTypes.GetMimeType("page.html")); } return NotFound(); } /// /// Gets the robots.txt. /// /// Robots.txt returned. /// The robots.txt. [HttpGet("/robots.txt")] [ProducesResponseType(StatusCodes.Status200OK)] [ApiExplorerSettings(IgnoreApi = true)] public ActionResult GetRobotsTxt() { return GetWebClientResource("robots.txt"); } /// /// Gets a resource from the web client. /// /// The resource name. /// Web client returned. /// Server does not host a web client. /// The resource. [HttpGet("/web/{*resourceName}")] [ApiExplorerSettings(IgnoreApi = true)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetWebClientResource([FromRoute] string resourceName) { if (!_appConfig.HostWebClient() || WebClientUiPath == null) { return NotFound("Server does not host a web client."); } var path = resourceName; var basePath = WebClientUiPath; // Bounce them to the startup wizard if it hasn't been completed yet if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase) && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase)) { return Redirect("index.html?start=wizard#!/wizardstart.html"); } var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read); return File(stream, MimeTypes.GetMimeType(path)); } /// /// Gets the favicon. /// /// Favicon.ico returned. /// The favicon. [HttpGet("/favicon.ico")] [ProducesResponseType(StatusCodes.Status200OK)] [ApiExplorerSettings(IgnoreApi = true)] public ActionResult GetFavIcon() { return GetWebClientResource("favicon.ico"); } /// /// Gets the path of the directory containing the static web interface content. /// /// The app configuration. /// The server configuration manager. /// The directory path, or null if the server is not hosting the web client. public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) { if (!appConfig.HostWebClient()) { return null; } if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) { return serverConfigManager.Configuration.DashboardSourcePath; } return serverConfigManager.ApplicationPaths.WebPath; } private IEnumerable GetConfigPages(IPlugin plugin) { return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); } private IEnumerable> GetPluginPages(IPlugin plugin) { if (!(plugin is IHasWebPages hasWebPages)) { return new List>(); } return hasWebPages.GetPages().Select(i => new Tuple(i, plugin)); } private IEnumerable> GetPluginPages() { return _appHost.Plugins.SelectMany(GetPluginPages); } } }