Merge pull request #2601 from mark-monteiro/support-running-without-web-content

Support Running Server Without Web Content
This commit is contained in:
Vasily 2020-04-01 23:38:43 +03:00 committed by GitHub
commit 07ea120ba9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 236 additions and 105 deletions

View File

@ -5,7 +5,7 @@ using MediaBrowser.Common.Configuration;
namespace Emby.Server.Implementations.AppBase namespace Emby.Server.Implementations.AppBase
{ {
/// <summary> /// <summary>
/// Provides a base class to hold common application paths used by both the Ui and Server. /// Provides a base class to hold common application paths used by both the UI and Server.
/// This can be subclassed to add application-specific paths. /// This can be subclassed to add application-specific paths.
/// </summary> /// </summary>
public abstract class BaseApplicationPaths : IApplicationPaths public abstract class BaseApplicationPaths : IApplicationPaths
@ -37,10 +37,7 @@ namespace Emby.Server.Implementations.AppBase
/// <value>The program data path.</value> /// <value>The program data path.</value>
public string ProgramDataPath { get; } public string ProgramDataPath { get; }
/// <summary> /// <inheritdoc/>
/// Gets the path to the web UI resources folder.
/// </summary>
/// <value>The web UI resources path.</value>
public string WebPath { get; } public string WebPath { get; }
/// <summary> /// <summary>

View File

@ -235,11 +235,6 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
public int HttpsPort { get; private set; } public int HttpsPort { get; private set; }
/// <summary>
/// Gets the content root for the webhost.
/// </summary>
public string ContentRoot { get; private set; }
/// <summary> /// <summary>
/// Gets the server configuration manager. /// Gets the server configuration manager.
/// </summary> /// </summary>
@ -612,13 +607,7 @@ namespace Emby.Server.Implementations
DiscoverTypes(); DiscoverTypes();
await RegisterResources(serviceCollection, startupConfig).ConfigureAwait(false); await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false);
ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath;
if (string.IsNullOrEmpty(ContentRoot))
{
ContentRoot = ServerConfigurationManager.ApplicationPaths.WebPath;
}
} }
public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next) public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
@ -649,9 +638,9 @@ namespace Emby.Server.Implementations
} }
/// <summary> /// <summary>
/// Registers resources that classes will depend on /// Registers services/resources with the service collection that will be available via DI.
/// </summary> /// </summary>
protected async Task RegisterResources(IServiceCollection serviceCollection, IConfiguration startupConfig) protected async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig)
{ {
serviceCollection.AddMemoryCache(); serviceCollection.AddMemoryCache();
@ -769,20 +758,8 @@ namespace Emby.Server.Implementations
CertificateInfo = GetCertificateInfo(true); CertificateInfo = GetCertificateInfo(true);
Certificate = GetCertificate(CertificateInfo); Certificate = GetCertificate(CertificateInfo);
HttpServer = new HttpListenerHost( serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
this, serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
LoggerFactory.CreateLogger<HttpListenerHost>(),
ServerConfigurationManager,
startupConfig,
NetworkManager,
JsonSerializer,
XmlSerializer,
CreateHttpListener())
{
GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading")
};
serviceCollection.AddSingleton(HttpServer);
ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
serviceCollection.AddSingleton(ImageProcessor); serviceCollection.AddSingleton(ImageProcessor);
@ -895,6 +872,14 @@ namespace Emby.Server.Implementations
((LibraryManager)LibraryManager).ItemRepository = ItemRepository; ((LibraryManager)LibraryManager).ItemRepository = ItemRepository;
} }
/// <summary>
/// Create services registered with the service container that need to be initialized at application startup.
/// </summary>
public void InitializeServices()
{
HttpServer = Resolve<IHttpServer>();
}
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
{ {
// Distinct these to prevent users from reporting problems that aren't actually problems // Distinct these to prevent users from reporting problems that aren't actually problems
@ -1212,8 +1197,6 @@ namespace Emby.Server.Implementations
}); });
} }
protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(LoggerFactory.CreateLogger<WebSocketSharpListener>());
private CertificateInfo GetCertificateInfo(bool generateCertificate) private CertificateInfo GetCertificateInfo(bool generateCertificate)
{ {
// Custom cert // Custom cert

View File

@ -1,51 +1,48 @@
using System; using System;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Browser namespace Emby.Server.Implementations.Browser
{ {
/// <summary> /// <summary>
/// Class BrowserLauncher. /// Assists in opening application URLs in an external browser.
/// </summary> /// </summary>
public static class BrowserLauncher public static class BrowserLauncher
{ {
/// <summary> /// <summary>
/// Opens the dashboard page. /// Opens the home page of the web client.
/// </summary>
/// <param name="page">The page.</param>
/// <param name="appHost">The app host.</param>
private static void OpenDashboardPage(string page, IServerApplicationHost appHost)
{
var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page;
OpenUrl(appHost, url);
}
/// <summary>
/// Opens the web client.
/// </summary> /// </summary>
/// <param name="appHost">The app host.</param> /// <param name="appHost">The app host.</param>
public static void OpenWebApp(IServerApplicationHost appHost) public static void OpenWebApp(IServerApplicationHost appHost)
{ {
OpenDashboardPage("index.html", appHost); TryOpenUrl(appHost, "/web/index.html");
} }
/// <summary> /// <summary>
/// Opens the URL. /// Opens the swagger API page.
/// </summary> /// </summary>
/// <param name="appHost">The application host instance.</param> /// <param name="appHost">The app host.</param>
public static void OpenSwaggerPage(IServerApplicationHost appHost)
{
TryOpenUrl(appHost, "/swagger/index.html");
}
/// <summary>
/// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
/// </summary>
/// <param name="appHost">The application host.</param>
/// <param name="url">The URL.</param> /// <param name="url">The URL.</param>
private static void OpenUrl(IServerApplicationHost appHost, string url) private static void TryOpenUrl(IServerApplicationHost appHost, string url)
{ {
try try
{ {
appHost.LaunchUrl(url); string baseUrl = appHost.GetLocalApiUrl("localhost");
appHost.LaunchUrl(baseUrl + url);
} }
catch (NotSupportedException) catch (Exception ex)
{
}
catch (Exception)
{ {
var logger = appHost.Resolve<ILogger>();
logger?.LogError(ex, "Failed to open browser window with URL {URL}", url);
} }
} }
} }

View File

@ -1,13 +1,22 @@
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Providers.Music;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations namespace Emby.Server.Implementations
{ {
/// <summary>
/// Static class containing the default configuration options for the web server.
/// </summary>
public static class ConfigurationOptions public static class ConfigurationOptions
{ {
public static Dictionary<string, string> Configuration => new Dictionary<string, string> /// <summary>
/// Gets a new copy of the default configuration options.
/// </summary>
public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
{ {
{ "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, { HostWebClientKey, bool.TrueString },
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" }, { FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" }, { FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString } { PlaylistsAllowDuplicatesKey, bool.TrueString }

View File

@ -2,7 +2,9 @@ using System.Threading.Tasks;
using Emby.Server.Implementations.Browser; using Emby.Server.Implementations.Browser;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Configuration;
namespace Emby.Server.Implementations.EntryPoints namespace Emby.Server.Implementations.EntryPoints
{ {
@ -11,10 +13,8 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary> /// </summary>
public sealed class StartupWizard : IServerEntryPoint public sealed class StartupWizard : IServerEntryPoint
{ {
/// <summary>
/// The app host.
/// </summary>
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _appConfig;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
/// <summary> /// <summary>
@ -22,9 +22,10 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary> /// </summary>
/// <param name="appHost">The application host.</param> /// <param name="appHost">The application host.</param>
/// <param name="config">The configuration manager.</param> /// <param name="config">The configuration manager.</param>
public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config) public StartupWizard(IServerApplicationHost appHost, IConfiguration appConfig, IServerConfigurationManager config)
{ {
_appHost = appHost; _appHost = appHost;
_appConfig = appConfig;
_config = config; _config = config;
} }
@ -36,7 +37,11 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask; return Task.CompletedTask;
} }
if (!_config.Configuration.IsStartupWizardCompleted) if (!_appConfig.HostWebClient())
{
BrowserLauncher.OpenSwaggerPage(_appHost);
}
else if (!_config.Configuration.IsStartupWizardCompleted)
{ {
BrowserLauncher.OpenWebApp(_appHost); BrowserLauncher.OpenWebApp(_appHost);
} }

View File

@ -17,6 +17,7 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -29,6 +30,12 @@ namespace Emby.Server.Implementations.HttpServer
{ {
public class HttpListenerHost : IHttpServer, IDisposable public class HttpListenerHost : IHttpServer, IDisposable
{ {
/// <summary>
/// The key for a setting that specifies the default redirect path
/// to use for requests where the URL base prefix is invalid or missing.
/// </summary>
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
@ -52,12 +59,13 @@ namespace Emby.Server.Implementations.HttpServer
INetworkManager networkManager, INetworkManager networkManager,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer, IXmlSerializer xmlSerializer,
IHttpListener socketListener) IHttpListener socketListener,
ILocalizationManager localizationManager)
{ {
_appHost = applicationHost; _appHost = applicationHost;
_logger = logger; _logger = logger;
_config = config; _config = config;
_defaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; _defaultRedirectPath = configuration[DefaultRedirectKey];
_baseUrlPrefix = _config.Configuration.BaseUrl; _baseUrlPrefix = _config.Configuration.BaseUrl;
_networkManager = networkManager; _networkManager = networkManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
@ -69,6 +77,7 @@ namespace Emby.Server.Implementations.HttpServer
Instance = this; Instance = this;
ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>(); ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
} }
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;

View File

@ -13,12 +13,14 @@ using System.Threading.Tasks;
using CommandLine; using CommandLine;
using Emby.Drawing; using Emby.Drawing;
using Emby.Server.Implementations; using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO; using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking; using Emby.Server.Implementations.Networking;
using Jellyfin.Drawing.Skia; using Jellyfin.Drawing.Skia;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Globalization; using MediaBrowser.Controller.Extensions;
using MediaBrowser.WebDashboard.Api;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -112,9 +114,10 @@ namespace Jellyfin.Server
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
// Create an instance of the application configuration to use for application startup
await InitLoggingConfigFile(appPaths).ConfigureAwait(false); await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
IConfiguration startupConfig = CreateAppConfiguration(appPaths);
// Create an instance of the application configuration to use for application startup
IConfiguration startupConfig = CreateAppConfiguration(options, appPaths);
// Initialize logging framework // Initialize logging framework
InitializeLoggingFramework(startupConfig, appPaths); InitializeLoggingFramework(startupConfig, appPaths);
@ -183,15 +186,31 @@ namespace Jellyfin.Server
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
GetImageEncoder(appPaths), GetImageEncoder(appPaths),
new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>())); new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
try try
{ {
// If hosting the web client, validate the client content path
if (startupConfig.HostWebClient())
{
string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager);
if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
{
throw new InvalidOperationException(
"The server is expected to host the web client, but the provided content directory is either " +
$"invalid or empty: {webContentPath}. If you do not want to host the web client with the " +
"server, you may set the '--nowebclient' command line flag, or set" +
$"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings.");
}
}
ServiceCollection serviceCollection = new ServiceCollection(); ServiceCollection serviceCollection = new ServiceCollection();
await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false);
var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build(); var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
// A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = webHost.Services; appHost.ServiceProvider = webHost.Services;
appHost.InitializeServices();
appHost.FindParts(); appHost.FindParts();
Migrations.MigrationRunner.Run(appHost, _loggerFactory); Migrations.MigrationRunner.Run(appHost, _loggerFactory);
@ -233,7 +252,12 @@ namespace Jellyfin.Server
} }
} }
private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths) private static IWebHostBuilder CreateWebHostBuilder(
ApplicationHost appHost,
IServiceCollection serviceCollection,
StartupOptions commandLineOpts,
IConfiguration startupConfig,
IApplicationPaths appPaths)
{ {
return new WebHostBuilder() return new WebHostBuilder()
.UseKestrel(options => .UseKestrel(options =>
@ -273,9 +297,8 @@ namespace Jellyfin.Server
} }
} }
}) })
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
.UseSerilog() .UseSerilog()
.UseContentRoot(appHost.ContentRoot)
.ConfigureServices(services => .ConfigureServices(services =>
{ {
// Merge the external ServiceCollection into ASP.NET DI // Merge the external ServiceCollection into ASP.NET DI
@ -398,9 +421,8 @@ namespace Jellyfin.Server
// webDir // webDir
// IF --webdir // IF --webdir
// ELSE IF $JELLYFIN_WEB_DIR // ELSE IF $JELLYFIN_WEB_DIR
// ELSE use <bindir>/jellyfin-web // ELSE <bindir>/jellyfin-web
var webDir = options.WebDir; var webDir = options.WebDir;
if (string.IsNullOrEmpty(webDir)) if (string.IsNullOrEmpty(webDir))
{ {
webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
@ -471,21 +493,33 @@ namespace Jellyfin.Server
await resource.CopyToAsync(dst).ConfigureAwait(false); await resource.CopyToAsync(dst).ConfigureAwait(false);
} }
private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths) private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths)
{ {
return new ConfigurationBuilder() return new ConfigurationBuilder()
.ConfigureAppConfiguration(appPaths) .ConfigureAppConfiguration(commandLineOpts, appPaths)
.Build(); .Build();
} }
private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths) private static IConfigurationBuilder ConfigureAppConfiguration(
this IConfigurationBuilder config,
StartupOptions commandLineOpts,
IApplicationPaths appPaths,
IConfiguration? startupConfig = null)
{ {
// Use the swagger API page as the default redirect path if not hosting the web client
var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
if (startupConfig != null && !startupConfig.HostWebClient())
{
inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html";
}
return config return config
.SetBasePath(appPaths.ConfigurationDirectoryPath) .SetBasePath(appPaths.ConfigurationDirectoryPath)
.AddInMemoryCollection(ConfigurationOptions.Configuration) .AddInMemoryCollection(inMemoryDefaultConfig)
.AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
.AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
.AddEnvironmentVariables("JELLYFIN_"); .AddEnvironmentVariables("JELLYFIN_")
.AddInMemoryCollection(commandLineOpts.ConvertToConfig());
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,11 @@
{
"profiles": {
"Jellyfin.Server": {
"commandName": "Project"
},
"Jellyfin.Server (nowebclient)": {
"commandName": "Project",
"commandLineArgs": "--nowebclient"
}
}
}

View File

@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Globalization;
using CommandLine; using CommandLine;
using Emby.Server.Implementations; using Emby.Server.Implementations;
using MediaBrowser.Controller.Extensions;
namespace Jellyfin.Server namespace Jellyfin.Server
{ {
@ -15,6 +18,12 @@ namespace Jellyfin.Server
[Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")] [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")]
public string? DataDir { get; set; } public string? DataDir { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the server should not host the web client.
/// </summary>
[Option("nowebclient", Required = false, HelpText = "Indicates that the web server should not host the web client.")]
public bool NoWebClient { get; set; }
/// <summary> /// <summary>
/// Gets or sets the path to the web directory. /// Gets or sets the path to the web directory.
/// </summary> /// </summary>
@ -66,5 +75,21 @@ namespace Jellyfin.Server
/// <inheritdoc /> /// <inheritdoc />
[Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
public string? RestartArgs { get; set; } public string? RestartArgs { get; set; }
/// <summary>
/// Gets the command line options as a dictionary that can be used in the .NET configuration system.
/// </summary>
/// <returns>The configuration dictionary.</returns>
public Dictionary<string, string> ConvertToConfig()
{
var config = new Dictionary<string, string>();
if (NoWebClient)
{
config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString);
}
return config;
}
} }
} }

View File

@ -1,3 +1,5 @@
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Common.Configuration namespace MediaBrowser.Common.Configuration
{ {
/// <summary> /// <summary>
@ -12,9 +14,12 @@ namespace MediaBrowser.Common.Configuration
string ProgramDataPath { get; } string ProgramDataPath { get; }
/// <summary> /// <summary>
/// Gets the path to the web UI resources folder /// Gets the path to the web UI resources folder.
/// </summary> /// </summary>
/// <value>The web UI resources path.</value> /// <remarks>
/// This value is not relevant if the server is configured to not host any static web content. Additionally,
/// the value for <see cref="ServerConfiguration.DashboardSourcePath"/> takes precedence over this one.
/// </remarks>
string WebPath { get; } string WebPath { get; }
/// <summary> /// <summary>

View File

@ -1,3 +1,4 @@
using System;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace MediaBrowser.Controller.Extensions namespace MediaBrowser.Controller.Extensions
@ -7,6 +8,11 @@ namespace MediaBrowser.Controller.Extensions
/// </summary> /// </summary>
public static class ConfigurationExtensions public static class ConfigurationExtensions
{ {
/// <summary>
/// The key for a setting that indicates whether the application should host web client content.
/// </summary>
public const string HostWebClientKey = "hostwebclient";
/// <summary> /// <summary>
/// The key for the FFmpeg probe size option. /// The key for the FFmpeg probe size option.
/// </summary> /// </summary>
@ -22,6 +28,15 @@ namespace MediaBrowser.Controller.Extensions
/// </summary> /// </summary>
public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
/// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
/// <returns>The parsed config value.</returns>
/// <exception cref="FormatException">The config value is not a valid bool string. See <see cref="bool.Parse(string)"/>.</exception>
public static bool HostWebClient(this IConfiguration configuration)
=> configuration.GetValue<bool>(HostWebClientKey);
/// <summary> /// <summary>
/// Gets the FFmpeg probe size from the <see cref="IConfiguration" />. /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />.
/// </summary> /// </summary>

View File

@ -82,6 +82,11 @@ namespace MediaBrowser.Controller
/// <returns>The local API URL.</returns> /// <returns>The local API URL.</returns>
string GetLocalApiUrl(IPAddress address); string GetLocalApiUrl(IPAddress address);
/// <summary>
/// Open a URL in an external browser window.
/// </summary>
/// <param name="url">The URL to open.</param>
/// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception>
void LaunchUrl(string url); void LaunchUrl(string url);
void EnableLoopback(string appName); void EnableLoopback(string appName);

View File

@ -148,9 +148,9 @@ namespace MediaBrowser.Model.Configuration
public bool EnableDashboardResponseCaching { get; set; } public bool EnableDashboardResponseCaching { get; set; }
/// <summary> /// <summary>
/// Allows the dashboard to be served from a custom path. /// Gets or sets a custom path to serve the dashboard from.
/// </summary> /// </summary>
/// <value>The dashboard source path.</value> /// <value>The dashboard source path, or null if the default path should be used.</value>
public string DashboardSourcePath { get; set; } public string DashboardSourcePath { get; set; }
/// <summary> /// <summary>

View File

@ -12,12 +12,14 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace MediaBrowser.WebDashboard.Api namespace MediaBrowser.WebDashboard.Api
@ -102,6 +104,7 @@ namespace MediaBrowser.WebDashboard.Api
/// <value>The HTTP result factory.</value> /// <value>The HTTP result factory.</value>
private readonly IHttpResultFactory _resultFactory; private readonly IHttpResultFactory _resultFactory;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _appConfig;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IResourceFileManager _resourceFileManager; private readonly IResourceFileManager _resourceFileManager;
@ -111,6 +114,7 @@ namespace MediaBrowser.WebDashboard.Api
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="appHost">The application host.</param> /// <param name="appHost">The application host.</param>
/// <param name="appConfig">The application configuration.</param>
/// <param name="resourceFileManager">The resource file manager.</param> /// <param name="resourceFileManager">The resource file manager.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param> /// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <param name="fileSystem">The file system.</param> /// <param name="fileSystem">The file system.</param>
@ -118,6 +122,7 @@ namespace MediaBrowser.WebDashboard.Api
public DashboardService( public DashboardService(
ILogger<DashboardService> logger, ILogger<DashboardService> logger,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IConfiguration appConfig,
IResourceFileManager resourceFileManager, IResourceFileManager resourceFileManager,
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
IFileSystem fileSystem, IFileSystem fileSystem,
@ -125,6 +130,7 @@ namespace MediaBrowser.WebDashboard.Api
{ {
_logger = logger; _logger = logger;
_appHost = appHost; _appHost = appHost;
_appConfig = appConfig;
_resourceFileManager = resourceFileManager; _resourceFileManager = resourceFileManager;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
@ -138,20 +144,30 @@ namespace MediaBrowser.WebDashboard.Api
public IRequest Request { get; set; } public IRequest Request { get; set; }
/// <summary> /// <summary>
/// Gets the path for the web interface. /// Gets the path of the directory containing the static web interface content, or null if the server is not
/// hosting the web client.
/// </summary> /// </summary>
/// <value>The path for the web interface.</value> public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager);
public string DashboardUIPath
/// <summary>
/// Gets the path of the directory containing the static web interface content.
/// </summary>
/// <param name="appConfig">The app configuration.</param>
/// <param name="serverConfigManager">The server configuration manager.</param>
/// <returns>The directory path, or null if the server is not hosting the web client.</returns>
public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
{ {
get if (!appConfig.HostWebClient())
{ {
if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath)) return null;
{
return _serverConfigurationManager.Configuration.DashboardSourcePath;
} }
return _serverConfigurationManager.ApplicationPaths.WebPath; if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
{
return serverConfigManager.Configuration.DashboardSourcePath;
} }
return serverConfigManager.ApplicationPaths.WebPath;
} }
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
@ -209,7 +225,7 @@ namespace MediaBrowser.WebDashboard.Api
return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
} }
return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersionString, null)); return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => PackageCreator.ModifyHtml(false, stream, null, _appHost.ApplicationVersionString, null));
} }
throw new ResourceNotFoundException(); throw new ResourceNotFoundException();
@ -307,6 +323,11 @@ namespace MediaBrowser.WebDashboard.Api
/// <returns>System.Object.</returns> /// <returns>System.Object.</returns>
public async Task<object> Get(GetDashboardResource request) public async Task<object> Get(GetDashboardResource request)
{ {
if (!_appConfig.HostWebClient() || DashboardUIPath == null)
{
throw new ResourceNotFoundException();
}
var path = request.ResourceName; var path = request.ResourceName;
var contentType = MimeTypes.GetMimeType(path); var contentType = MimeTypes.GetMimeType(path);
@ -378,6 +399,11 @@ namespace MediaBrowser.WebDashboard.Api
public async Task<object> Get(GetDashboardPackage request) public async Task<object> Get(GetDashboardPackage request)
{ {
if (!_appConfig.HostWebClient() || DashboardUIPath == null)
{
throw new ResourceNotFoundException();
}
var mode = request.Mode; var mode = request.Mode;
var inputPath = string.IsNullOrWhiteSpace(mode) ? var inputPath = string.IsNullOrWhiteSpace(mode) ?

View File

@ -31,7 +31,8 @@ namespace MediaBrowser.WebDashboard.Api
if (resourceStream != null && IsCoreHtml(virtualPath)) if (resourceStream != null && IsCoreHtml(virtualPath))
{ {
resourceStream = await ModifyHtml(virtualPath, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); bool isMainIndexPage = string.Equals(virtualPath, "index.html", StringComparison.OrdinalIgnoreCase);
resourceStream = await ModifyHtml(isMainIndexPage, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false);
} }
return resourceStream; return resourceStream;
@ -47,16 +48,25 @@ namespace MediaBrowser.WebDashboard.Api
return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase); return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase);
} }
// Modifies the HTML by adding common meta tags, css and js. /// <summary>
public async Task<Stream> ModifyHtml( /// Modifies the source HTML stream by adding common meta tags, css and js.
string path, /// </summary>
/// <param name="isMainIndexPage">True if the stream contains content for the main index page.</param>
/// <param name="sourceStream">The stream whose content should be modified.</param>
/// <param name="mode">The client mode ('cordova', 'android', etc).</param>
/// <param name="appVersion">The application version.</param>
/// <param name="localizationCulture">The localization culture.</param>
/// <returns>
/// A task that represents the async operation to read and modify the input stream.
/// The task result contains a stream containing the modified HTML content.
/// </returns>
public static async Task<Stream> ModifyHtml(
bool isMainIndexPage,
Stream sourceStream, Stream sourceStream,
string mode, string mode,
string appVersion, string appVersion,
string localizationCulture) string localizationCulture)
{ {
var isMainIndexPage = string.Equals(path, "index.html", StringComparison.OrdinalIgnoreCase);
string html; string html;
using (var reader = new StreamReader(sourceStream, Encoding.UTF8)) using (var reader = new StreamReader(sourceStream, Encoding.UTF8))
{ {