aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/CoreAppHost.cs35
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs5
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj11
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs2
-rw-r--r--Jellyfin.Server/Program.cs187
-rw-r--r--Jellyfin.Server/Properties/launchSettings.json17
-rw-r--r--Jellyfin.Server/Startup.cs11
-rw-r--r--Jellyfin.Server/StartupOptions.cs34
8 files changed, 220 insertions, 82 deletions
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 8b4b61e29..f678e714c 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -1,10 +1,13 @@
+using System;
using System.Collections.Generic;
using System.Reflection;
+using Emby.Drawing;
using Emby.Server.Implementations;
+using Jellyfin.Drawing.Skia;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server
@@ -21,30 +24,40 @@ namespace Jellyfin.Server
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="configuration">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
public CoreAppHost(
ServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
StartupOptions options,
IFileSystem fileSystem,
- IImageEncoder imageEncoder,
- INetworkManager networkManager,
- IConfiguration configuration)
+ INetworkManager networkManager)
: base(
applicationPaths,
loggerFactory,
options,
fileSystem,
- imageEncoder,
- networkManager,
- configuration)
+ networkManager)
{
}
- /// <inheritdoc />
- public override bool CanSelfRestart => StartupOptions.RestartPath != null;
+ /// <inheritdoc/>
+ protected override void RegisterServices(IServiceCollection serviceCollection)
+ {
+ // Register an image encoder
+ bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable();
+ Type imageEncoderType = useSkiaEncoder
+ ? typeof(SkiaEncoder)
+ : typeof(NullImageEncoder);
+ serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
+
+ // Log a warning if the Skia encoder could not be used
+ if (!useSkiaEncoder)
+ {
+ Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
+ }
+
+ base.RegisterServices(serviceCollection);
+ }
/// <inheritdoc />
protected override void RestartInternal() => Program.Restart();
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index dd4f9cd23..71ef9a69a 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -71,6 +71,11 @@ namespace Jellyfin.Server.Extensions
// Clear app parts to avoid other assemblies being picked up
.ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
.AddApplicationPart(typeof(StartupController).Assembly)
+ .AddJsonOptions(options =>
+ {
+ // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
+ options.JsonSerializerOptions.PropertyNamingPolicy = null;
+ })
.AddControllersAsServices();
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index bc18f11fd..88114d999 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -1,5 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
+ <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+ <PropertyGroup>
+ <ProjectGuid>{07E39F42-A2C6-4B32-AF8C-725F957A73FF}</ProjectGuid>
+ </PropertyGroup>
+
<PropertyGroup>
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
@@ -36,8 +41,10 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.7.82" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.1" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
+ <PackageReference Include="prometheus-net" Version="3.5.0" />
+ <PackageReference Include="prometheus-net.AspNetCore" Version="3.5.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
index 673f0e415..6f8e4a8ff 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
@@ -1,8 +1,6 @@
using System;
-using System.IO;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 7c3d0f277..9635cc6ec 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -13,20 +12,23 @@ using System.Threading.Tasks;
using CommandLine;
using Emby.Drawing;
using Emby.Server.Implementations;
+using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Networking;
using Jellyfin.Drawing.Skia;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Model.Globalization;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.WebDashboard.Api;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
-using Serilog.Events;
using Serilog.Extensions.Logging;
using SQLitePCL;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -112,10 +114,13 @@ namespace Jellyfin.Server
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
- IConfiguration appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false);
+ await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
- CreateLogger(appConfig, appPaths);
+ // Create an instance of the application configuration to use for application startup
+ IConfiguration startupConfig = CreateAppConfiguration(options, appPaths);
+ // Initialize logging framework
+ InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
// Log uncaught exceptions to the logging instead of std error
@@ -179,24 +184,37 @@ namespace Jellyfin.Server
_loggerFactory,
options,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
- GetImageEncoder(appPaths),
- new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
- appConfig);
+ new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
+
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();
- await appHost.InitAsync(serviceCollection).ConfigureAwait(false);
+ appHost.Init(serviceCollection);
- var host = CreateWebHostBuilder(appHost, serviceCollection).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.
- appHost.ServiceProvider = host.Services;
- appHost.FindParts();
+ // 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;
+ await appHost.InitializeServices().ConfigureAwait(false);
Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try
{
- await host.StartAsync().ConfigureAwait(false);
+ await webHost.StartAsync().ConfigureAwait(false);
}
catch
{
@@ -232,19 +250,30 @@ namespace Jellyfin.Server
}
}
- private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection)
+ private static IWebHostBuilder CreateWebHostBuilder(
+ ApplicationHost appHost,
+ IServiceCollection serviceCollection,
+ StartupOptions commandLineOpts,
+ IConfiguration startupConfig,
+ IApplicationPaths appPaths)
{
return new WebHostBuilder()
- .UseKestrel(options =>
+ .UseKestrel((builderContext, options) =>
{
var addresses = appHost.ServerConfigurationManager
.Configuration
.LocalNetworkAddresses
.Select(appHost.NormalizeConfiguredLocalAddress)
.Where(i => i != null)
- .ToList();
- if (addresses.Any())
+ .ToHashSet();
+ if (addresses.Any() && !addresses.Contains(IPAddress.Any))
{
+ if (!addresses.Contains(IPAddress.Loopback))
+ {
+ // we must listen on loopback for LiveTV to function regardless of the settings
+ addresses.Add(IPAddress.Loopback);
+ }
+
foreach (var address in addresses)
{
_logger.LogInformation("Kestrel listening on {IpAddress}", address);
@@ -252,10 +281,19 @@ namespace Jellyfin.Server
if (appHost.EnableHttps && appHost.Certificate != null)
{
- options.Listen(
- address,
- appHost.HttpsPort,
- listenOptions => listenOptions.UseHttps(appHost.Certificate));
+ options.Listen(address, appHost.HttpsPort, listenOptions =>
+ {
+ listenOptions.UseHttps(appHost.Certificate);
+ listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+ });
+ }
+ else if (builderContext.HostingEnvironment.IsDevelopment())
+ {
+ options.Listen(address, appHost.HttpsPort, listenOptions =>
+ {
+ listenOptions.UseHttps();
+ listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+ });
}
}
}
@@ -266,14 +304,24 @@ namespace Jellyfin.Server
if (appHost.EnableHttps && appHost.Certificate != null)
{
- options.ListenAnyIP(
- appHost.HttpsPort,
- listenOptions => listenOptions.UseHttps(appHost.Certificate));
+ options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
+ {
+ listenOptions.UseHttps(appHost.Certificate);
+ listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+ });
+ }
+ else if (builderContext.HostingEnvironment.IsDevelopment())
+ {
+ options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
+ {
+ listenOptions.UseHttps();
+ listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
+ });
}
}
})
+ .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
.UseSerilog()
- .UseContentRoot(appHost.ContentRoot)
.ConfigureServices(services =>
{
// Merge the external ServiceCollection into ASP.NET DI
@@ -396,9 +444,8 @@ namespace Jellyfin.Server
// webDir
// IF --webdir
// ELSE IF $JELLYFIN_WEB_DIR
- // ELSE use <bindir>/jellyfin-web
+ // ELSE <bindir>/jellyfin-web
var webDir = options.WebDir;
-
if (string.IsNullOrEmpty(webDir))
{
webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
@@ -445,38 +492,63 @@ namespace Jellyfin.Server
return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
}
- private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths)
+ /// <summary>
+ /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
+ /// already.
+ /// </summary>
+ private static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
{
- const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
+ // Do nothing if the config file already exists
string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault);
-
- if (!File.Exists(configPath))
+ if (File.Exists(configPath))
{
- // For some reason the csproj name is used instead of the assembly name
- await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath);
- if (resource == null)
- {
- throw new InvalidOperationException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Invalid resource path: '{0}'",
- ResourcePath));
- }
-
- await using Stream dst = File.Open(configPath, FileMode.CreateNew);
- await resource.CopyToAsync(dst).ConfigureAwait(false);
+ return;
}
+ // Get a stream of the resource contents
+ // NOTE: The .csproj name is used instead of the assembly name in the resource path
+ const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
+ await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
+ ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
+
+ // Copy the resource contents to the expected file path for the config file
+ await using Stream dst = File.Open(configPath, FileMode.CreateNew);
+ await resource.CopyToAsync(dst).ConfigureAwait(false);
+ }
+
+ private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths)
+ {
return new ConfigurationBuilder()
+ .ConfigureAppConfiguration(commandLineOpts, appPaths)
+ .Build();
+ }
+
+ 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
.SetBasePath(appPaths.ConfigurationDirectoryPath)
- .AddInMemoryCollection(ConfigurationOptions.Configuration)
+ .AddInMemoryCollection(inMemoryDefaultConfig)
.AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
.AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
.AddEnvironmentVariables("JELLYFIN_")
- .Build();
+ .AddInMemoryCollection(commandLineOpts.ConvertToConfig());
}
- private static void CreateLogger(IConfiguration configuration, IApplicationPaths appPaths)
+ /// <summary>
+ /// Initialize Serilog using configuration and fall back to defaults on failure.
+ /// </summary>
+ private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
{
try
{
@@ -503,25 +575,6 @@ namespace Jellyfin.Server
}
}
- private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths)
- {
- try
- {
- // Test if the native lib is available
- SkiaEncoder.TestSkia();
-
- return new SkiaEncoder(
- _loggerFactory.CreateLogger<SkiaEncoder>(),
- appPaths);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Skia not available. Will fallback to NullIMageEncoder.");
- }
-
- return new NullImageEncoder();
- }
-
private static void StartNewInstance(StartupOptions options)
{
_logger.LogInformation("Starting new instance");
diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json
new file mode 100644
index 000000000..b6e2bcf97
--- /dev/null
+++ b/Jellyfin.Server/Properties/launchSettings.json
@@ -0,0 +1,17 @@
+{
+ "profiles": {
+ "Jellyfin.Server": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Jellyfin.Server (nowebclient)": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "commandLineArgs": "--nowebclient"
+ }
+ }
+}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 4d7d56e9d..8bcfd1350 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Prometheus;
namespace Jellyfin.Server
{
@@ -69,9 +70,19 @@ namespace Jellyfin.Server
app.UseJellyfinApiSwagger();
app.UseRouting();
app.UseAuthorization();
+ if (_serverConfigurationManager.Configuration.EnableMetrics)
+ {
+ // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad
+ app.UseHttpMetrics();
+ }
+
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
+ if (_serverConfigurationManager.Configuration.EnableMetrics)
+ {
+ endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics");
+ }
});
app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index 1fb1c5af8..6e15d058f 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -1,5 +1,8 @@
+using System.Collections.Generic;
using CommandLine;
using Emby.Server.Implementations;
+using Emby.Server.Implementations.Updates;
+using MediaBrowser.Controller.Extensions;
namespace Jellyfin.Server
{
@@ -16,6 +19,12 @@ namespace Jellyfin.Server
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>
/// Gets or sets the path to the web directory.
/// </summary>
/// <value>The path to the web directory.</value>
@@ -66,5 +75,30 @@ namespace Jellyfin.Server
/// <inheritdoc />
[Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
public string? RestartArgs { get; set; }
+
+ /// <inheritdoc />
+ [Option("plugin-manifest-url", Required = false, HelpText = "A custom URL for the plugin repository JSON manifest")]
+ public string? PluginManifestUrl { 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 (PluginManifestUrl != null)
+ {
+ config.Add(InstallationManager.PluginManifestUrlKey, PluginManifestUrl);
+ }
+
+ if (NoWebClient)
+ {
+ config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString);
+ }
+
+ return config;
+ }
}
}