DashboardController.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net.Mime;
  6. using Jellyfin.Api.Attributes;
  7. using Jellyfin.Api.Models;
  8. using MediaBrowser.Common.Plugins;
  9. using MediaBrowser.Controller;
  10. using MediaBrowser.Controller.Configuration;
  11. using MediaBrowser.Controller.Extensions;
  12. using MediaBrowser.Controller.Plugins;
  13. using MediaBrowser.Model.Net;
  14. using MediaBrowser.Model.Plugins;
  15. using Microsoft.AspNetCore.Http;
  16. using Microsoft.AspNetCore.Http.Extensions;
  17. using Microsoft.AspNetCore.Mvc;
  18. using Microsoft.Extensions.Configuration;
  19. using Microsoft.Extensions.Logging;
  20. namespace Jellyfin.Api.Controllers
  21. {
  22. /// <summary>
  23. /// The dashboard controller.
  24. /// </summary>
  25. [Route("")]
  26. public class DashboardController : BaseJellyfinApiController
  27. {
  28. private readonly ILogger<DashboardController> _logger;
  29. private readonly IServerApplicationHost _appHost;
  30. private readonly IConfiguration _appConfig;
  31. private readonly IServerConfigurationManager _serverConfigurationManager;
  32. private readonly IResourceFileManager _resourceFileManager;
  33. /// <summary>
  34. /// Initializes a new instance of the <see cref="DashboardController"/> class.
  35. /// </summary>
  36. /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
  37. /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
  38. /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param>
  39. /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param>
  40. /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
  41. public DashboardController(
  42. ILogger<DashboardController> logger,
  43. IServerApplicationHost appHost,
  44. IConfiguration appConfig,
  45. IResourceFileManager resourceFileManager,
  46. IServerConfigurationManager serverConfigurationManager)
  47. {
  48. _logger = logger;
  49. _appHost = appHost;
  50. _appConfig = appConfig;
  51. _resourceFileManager = resourceFileManager;
  52. _serverConfigurationManager = serverConfigurationManager;
  53. }
  54. /// <summary>
  55. /// Gets the path of the directory containing the static web interface content, or null if the server is not
  56. /// hosting the web client.
  57. /// </summary>
  58. private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager);
  59. /// <summary>
  60. /// Gets the configuration pages.
  61. /// </summary>
  62. /// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
  63. /// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param>
  64. /// <response code="200">ConfigurationPages returned.</response>
  65. /// <response code="404">Server still loading.</response>
  66. /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
  67. [HttpGet("web/ConfigurationPages")]
  68. [ProducesResponseType(StatusCodes.Status200OK)]
  69. [ProducesResponseType(StatusCodes.Status404NotFound)]
  70. public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
  71. [FromQuery] bool? enableInMainMenu,
  72. [FromQuery] ConfigurationPageType? pageType)
  73. {
  74. const string unavailableMessage = "The server is still loading. Please try again momentarily.";
  75. var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList();
  76. if (pages == null)
  77. {
  78. return NotFound(unavailableMessage);
  79. }
  80. // Don't allow a failing plugin to fail them all
  81. var configPages = pages.Select(p =>
  82. {
  83. try
  84. {
  85. return new ConfigurationPageInfo(p);
  86. }
  87. catch (Exception ex)
  88. {
  89. _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name);
  90. return null;
  91. }
  92. })
  93. .Where(i => i != null)
  94. .ToList();
  95. configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
  96. if (pageType.HasValue)
  97. {
  98. configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList();
  99. }
  100. if (enableInMainMenu.HasValue)
  101. {
  102. configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
  103. }
  104. return configPages;
  105. }
  106. /// <summary>
  107. /// Gets a dashboard configuration page.
  108. /// </summary>
  109. /// <param name="name">The name of the page.</param>
  110. /// <response code="200">ConfigurationPage returned.</response>
  111. /// <response code="404">Plugin configuration page not found.</response>
  112. /// <returns>The configuration page.</returns>
  113. [HttpGet("web/ConfigurationPage")]
  114. [ProducesResponseType(StatusCodes.Status200OK)]
  115. [ProducesResponseType(StatusCodes.Status404NotFound)]
  116. [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
  117. public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
  118. {
  119. IPlugin? plugin = null;
  120. Stream? stream = null;
  121. var isJs = false;
  122. var isTemplate = false;
  123. var page = _appHost.GetExports<IPluginConfigurationPage>().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
  124. if (page != null)
  125. {
  126. plugin = page.Plugin;
  127. stream = page.GetHtmlStream();
  128. }
  129. if (plugin == null)
  130. {
  131. var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
  132. if (altPage != null)
  133. {
  134. plugin = altPage.Item2;
  135. stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
  136. isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
  137. isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
  138. }
  139. }
  140. if (plugin != null && stream != null)
  141. {
  142. if (isJs)
  143. {
  144. return File(stream, MimeTypes.GetMimeType("page.js"));
  145. }
  146. if (isTemplate)
  147. {
  148. return File(stream, MimeTypes.GetMimeType("page.html"));
  149. }
  150. return File(stream, MimeTypes.GetMimeType("page.html"));
  151. }
  152. return NotFound();
  153. }
  154. /// <summary>
  155. /// Gets the robots.txt.
  156. /// </summary>
  157. /// <response code="200">Robots.txt returned.</response>
  158. /// <returns>The robots.txt.</returns>
  159. [HttpGet("robots.txt")]
  160. [ProducesResponseType(StatusCodes.Status200OK)]
  161. [ApiExplorerSettings(IgnoreApi = true)]
  162. public ActionResult GetRobotsTxt()
  163. {
  164. return GetWebClientResource("robots.txt");
  165. }
  166. /// <summary>
  167. /// Gets a resource from the web client.
  168. /// </summary>
  169. /// <param name="resourceName">The resource name.</param>
  170. /// <response code="200">Web client returned.</response>
  171. /// <response code="404">Server does not host a web client.</response>
  172. /// <returns>The resource.</returns>
  173. [HttpGet("web/{*resourceName}")]
  174. [ApiExplorerSettings(IgnoreApi = true)]
  175. [ProducesResponseType(StatusCodes.Status200OK)]
  176. [ProducesResponseType(StatusCodes.Status404NotFound)]
  177. public ActionResult GetWebClientResource([FromRoute] string resourceName)
  178. {
  179. if (!_appConfig.HostWebClient() || WebClientUiPath == null)
  180. {
  181. return NotFound("Server does not host a web client.");
  182. }
  183. var path = resourceName;
  184. var basePath = WebClientUiPath;
  185. var requestPathAndQuery = Request.GetEncodedPathAndQuery();
  186. // Bounce them to the startup wizard if it hasn't been completed yet
  187. if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
  188. && !requestPathAndQuery.Contains("wizard", StringComparison.OrdinalIgnoreCase)
  189. && requestPathAndQuery.Contains("index", StringComparison.OrdinalIgnoreCase))
  190. {
  191. return Redirect("index.html?start=wizard#!/wizardstart.html");
  192. }
  193. var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read);
  194. return File(stream, MimeTypes.GetMimeType(path));
  195. }
  196. /// <summary>
  197. /// Gets the favicon.
  198. /// </summary>
  199. /// <response code="200">Favicon.ico returned.</response>
  200. /// <returns>The favicon.</returns>
  201. [HttpGet("favicon.ico")]
  202. [ProducesResponseType(StatusCodes.Status200OK)]
  203. [ApiExplorerSettings(IgnoreApi = true)]
  204. public ActionResult GetFavIcon()
  205. {
  206. return GetWebClientResource("favicon.ico");
  207. }
  208. /// <summary>
  209. /// Gets the path of the directory containing the static web interface content.
  210. /// </summary>
  211. /// <param name="appConfig">The app configuration.</param>
  212. /// <param name="serverConfigManager">The server configuration manager.</param>
  213. /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
  214. public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
  215. {
  216. if (!appConfig.HostWebClient())
  217. {
  218. return null;
  219. }
  220. if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
  221. {
  222. return serverConfigManager.Configuration.DashboardSourcePath;
  223. }
  224. return serverConfigManager.ApplicationPaths.WebPath;
  225. }
  226. private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
  227. {
  228. return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
  229. }
  230. private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
  231. {
  232. if (!(plugin is IHasWebPages hasWebPages))
  233. {
  234. return new List<Tuple<PluginPageInfo, IPlugin>>();
  235. }
  236. return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
  237. }
  238. private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
  239. {
  240. return _appHost.Plugins.SelectMany(GetPluginPages);
  241. }
  242. }
  243. }