Merge branch 'master' into mediaencoding

This commit is contained in:
Bond_009
2019-12-10 23:00:59 +01:00
258 changed files with 3301 additions and 1673 deletions

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Builder;
namespace Jellyfin.Server.Extensions
{
/// <summary>
/// Extensions for adding API specific functionality to the application pipeline.
/// </summary>
public static class ApiApplicationBuilderExtensions
{
/// <summary>
/// Adds swagger and swagger UI to the application pipeline.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder)
{
applicationBuilder.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
return applicationBuilder.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1");
});
}
}
}

View File

@@ -0,0 +1,90 @@
using Jellyfin.Api;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
namespace Jellyfin.Server.Extensions
{
/// <summary>
/// API specific extensions for the service collection.
/// </summary>
public static class ApiServiceCollectionExtensions
{
/// <summary>
/// Adds jellyfin API authorization policies to the DI container.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{
options.AddPolicy(
Policies.RequiresElevation,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new RequiresElevationRequirement());
});
options.AddPolicy(
Policies.FirstTimeSetupOrElevated,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
});
});
}
/// <summary>
/// Adds custom legacy authentication to the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <returns>The updated service collection.</returns>
public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection)
{
return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication)
.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(AuthenticationSchemes.CustomAuthentication, null);
}
/// <summary>
/// Extension method for adding the jellyfin API to the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="baseUrl">The base url for the API.</param>
/// <returns>The MVC builder.</returns>
public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl)
{
return serviceCollection.AddMvc(opts =>
{
opts.UseGeneralRoutePrefix(baseUrl);
})
// Clear app parts to avoid other assemblies being picked up
.ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
.AddApplicationPart(typeof(StartupController).Assembly)
.AddControllersAsServices();
}
/// <summary>
/// Adds Swagger to the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection)
{
return serviceCollection.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
});
}
}
}

View File

@@ -10,6 +10,7 @@
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -20,6 +21,10 @@
<EmbeddedResource Include="Resources/Configuration/*" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" />
@@ -41,6 +46,7 @@
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.1.1" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.2" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>

View File

@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -19,9 +20,12 @@ using Jellyfin.Drawing.Skia;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Globalization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
using SQLitePCL;
@@ -36,7 +40,7 @@ namespace Jellyfin.Server
{
private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static ILogger _logger;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
/// <summary>
@@ -87,6 +91,12 @@ namespace Jellyfin.Server
{
var stopWatch = new Stopwatch();
stopWatch.Start();
// Log all uncaught exceptions to std error
static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject.ToString());
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
ServerApplicationPaths appPaths = CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
@@ -98,6 +108,8 @@ namespace Jellyfin.Server
_logger = _loggerFactory.CreateLogger("Main");
// Log uncaught exceptions to the logging instead of std error
AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
AppDomain.CurrentDomain.UnhandledException += (sender, e)
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
@@ -130,7 +142,7 @@ namespace Jellyfin.Server
_logger.LogInformation(
"Jellyfin version: {Version}",
Assembly.GetEntryAssembly().GetName().Version.ToString(3));
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
@@ -162,7 +174,24 @@ namespace Jellyfin.Server
appConfig);
try
{
await appHost.InitAsync(new ServiceCollection()).ConfigureAwait(false);
ServiceCollection serviceCollection = new ServiceCollection();
await appHost.InitAsync(serviceCollection).ConfigureAwait(false);
var host = CreateWebHostBuilder(appHost, serviceCollection).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();
try
{
await host.StartAsync().ConfigureAwait(false);
}
catch
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again.");
throw;
}
appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager);
@@ -194,6 +223,55 @@ namespace Jellyfin.Server
}
}
private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection)
{
return new WebHostBuilder()
.UseKestrel(options =>
{
var addresses = appHost.ServerConfigurationManager
.Configuration
.LocalNetworkAddresses
.Select(appHost.NormalizeConfiguredLocalAddress)
.Where(i => i != null)
.ToList();
if (addresses.Any())
{
foreach (var address in addresses)
{
_logger.LogInformation("Kestrel listening on {ipaddr}", address);
options.Listen(address, appHost.HttpPort);
if (appHost.EnableHttps && appHost.Certificate != null)
{
options.Listen(
address,
appHost.HttpsPort,
listenOptions => listenOptions.UseHttps(appHost.Certificate));
}
}
}
else
{
_logger.LogInformation("Kestrel listening on all interfaces");
options.ListenAnyIP(appHost.HttpPort);
if (appHost.EnableHttps && appHost.Certificate != null)
{
options.ListenAnyIP(
appHost.HttpsPort,
listenOptions => listenOptions.UseHttps(appHost.Certificate));
}
}
})
.UseContentRoot(appHost.ContentRoot)
.ConfigureServices(services =>
{
// Merge the external ServiceCollection into ASP.NET DI
services.TryAdd(serviceCollection);
})
.UseStartup<Startup>();
}
/// <summary>
/// Create the data, config and log paths from the variety of inputs(command line args,
/// environment variables) or decide on what default to use. For Windows it's %AppPath%
@@ -359,16 +437,25 @@ namespace Jellyfin.Server
private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths)
{
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json");
if (!File.Exists(configPath))
{
// For some reason the csproj name is used instead of the assembly name
using (Stream rscstr = typeof(Program).Assembly
.GetManifestResourceStream("Jellyfin.Server.Resources.Configuration.logging.json"))
using (Stream fstr = File.Open(configPath, FileMode.CreateNew))
using (Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath))
{
await rscstr.CopyToAsync(fstr).ConfigureAwait(false);
if (resource == null)
{
throw new InvalidOperationException(
string.Format(
CultureInfo.InvariantCulture,
"Invalid resource path: '{0}'",
ResourcePath));
}
using Stream dst = File.Open(configPath, FileMode.CreateNew);
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
}
@@ -431,7 +518,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Starting new instance");
string module = options.RestartPath;
var module = options.RestartPath;
if (string.IsNullOrWhiteSpace(module))
{
@@ -439,7 +526,6 @@ namespace Jellyfin.Server
}
string commandLineArgsString;
if (options.RestartArgs != null)
{
commandLineArgsString = options.RestartArgs ?? string.Empty;

View File

@@ -17,6 +17,9 @@
"Args": {
"path": "%JELLYFIN_LOG_DIR%//log_.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 3,
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 100000000,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"
}
}

View File

@@ -0,0 +1,81 @@
using Jellyfin.Server.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Jellyfin.Server
{
/// <summary>
/// Startup configuration for the Kestrel webhost.
/// </summary>
public class Startup
{
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
public Startup(IServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
}
/// <summary>
/// Configures the service collection for the webhost.
/// </summary>
/// <param name="services">The service collection.</param>
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
services.AddHttpContextAccessor();
services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'));
services.AddJellyfinApiSwagger();
// configure custom legacy authentication
services.AddCustomAuthentication();
services.AddJellyfinApiAuthorization();
}
/// <summary>
/// Configures the app builder for the webhost.
/// </summary>
/// <param name="app">The application builder.</param>
/// <param name="env">The webhost environment.</param>
/// <param name="serverApplicationHost">The server application host.</param>
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IServerApplicationHost serverApplicationHost)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseWebSockets();
app.UseResponseCompression();
// TODO app.UseMiddleware<WebSocketMiddleware>();
app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
// TODO use when old API is removed: app.UseAuthentication();
app.UseJellyfinApiSwagger();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Use(serverApplicationHost.ExecuteHttpHandlerAsync);
}
}
}

View File

@@ -13,39 +13,39 @@ namespace Jellyfin.Server
/// </summary>
/// <value>The path to the data directory.</value>
[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 the path to the web directory.
/// </summary>
/// <value>The path to the web directory.</value>
[Option('w', "webdir", Required = false, HelpText = "Path to the Jellyfin web UI resources.")]
public string WebDir { get; set; }
public string? WebDir { get; set; }
/// <summary>
/// Gets or sets the path to the cache directory.
/// </summary>
/// <value>The path to the cache directory.</value>
[Option('C', "cachedir", Required = false, HelpText = "Path to use for caching.")]
public string CacheDir { get; set; }
public string? CacheDir { get; set; }
/// <summary>
/// Gets or sets the path to the config directory.
/// </summary>
/// <value>The path to the config directory.</value>
[Option('c', "configdir", Required = false, HelpText = "Path to use for configuration data (user settings and pictures).")]
public string ConfigDir { get; set; }
public string? ConfigDir { get; set; }
/// <summary>
/// Gets or sets the path to the log directory.
/// </summary>
/// <value>The path to the log directory.</value>
[Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")]
public string LogDir { get; set; }
public string? LogDir { get; set; }
/// <inheritdoc />
[Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")]
public string FFmpegPath { get; set; }
public string? FFmpegPath { get; set; }
/// <inheritdoc />
[Option("service", Required = false, HelpText = "Run as headless service.")]
@@ -57,14 +57,14 @@ namespace Jellyfin.Server
/// <inheritdoc />
[Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")]
public string PackageName { get; set; }
public string? PackageName { get; set; }
/// <inheritdoc />
[Option("restartpath", Required = false, HelpText = "Path to restart script.")]
public string RestartPath { get; set; }
public string? RestartPath { get; set; }
/// <inheritdoc />
[Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
public string RestartArgs { get; set; }
public string? RestartArgs { get; set; }
}
}