diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 7c2ea50e2..8517abed6 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -20,16 +20,14 @@ namespace Emby.Server.Implementations.IO protected ILogger Logger; private readonly List _shortcutHandlers = new List(); - private readonly string _tempPath; - private readonly bool _isEnvironmentCaseInsensitive; public ManagedFileSystem( - ILoggerFactory loggerFactory, + ILogger logger, IApplicationPaths applicationPaths) { - Logger = loggerFactory.CreateLogger("FileSystem"); + Logger = logger; _tempPath = applicationPaths.TempDirectory; _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 5060476ba..f6c120801 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,14 +2,11 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Reflection; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; using SkiaSharp; @@ -18,24 +15,28 @@ namespace Jellyfin.Drawing.Skia public class SkiaEncoder : IImageEncoder { private readonly ILogger _logger; - private static IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private static ILocalizationManager _localizationManager; + private readonly IApplicationPaths _appPaths; + private readonly ILocalizationManager _localizationManager; + + private static readonly HashSet _transparentImageTypes + = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; public SkiaEncoder( - ILoggerFactory loggerFactory, + ILogger logger, IApplicationPaths appPaths, - IFileSystem fileSystem, ILocalizationManager localizationManager) { - _logger = loggerFactory.CreateLogger("ImageEncoder"); + _logger = logger; _appPaths = appPaths; - _fileSystem = fileSystem; _localizationManager = localizationManager; - - LogVersion(); } + public string Name => "Skia"; + + public bool SupportsImageCollageCreation => true; + + public bool SupportsImageEncoding => true; + public IReadOnlyCollection SupportedInputFormats => new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -66,17 +67,15 @@ namespace Jellyfin.Drawing.Skia public IReadOnlyCollection SupportedOutputFormats => new HashSet() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; - private void LogVersion() + /// + /// Test to determine if the native lib is available + /// + public static void TestSkia() { // test an operation that requires the native library SKPMColor.PreMultiply(SKColors.Black); - - _logger.LogInformation("SkiaSharp version: " + GetVersion()); } - public static Version GetVersion() - => typeof(SKBitmap).GetTypeInfo().Assembly.GetName().Version; - private static bool IsTransparent(SKColor color) => (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0; @@ -106,6 +105,7 @@ namespace Jellyfin.Drawing.Skia return false; } } + return true; } @@ -118,6 +118,7 @@ namespace Jellyfin.Drawing.Skia return false; } } + return true; } @@ -197,7 +198,7 @@ namespace Jellyfin.Drawing.Skia private static bool HasDiacritics(string text) => !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal); - private static bool RequiresSpecialCharacterHack(string path) + private bool RequiresSpecialCharacterHack(string path) { if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter)) { @@ -212,7 +213,7 @@ namespace Jellyfin.Drawing.Skia return false; } - private static string NormalizePath(string path, IFileSystem fileSystem) + private string NormalizePath(string path) { if (!RequiresSpecialCharacterHack(path)) { @@ -255,21 +256,18 @@ namespace Jellyfin.Drawing.Skia } } - private static readonly HashSet TransparentImageTypes - = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; - - internal static SKBitmap Decode(string path, bool forceCleanBitmap, IFileSystem fileSystem, ImageOrientation? orientation, out SKEncodedOrigin origin) + internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) { if (!File.Exists(path)) { throw new FileNotFoundException("File not found", path); } - var requiresTransparencyHack = TransparentImageTypes.Contains(Path.GetExtension(path)); + var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path)); if (requiresTransparencyHack || forceCleanBitmap) { - using (var stream = new SKFileStream(NormalizePath(path, fileSystem))) + using (var stream = new SKFileStream(NormalizePath(path))) using (var codec = SKCodec.Create(stream)) { if (codec == null) @@ -290,11 +288,11 @@ namespace Jellyfin.Drawing.Skia } } - var resultBitmap = SKBitmap.Decode(NormalizePath(path, fileSystem)); + var resultBitmap = SKBitmap.Decode(NormalizePath(path)); if (resultBitmap == null) { - return Decode(path, true, fileSystem, orientation, out origin); + return Decode(path, true, orientation, out origin); } // If we have to resize these they often end up distorted @@ -302,7 +300,7 @@ namespace Jellyfin.Drawing.Skia { using (resultBitmap) { - return Decode(path, true, fileSystem, orientation, out origin); + return Decode(path, true, orientation, out origin); } } @@ -314,13 +312,13 @@ namespace Jellyfin.Drawing.Skia { if (cropWhitespace) { - using (var bitmap = Decode(path, forceAnalyzeBitmap, _fileSystem, orientation, out origin)) + using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin)) { return CropWhiteSpace(bitmap); } } - return Decode(path, forceAnalyzeBitmap, _fileSystem, orientation, out origin); + return Decode(path, forceAnalyzeBitmap, orientation, out origin); } private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation) @@ -607,16 +605,16 @@ namespace Jellyfin.Drawing.Skia if (ratio >= 1.4) { - new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); } else if (ratio >= .9) { - new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); } else { // TODO: Create Poster collage capability - new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); } } @@ -645,11 +643,5 @@ namespace Jellyfin.Drawing.Skia _logger.LogError(ex, "Error drawing indicator overlay"); } } - - public string Name => "Skia"; - - public bool SupportsImageCollageCreation => true; - - public bool SupportsImageEncoding => true; } } diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 7d404ce64..1f2a6e81a 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -1,21 +1,17 @@ using System; using System.Collections.Generic; using System.IO; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.IO; using SkiaSharp; namespace Jellyfin.Drawing.Skia { public class StripCollageBuilder { - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; + private readonly SkiaEncoder _skiaEncoder; - public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem) + public StripCollageBuilder(SkiaEncoder skiaEncoder) { - _appPaths = appPaths; - _fileSystem = fileSystem; + _skiaEncoder = skiaEncoder; } public static SKEncodedImageFormat GetEncodedFormat(string outputPath) @@ -25,19 +21,28 @@ namespace Jellyfin.Drawing.Skia throw new ArgumentNullException(nameof(outputPath)); } - var ext = Path.GetExtension(outputPath).ToLowerInvariant(); + var ext = Path.GetExtension(outputPath); - if (ext == ".jpg" || ext == ".jpeg") + if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) + || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase)) + { return SKEncodedImageFormat.Jpeg; + } - if (ext == ".webp") + if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) + { return SKEncodedImageFormat.Webp; + } - if (ext == ".gif") + if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) + { return SKEncodedImageFormat.Gif; + } - if (ext == ".bmp") + if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) + { return SKEncodedImageFormat.Bmp; + } // default to png return SKEncodedImageFormat.Png; @@ -47,25 +52,19 @@ namespace Jellyfin.Drawing.Skia { using (var bitmap = BuildSquareCollageBitmap(paths, width, height)) using (var outputStream = new SKFileWStream(outputPath)) + using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels())) { - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels())) - { - pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); - } + pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); } } public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) { using (var bitmap = BuildThumbCollageBitmap(paths, width, height)) + using (var outputStream = new SKFileWStream(outputPath)) + using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels())) { - using (var outputStream = new SKFileWStream(outputPath)) - { - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels())) - { - pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); - } - } + pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); } } @@ -127,7 +126,7 @@ namespace Jellyfin.Drawing.Skia currentIndex = 0; } - bitmap = SkiaEncoder.Decode(paths[currentIndex], false, _fileSystem, null, out var origin); + bitmap = _skiaEncoder.Decode(paths[currentIndex], false, null, out var origin); imagesTested[currentIndex] = 0; @@ -156,7 +155,6 @@ namespace Jellyfin.Drawing.Skia { for (var y = 0; y < 2; y++) { - using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) { imageIndex = newIndex; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 049dd761b..91752a16d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -34,7 +34,6 @@ namespace Jellyfin.Server private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static ILogger _logger; private static bool _restartOnShutdown; - private static IConfiguration appConfig; public static Task Main(string[] args) { @@ -76,7 +75,7 @@ namespace Jellyfin.Server // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); - appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false); + IConfiguration appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false); CreateLogger(appConfig, appPaths); @@ -116,8 +115,6 @@ namespace Jellyfin.Server ApplicationHost.LogEnvironmentInfo(_logger, appPaths); - SQLitePCL.Batteries_V2.Init(); - // Increase the max http request limit // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); @@ -129,20 +126,20 @@ namespace Jellyfin.Server ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); #pragma warning restore CA5359 - var fileSystem = new ManagedFileSystem(_loggerFactory, appPaths); + SQLitePCL.Batteries_V2.Init(); using (var appHost = new CoreAppHost( appPaths, _loggerFactory, options, - fileSystem, + new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths), new NullImageEncoder(), new NetworkManager(_loggerFactory), appConfig)) { await appHost.InitAsync(new ServiceCollection()).ConfigureAwait(false); - appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager); + appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager); await appHost.RunStartupTasksAsync().ConfigureAwait(false); @@ -165,7 +162,7 @@ namespace Jellyfin.Server /// /// 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% + /// environment variables) or decide on what default to use. For Windows it's %AppPath% /// for everything else the XDG approach is followed: /// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html /// @@ -187,7 +184,9 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(dataDir)) { // LocalApplicationData follows the XDG spec on unix machines - dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "jellyfin"); + dataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "jellyfin"); } } @@ -206,20 +205,26 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(configDir)) { - if (options.DataDir != null || Directory.Exists(Path.Combine(dataDir, "config")) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (options.DataDir != null + || Directory.Exists(Path.Combine(dataDir, "config")) + || RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Hang config folder off already set dataDir configDir = Path.Combine(dataDir, "config"); } else { - // $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored. + // $XDG_CONFIG_HOME defines the base directory relative to which + // user specific configuration files should be stored. configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); - // If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME /.config should be used. + // If $XDG_CONFIG_HOME is either not set or empty, + // a default equal to $HOME /.config should be used. if (string.IsNullOrEmpty(configDir)) { - configDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); + configDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config"); } configDir = Path.Combine(configDir, "jellyfin"); @@ -247,13 +252,17 @@ namespace Jellyfin.Server } else { - // $XDG_CACHE_HOME defines the base directory relative to which user specific non-essential data files should be stored. + // $XDG_CACHE_HOME defines the base directory relative to which + // user specific non-essential data files should be stored. cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); - // If $XDG_CACHE_HOME is either not set or empty, a default equal to $HOME/.cache should be used. + // If $XDG_CACHE_HOME is either not set or empty, + // a default equal to $HOME/.cache should be used. if (string.IsNullOrEmpty(cacheDir)) { - cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache"); + cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".cache"); } cacheDir = Path.Combine(cacheDir, "jellyfin"); @@ -362,17 +371,22 @@ namespace Jellyfin.Server } private static IImageEncoder GetImageEncoder( - IFileSystem fileSystem, IApplicationPaths appPaths, ILocalizationManager localizationManager) { try { - return new SkiaEncoder(_loggerFactory, appPaths, fileSystem, localizationManager); + // Test if the native lib is available + SkiaEncoder.TestSkia(); + + return new SkiaEncoder( + _loggerFactory.CreateLogger(), + appPaths, + localizationManager); } catch (Exception ex) { - _logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder."); + _logger.LogWarning(ex, "Skia not available. Will fallback to NullIMageEncoder."); } return new NullImageEncoder(); @@ -386,7 +400,7 @@ namespace Jellyfin.Server if (string.IsNullOrWhiteSpace(module)) { - module = Environment.GetCommandLineArgs().First(); + module = Environment.GetCommandLineArgs()[0]; } string commandLineArgsString; @@ -398,7 +412,7 @@ namespace Jellyfin.Server else { commandLineArgsString = string.Join( - " ", + ' ', Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument)); }