From 4ba168c8a1be41d1c9db581bc79f170e24327d19 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Sun, 15 Aug 2021 20:32:08 +0200 Subject: [PATCH 01/16] Add splashscreen builder --- Emby.Drawing/NullImageEncoder.cs | 6 + Jellyfin.Drawing.Skia/SkiaEncoder.cs | 7 + Jellyfin.Drawing.Skia/SkiaHelper.cs | 37 ++++ Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 162 ++++++++++++++++++ Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 32 +--- .../Drawing/IImageEncoder.cs | 6 + .../Drawing/SplashscreenOptions.cs | 59 +++++++ 7 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 Jellyfin.Drawing.Skia/SplashscreenBuilder.cs create mode 100644 MediaBrowser.Controller/Drawing/SplashscreenOptions.cs diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index 1c05aa9161..ed12f6acb2 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -43,6 +43,12 @@ namespace Emby.Drawing throw new NotImplementedException(); } + /// + public void CreateSplashscreen(SplashscreenOptions options) + { + throw new NotImplementedException(); + } + /// public string GetImageBlurHash(int xComp, int yComp, string path) { diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 6d0a5ac2b9..16de5d7fde 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -492,6 +492,13 @@ namespace Jellyfin.Drawing.Skia } } + /// + public void CreateSplashscreen(SplashscreenOptions options) + { + var splashBuilder = new SplashscreenBuilder(this); + splashBuilder.GenerateSplash(options); + } + private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) { try diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/Jellyfin.Drawing.Skia/SkiaHelper.cs index f9c79c8558..35dcebdaba 100644 --- a/Jellyfin.Drawing.Skia/SkiaHelper.cs +++ b/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using SkiaSharp; namespace Jellyfin.Drawing.Skia @@ -19,5 +20,41 @@ namespace Jellyfin.Drawing.Skia throw new SkiaCodecException(result); } } + + /// + /// Gets the next valid image as a bitmap. + /// + /// The current skia encoder. + /// The list of image paths. + /// The current checked indes. + /// The new index. + /// A valid bitmap, or null if no bitmap exists after currentIndex. + public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList paths, int currentIndex, out int newIndex) + { + var imagesTested = new Dictionary(); + SKBitmap? bitmap = null; + + while (imagesTested.Count < paths.Count) + { + if (currentIndex >= paths.Count) + { + currentIndex = 0; + } + + bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); + + imagesTested[currentIndex] = 0; + + currentIndex++; + + if (bitmap != null) + { + break; + } + } + + newIndex = currentIndex; + return bitmap; + } } } diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs new file mode 100644 index 0000000000..8b6942be0c --- /dev/null +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Drawing; +using SkiaSharp; + +namespace Jellyfin.Drawing.Skia +{ + /// + /// Used to build the splashscreen. + /// + public class SplashscreenBuilder + { + private const int Rows = 6; + private const int Spacing = 20; + + private readonly SkiaEncoder _skiaEncoder; + + private Random? _random; + private int _finalWidth; + private int _finalHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The SkiaEncoder. + public SplashscreenBuilder(SkiaEncoder skiaEncoder) + { + _skiaEncoder = skiaEncoder; + } + + /// + /// Generate a splashscreen. + /// + /// The options to generate the splashscreen. + public void GenerateSplash(SplashscreenOptions options) + { + _finalWidth = options.Width; + _finalHeight = options.Height; + var wall = GenerateCollage(options.PortraitInputPaths, options.LandscapeInputPaths, options.ApplyFilter); + var transformed = Transform3D(wall); + + using var outputStream = new SKFileWStream(options.OutputPath); + using var pixmap = new SKPixmap(new SKImageInfo(_finalWidth, _finalHeight), transformed.GetPixels()); + pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(options.OutputPath), 90); + } + + /// + /// Generates a collage of posters and landscape pictures. + /// + /// The poster paths. + /// The landscape paths. + /// Whether to apply the darkening filter. + /// The created collage as a bitmap. + private SKBitmap GenerateCollage(IReadOnlyList poster, IReadOnlyList backdrop, bool applyFilter) + { + _random = new Random(); + + var posterIndex = 0; + var backdropIndex = 0; + + // use higher resolution than final image + var bitmap = new SKBitmap(_finalWidth * 3, _finalHeight * 2); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + + int posterHeight = _finalHeight * 2 / 6; + + for (int i = 0; i < Rows; i++) + { + int imageCounter = _random.Next(0, 5); + int currentWidthPos = i * 75; + int currentHeight = i * (posterHeight + Spacing); + + while (currentWidthPos < _finalWidth * 3) + { + SKBitmap? currentImage; + + switch (imageCounter) + { + case 0: + case 2: + case 3: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, poster, posterIndex, out int newPosterIndex); + posterIndex = newPosterIndex; + break; + default: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrop, backdropIndex, out int newBackdropIndex); + backdropIndex = newBackdropIndex; + break; + } + + if (currentImage == null) + { + throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!"); + } + + // resize to the same aspect as the original + var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height); + using var resizedBitmap = new SKBitmap(imageWidth, posterHeight); + currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High); + + // draw on canvas + canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight); + + currentWidthPos += imageWidth + Spacing; + + currentImage.Dispose(); + + if (imageCounter >= 4) + { + imageCounter = 0; + } + else + { + imageCounter++; + } + } + } + + if (applyFilter) + { + var paintColor = new SKPaint + { + Color = SKColors.Black.WithAlpha(0x50), + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(0, 0, _finalWidth * 3, _finalHeight * 2, paintColor); + } + + return bitmap; + } + + /// + /// Transform the collage in 3D space. + /// + /// The bitmap to transform. + /// The transformed image. + private SKBitmap Transform3D(SKBitmap input) + { + var bitmap = new SKBitmap(_finalWidth, _finalHeight); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + var matrix = new SKMatrix + { + ScaleX = 0.324108899f, + ScaleY = 0.563934922f, + SkewX = -0.244337708f, + SkewY = 0.0377609022f, + TransX = 42.0407715f, + TransY = -198.104706f, + Persp0 = -9.08959337E-05f, + Persp1 = 6.85242048E-05f, + Persp2 = 0.988209724f + }; + + canvas.SetMatrix(matrix); + canvas.DrawBitmap(input, 0, 0); + + return bitmap; + } + } +} diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index d1cc2255d7..6bece9db6e 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -99,7 +99,7 @@ namespace Jellyfin.Drawing.Skia using var canvas = new SKCanvas(bitmap); canvas.Clear(SKColors.Black); - using var backdrop = GetNextValidImage(paths, 0, out _); + using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _); if (backdrop == null) { return bitmap; @@ -152,34 +152,6 @@ namespace Jellyfin.Drawing.Skia return bitmap; } - private SKBitmap? GetNextValidImage(IReadOnlyList paths, int currentIndex, out int newIndex) - { - var imagesTested = new Dictionary(); - SKBitmap? bitmap = null; - - while (imagesTested.Count < paths.Count) - { - if (currentIndex >= paths.Count) - { - currentIndex = 0; - } - - bitmap = _skiaEncoder.Decode(paths[currentIndex], false, null, out _); - - imagesTested[currentIndex] = 0; - - currentIndex++; - - if (bitmap != null) - { - break; - } - } - - newIndex = currentIndex; - return bitmap; - } - private SKBitmap BuildSquareCollageBitmap(IReadOnlyList paths, int width, int height) { var bitmap = new SKBitmap(width, height); @@ -192,7 +164,7 @@ namespace Jellyfin.Drawing.Skia { for (var y = 0; y < 2; y++) { - using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex); + using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex); imageIndex = newIndex; if (currentBitmap == null) diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 4e67cfee4f..57d73699f6 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -74,5 +74,11 @@ namespace MediaBrowser.Controller.Drawing /// The options to use when creating the collage. /// Optional. void CreateImageCollage(ImageCollageOptions options, string? libraryName); + + /// + /// Creates a splashscreen image. + /// + /// The options to use when creating the splashscreen. + void CreateSplashscreen(SplashscreenOptions options); } } diff --git a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs new file mode 100644 index 0000000000..d70773d8f8 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Drawing +{ + /// + /// Options used to generate the splashscreen. + /// + public class SplashscreenOptions + { + /// + /// Initializes a new instance of the class. + /// + /// The portrait input paths. + /// The landscape input paths. + /// The output path. + /// Optional. The image width. + /// Optional. The image height. + /// Optional. Apply a darkening filter. + public SplashscreenOptions(IReadOnlyList portraitInputPaths, IReadOnlyList landscapeInputPaths, string outputPath, int width = 1920, int height = 1080, bool applyFilter = false) + { + PortraitInputPaths = portraitInputPaths; + LandscapeInputPaths = landscapeInputPaths; + OutputPath = outputPath; + Width = width; + Height = height; + ApplyFilter = applyFilter; + } + + /// + /// Gets or sets the poster input paths. + /// + public IReadOnlyList PortraitInputPaths { get; set; } + + /// + /// Gets or sets the landscape input paths. + /// + public IReadOnlyList LandscapeInputPaths { get; set; } + + /// + /// Gets or sets the output path. + /// + public string OutputPath { get; set; } + + /// + /// Gets or sets the width. + /// + public int Width { get; set; } + + /// + /// Gets or sets the height. + /// + public int Height { get; set; } + + /// + /// Gets or sets a value indicating whether to apply a darkening filter at the end. + /// + public bool ApplyFilter { get; set; } + } +} From c934269a6e915898629dd45e8fc8a55a75d91efd Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Sun, 15 Aug 2021 20:35:14 +0200 Subject: [PATCH 02/16] Add api controller for splashscreen --- .../Controllers/SplashscreenController.cs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 Jellyfin.Api/Controllers/SplashscreenController.cs diff --git a/Jellyfin.Api/Controllers/SplashscreenController.cs b/Jellyfin.Api/Controllers/SplashscreenController.cs new file mode 100644 index 0000000000..0fc9af60bb --- /dev/null +++ b/Jellyfin.Api/Controllers/SplashscreenController.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Jellyfin.Api.Attributes; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Splashscreen controller. + /// + [Route("Splashscreen")] + public class SplashscreenController : BaseJellyfinApiController + { + private readonly IImageEncoder _imageEncoder; + private readonly IItemRepository _itemRepository; + private readonly IApplicationPaths _appPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public SplashscreenController( + IImageEncoder imageEncoder, + IItemRepository itemRepository, + IApplicationPaths applicationPaths, + ILogger logger) + { + _imageEncoder = imageEncoder; + _itemRepository = itemRepository; + _appPaths = applicationPaths; + _logger = logger; + } + + /// + /// Generates or gets the splashscreen. + /// + /// Darken the generated image. + /// The image width. + /// The image height. + /// Whether to regenerate the image, regardless if one already exists. + /// The splashscreen. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesImageFile] + public ActionResult GetSplashscreen( + [FromQuery] bool? darken = false, + [FromQuery] int? width = 1920, + [FromQuery] int? height = 1080, + [FromQuery] bool? regenerate = false) + { + var outputPath = Path.Combine(_appPaths.DataPath, $"splashscreen-{width}x{height}-{darken}.jpg"); + + if (!System.IO.File.Exists(outputPath) || (regenerate ?? false)) + { + var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); + var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); + if (landscape.Count == 0) + { + _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen."); + landscape = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); + } + + _imageEncoder.CreateSplashscreen(new SplashscreenOptions(posters, landscape, outputPath, width!.Value, height!.Value, darken!.Value)); + } + + return PhysicalFile(outputPath, MimeTypes.GetMimeType(outputPath)); + } + + private IReadOnlyList GetItemsWithImageType(ImageType imageType) + { + return _itemRepository.GetItemList(new InternalItemsQuery + { + CollapseBoxSetItems = false, + Recursive = true, + DtoOptions = new DtoOptions(false), + ImageTypes = new ImageType[] { imageType }, + Limit = 8, + OrderBy = new ValueTuple[] + { + new ValueTuple(ItemSortBy.Random, SortOrder.Ascending) + }, + IncludeItemTypes = new string[] { "Movie", "Series" } + }); + } + } +} From 0fd4ff44513b195bb43368c7002d08991fed6c98 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Mon, 16 Aug 2021 12:26:41 +0200 Subject: [PATCH 03/16] Always use 1080p, add max parental rating --- .../Controllers/SplashscreenController.cs | 15 +++++++------ Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 22 +++++++++---------- .../Drawing/SplashscreenOptions.cs | 14 +----------- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/Jellyfin.Api/Controllers/SplashscreenController.cs b/Jellyfin.Api/Controllers/SplashscreenController.cs index 0fc9af60bb..48a559b281 100644 --- a/Jellyfin.Api/Controllers/SplashscreenController.cs +++ b/Jellyfin.Api/Controllers/SplashscreenController.cs @@ -52,8 +52,6 @@ namespace Jellyfin.Api.Controllers /// Generates or gets the splashscreen. /// /// Darken the generated image. - /// The image width. - /// The image height. /// Whether to regenerate the image, regardless if one already exists. /// The splashscreen. [HttpGet] @@ -62,11 +60,9 @@ namespace Jellyfin.Api.Controllers [ProducesImageFile] public ActionResult GetSplashscreen( [FromQuery] bool? darken = false, - [FromQuery] int? width = 1920, - [FromQuery] int? height = 1080, [FromQuery] bool? regenerate = false) { - var outputPath = Path.Combine(_appPaths.DataPath, $"splashscreen-{width}x{height}-{darken}.jpg"); + var outputPath = Path.Combine(_appPaths.DataPath, $"splashscreen-{darken}.jpg"); if (!System.IO.File.Exists(outputPath) || (regenerate ?? false)) { @@ -74,11 +70,13 @@ namespace Jellyfin.Api.Controllers var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); if (landscape.Count == 0) { + // Thumb images fit better because they include the title in the image but are not provided with TMDb. + // Using backdrops as a fallback to generate an image at all _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen."); landscape = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); } - _imageEncoder.CreateSplashscreen(new SplashscreenOptions(posters, landscape, outputPath, width!.Value, height!.Value, darken!.Value)); + _imageEncoder.CreateSplashscreen(new SplashscreenOptions(posters, landscape, outputPath, darken!.Value)); } return PhysicalFile(outputPath, MimeTypes.GetMimeType(outputPath)); @@ -86,13 +84,16 @@ namespace Jellyfin.Api.Controllers private IReadOnlyList GetItemsWithImageType(ImageType imageType) { + // todo make included libraries configurable return _itemRepository.GetItemList(new InternalItemsQuery { CollapseBoxSetItems = false, Recursive = true, DtoOptions = new DtoOptions(false), ImageTypes = new ImageType[] { imageType }, - Limit = 8, + Limit = 30, + // todo max parental rating configurable + MaxParentalRating = 10, OrderBy = new ValueTuple[] { new ValueTuple(ItemSortBy.Random, SortOrder.Ascending) diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 8b6942be0c..4773464b43 100644 --- a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -10,14 +10,17 @@ namespace Jellyfin.Drawing.Skia /// public class SplashscreenBuilder { + private const int FinalWidth = 1920; + private const int FinalHeight = 1080; + // generated collage resolution should be higher than the final resolution + private const int WallWidth = FinalWidth * 3; + private const int WallHeight = FinalHeight * 2; private const int Rows = 6; private const int Spacing = 20; private readonly SkiaEncoder _skiaEncoder; private Random? _random; - private int _finalWidth; - private int _finalHeight; /// /// Initializes a new instance of the class. @@ -34,13 +37,11 @@ namespace Jellyfin.Drawing.Skia /// The options to generate the splashscreen. public void GenerateSplash(SplashscreenOptions options) { - _finalWidth = options.Width; - _finalHeight = options.Height; var wall = GenerateCollage(options.PortraitInputPaths, options.LandscapeInputPaths, options.ApplyFilter); var transformed = Transform3D(wall); using var outputStream = new SKFileWStream(options.OutputPath); - using var pixmap = new SKPixmap(new SKImageInfo(_finalWidth, _finalHeight), transformed.GetPixels()); + using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels()); pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(options.OutputPath), 90); } @@ -58,12 +59,11 @@ namespace Jellyfin.Drawing.Skia var posterIndex = 0; var backdropIndex = 0; - // use higher resolution than final image - var bitmap = new SKBitmap(_finalWidth * 3, _finalHeight * 2); + var bitmap = new SKBitmap(WallWidth, WallHeight); using var canvas = new SKCanvas(bitmap); canvas.Clear(SKColors.Black); - int posterHeight = _finalHeight * 2 / 6; + int posterHeight = WallHeight / 6; for (int i = 0; i < Rows; i++) { @@ -71,7 +71,7 @@ namespace Jellyfin.Drawing.Skia int currentWidthPos = i * 75; int currentHeight = i * (posterHeight + Spacing); - while (currentWidthPos < _finalWidth * 3) + while (currentWidthPos < WallWidth) { SKBitmap? currentImage; @@ -124,7 +124,7 @@ namespace Jellyfin.Drawing.Skia Color = SKColors.Black.WithAlpha(0x50), Style = SKPaintStyle.Fill }; - canvas.DrawRect(0, 0, _finalWidth * 3, _finalHeight * 2, paintColor); + canvas.DrawRect(0, 0, WallWidth, WallHeight, paintColor); } return bitmap; @@ -137,7 +137,7 @@ namespace Jellyfin.Drawing.Skia /// The transformed image. private SKBitmap Transform3D(SKBitmap input) { - var bitmap = new SKBitmap(_finalWidth, _finalHeight); + var bitmap = new SKBitmap(FinalWidth, FinalHeight); using var canvas = new SKCanvas(bitmap); canvas.Clear(SKColors.Black); var matrix = new SKMatrix diff --git a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs index d70773d8f8..0534d60b69 100644 --- a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs +++ b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs @@ -16,13 +16,11 @@ namespace MediaBrowser.Controller.Drawing /// Optional. The image width. /// Optional. The image height. /// Optional. Apply a darkening filter. - public SplashscreenOptions(IReadOnlyList portraitInputPaths, IReadOnlyList landscapeInputPaths, string outputPath, int width = 1920, int height = 1080, bool applyFilter = false) + public SplashscreenOptions(IReadOnlyList portraitInputPaths, IReadOnlyList landscapeInputPaths, string outputPath, bool applyFilter = false) { PortraitInputPaths = portraitInputPaths; LandscapeInputPaths = landscapeInputPaths; OutputPath = outputPath; - Width = width; - Height = height; ApplyFilter = applyFilter; } @@ -41,16 +39,6 @@ namespace MediaBrowser.Controller.Drawing /// public string OutputPath { get; set; } - /// - /// Gets or sets the width. - /// - public int Width { get; set; } - - /// - /// Gets or sets the height. - /// - public int Height { get; set; } - /// /// Gets or sets a value indicating whether to apply a darkening filter at the end. /// From 3fb3ee074a375d0afd4d7d81252ba535d885afb8 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 18:11:38 +0200 Subject: [PATCH 04/16] Remove splashscreen generation from IImageEncoder and add IImageGenerator --- Emby.Drawing/NullImageEncoder.cs | 6 - .../Controllers/SplashscreenController.cs | 105 ------------------ .../DefaultImageGenerator.cs | 83 ++++++++++++++ Jellyfin.Drawing.Skia/SkiaEncoder.cs | 7 -- Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 20 ++-- Jellyfin.Server/CoreAppHost.cs | 3 + .../Drawing/GeneratedImages.cs | 13 +++ .../Drawing/IImageEncoder.cs | 6 - .../Drawing/IImageGenerator.cs | 17 +++ .../Drawing/SplashscreenOptions.cs | 20 +--- 10 files changed, 128 insertions(+), 152 deletions(-) delete mode 100644 Jellyfin.Api/Controllers/SplashscreenController.cs create mode 100644 Jellyfin.Drawing.Skia/DefaultImageGenerator.cs create mode 100644 MediaBrowser.Controller/Drawing/GeneratedImages.cs create mode 100644 MediaBrowser.Controller/Drawing/IImageGenerator.cs diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index ed12f6acb2..1c05aa9161 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -43,12 +43,6 @@ namespace Emby.Drawing throw new NotImplementedException(); } - /// - public void CreateSplashscreen(SplashscreenOptions options) - { - throw new NotImplementedException(); - } - /// public string GetImageBlurHash(int xComp, int yComp, string path) { diff --git a/Jellyfin.Api/Controllers/SplashscreenController.cs b/Jellyfin.Api/Controllers/SplashscreenController.cs deleted file mode 100644 index 48a559b281..0000000000 --- a/Jellyfin.Api/Controllers/SplashscreenController.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Jellyfin.Api.Attributes; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Querying; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Api.Controllers -{ - /// - /// Splashscreen controller. - /// - [Route("Splashscreen")] - public class SplashscreenController : BaseJellyfinApiController - { - private readonly IImageEncoder _imageEncoder; - private readonly IItemRepository _itemRepository; - private readonly IApplicationPaths _appPaths; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public SplashscreenController( - IImageEncoder imageEncoder, - IItemRepository itemRepository, - IApplicationPaths applicationPaths, - ILogger logger) - { - _imageEncoder = imageEncoder; - _itemRepository = itemRepository; - _appPaths = applicationPaths; - _logger = logger; - } - - /// - /// Generates or gets the splashscreen. - /// - /// Darken the generated image. - /// Whether to regenerate the image, regardless if one already exists. - /// The splashscreen. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [ProducesImageFile] - public ActionResult GetSplashscreen( - [FromQuery] bool? darken = false, - [FromQuery] bool? regenerate = false) - { - var outputPath = Path.Combine(_appPaths.DataPath, $"splashscreen-{darken}.jpg"); - - if (!System.IO.File.Exists(outputPath) || (regenerate ?? false)) - { - var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); - var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); - if (landscape.Count == 0) - { - // Thumb images fit better because they include the title in the image but are not provided with TMDb. - // Using backdrops as a fallback to generate an image at all - _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen."); - landscape = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); - } - - _imageEncoder.CreateSplashscreen(new SplashscreenOptions(posters, landscape, outputPath, darken!.Value)); - } - - return PhysicalFile(outputPath, MimeTypes.GetMimeType(outputPath)); - } - - private IReadOnlyList GetItemsWithImageType(ImageType imageType) - { - // todo make included libraries configurable - return _itemRepository.GetItemList(new InternalItemsQuery - { - CollapseBoxSetItems = false, - Recursive = true, - DtoOptions = new DtoOptions(false), - ImageTypes = new ImageType[] { imageType }, - Limit = 30, - // todo max parental rating configurable - MaxParentalRating = 10, - OrderBy = new ValueTuple[] - { - new ValueTuple(ItemSortBy.Random, SortOrder.Ascending) - }, - IncludeItemTypes = new string[] { "Movie", "Series" } - }); - } - } -} diff --git a/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs b/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs new file mode 100644 index 0000000000..780d0b060c --- /dev/null +++ b/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Drawing.Skia +{ + /// + /// The default image generator. + /// + public class DefaultImageGenerator : IImageGenerator + { + private readonly IImageEncoder _imageEncoder; + private readonly IItemRepository _itemRepository; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public DefaultImageGenerator( + IImageEncoder imageEncoder, + IItemRepository itemRepository, + ILogger logger) + { + _imageEncoder = imageEncoder; + _itemRepository = itemRepository; + _logger = logger; + } + + /// + public GeneratedImages[] GetSupportedImages() + { + return new[] { GeneratedImages.Splashscreen }; + } + + /// + public void GenerateSplashscreen(SplashscreenOptions generationOptions) + { + var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); + var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); + if (landscape.Count == 0) + { + // Thumb images fit better because they include the title in the image but are not provided with TMDb. + // Using backdrops as a fallback to generate an image at all + _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen."); + landscape = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); + } + + var splashBuilder = new SplashscreenBuilder((SkiaEncoder)_imageEncoder); + splashBuilder.GenerateSplash(posters, landscape, generationOptions.OutputPath, generationOptions.ApplyFilter); + } + + private IReadOnlyList GetItemsWithImageType(ImageType imageType) + { + // todo make included libraries configurable + return _itemRepository.GetItemList(new InternalItemsQuery + { + CollapseBoxSetItems = false, + Recursive = true, + DtoOptions = new DtoOptions(false), + ImageTypes = new ImageType[] { imageType }, + Limit = 30, + // todo max parental rating configurable + MaxParentalRating = 10, + OrderBy = new ValueTuple[] + { + new ValueTuple(ItemSortBy.Random, SortOrder.Ascending) + }, + IncludeItemTypes = new string[] { "Movie", "Series" } + }); + } + } +} diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 16de5d7fde..6d0a5ac2b9 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -492,13 +492,6 @@ namespace Jellyfin.Drawing.Skia } } - /// - public void CreateSplashscreen(SplashscreenOptions options) - { - var splashBuilder = new SplashscreenBuilder(this); - splashBuilder.GenerateSplash(options); - } - private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) { try diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 4773464b43..7cb10bfeef 100644 --- a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using MediaBrowser.Controller.Drawing; using SkiaSharp; namespace Jellyfin.Drawing.Skia @@ -34,25 +33,28 @@ namespace Jellyfin.Drawing.Skia /// /// Generate a splashscreen. /// - /// The options to generate the splashscreen. - public void GenerateSplash(SplashscreenOptions options) + /// The poster paths. + /// The landscape paths. + /// The output path. + /// Whether to apply the darkening filter. + public void GenerateSplash(IReadOnlyList posters, IReadOnlyList backdrop, string outputPath, bool applyFilter) { - var wall = GenerateCollage(options.PortraitInputPaths, options.LandscapeInputPaths, options.ApplyFilter); + var wall = GenerateCollage(posters, backdrop, applyFilter); var transformed = Transform3D(wall); - using var outputStream = new SKFileWStream(options.OutputPath); + using var outputStream = new SKFileWStream(outputPath); using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels()); - pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(options.OutputPath), 90); + pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90); } /// /// Generates a collage of posters and landscape pictures. /// - /// The poster paths. + /// The poster paths. /// The landscape paths. /// Whether to apply the darkening filter. /// The created collage as a bitmap. - private SKBitmap GenerateCollage(IReadOnlyList poster, IReadOnlyList backdrop, bool applyFilter) + private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrop, bool applyFilter) { _random = new Random(); @@ -80,7 +82,7 @@ namespace Jellyfin.Drawing.Skia case 0: case 2: case 3: - currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, poster, posterIndex, out int newPosterIndex); + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex); posterIndex = newPosterIndex; break; default: diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 67e50b92d9..ac7ab2dee0 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -85,6 +85,9 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + // TODO search plugins + ServiceCollection.AddSingleton(); + // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/MediaBrowser.Controller/Drawing/GeneratedImages.cs b/MediaBrowser.Controller/Drawing/GeneratedImages.cs new file mode 100644 index 0000000000..47b60979b2 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/GeneratedImages.cs @@ -0,0 +1,13 @@ +namespace MediaBrowser.Controller.Drawing +{ + /// + /// Which generated images an supports. + /// + public enum GeneratedImages + { + /// + /// The splashscreen. + /// + Splashscreen + } +} diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 57d73699f6..4e67cfee4f 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -74,11 +74,5 @@ namespace MediaBrowser.Controller.Drawing /// The options to use when creating the collage. /// Optional. void CreateImageCollage(ImageCollageOptions options, string? libraryName); - - /// - /// Creates a splashscreen image. - /// - /// The options to use when creating the splashscreen. - void CreateSplashscreen(SplashscreenOptions options); } } diff --git a/MediaBrowser.Controller/Drawing/IImageGenerator.cs b/MediaBrowser.Controller/Drawing/IImageGenerator.cs new file mode 100644 index 0000000000..21699c3f0b --- /dev/null +++ b/MediaBrowser.Controller/Drawing/IImageGenerator.cs @@ -0,0 +1,17 @@ +namespace MediaBrowser.Controller.Drawing +{ + public interface IImageGenerator + { + /// + /// Gets the supported generated images of the image generator. + /// + /// The supported images. + GeneratedImages[] GetSupportedImages(); + + /// + /// Generates a splashscreen. + /// + /// The options used to generate the splashscreen. + void GenerateSplashscreen(SplashscreenOptions generationOptions); + } +} diff --git a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs index 0534d60b69..ba268b8eb7 100644 --- a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs +++ b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace MediaBrowser.Controller.Drawing { /// @@ -10,30 +8,14 @@ namespace MediaBrowser.Controller.Drawing /// /// Initializes a new instance of the class. /// - /// The portrait input paths. - /// The landscape input paths. /// The output path. - /// Optional. The image width. - /// Optional. The image height. /// Optional. Apply a darkening filter. - public SplashscreenOptions(IReadOnlyList portraitInputPaths, IReadOnlyList landscapeInputPaths, string outputPath, bool applyFilter = false) + public SplashscreenOptions(string outputPath, bool applyFilter = false) { - PortraitInputPaths = portraitInputPaths; - LandscapeInputPaths = landscapeInputPaths; OutputPath = outputPath; ApplyFilter = applyFilter; } - /// - /// Gets or sets the poster input paths. - /// - public IReadOnlyList PortraitInputPaths { get; set; } - - /// - /// Gets or sets the landscape input paths. - /// - public IReadOnlyList LandscapeInputPaths { get; set; } - /// /// Gets or sets the output path. /// From e026ba84c5e0d0a761cb1cae403e6f601e6fa2e0 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 18:12:45 +0200 Subject: [PATCH 05/16] Add Splashscreen API endpoint to ImageController --- Jellyfin.Api/Controllers/ImageController.cs | 230 +++++++++++++----- .../Branding/BrandingOptions.cs | 8 + 2 files changed, 173 insertions(+), 65 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 86933074d0..0755b6c44c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -11,12 +11,14 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Branding; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -44,6 +46,8 @@ namespace Jellyfin.Api.Controllers private readonly IAuthorizationContext _authContext; private readonly ILogger _logger; private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IApplicationPaths _appPaths; + private readonly IImageGenerator _imageGenerator; /// /// Initializes a new instance of the class. @@ -56,6 +60,8 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. public ImageController( IUserManager userManager, ILibraryManager libraryManager, @@ -64,7 +70,9 @@ namespace Jellyfin.Api.Controllers IFileSystem fileSystem, IAuthorizationContext authContext, ILogger logger, - IServerConfigurationManager serverConfigurationManager) + IServerConfigurationManager serverConfigurationManager, + IApplicationPaths appPaths, + IImageGenerator imageGenerator) { _userManager = userManager; _libraryManager = libraryManager; @@ -74,6 +82,8 @@ namespace Jellyfin.Api.Controllers _authContext = authContext; _logger = logger; _serverConfigurationManager = serverConfigurationManager; + _appPaths = appPaths; + _imageGenerator = imageGenerator; } /// @@ -1692,6 +1702,130 @@ namespace Jellyfin.Api.Controllers .ConfigureAwait(false); } + /// + /// Generates or gets the splashscreen. + /// + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Darken the generated image. + /// Splashscreen returned successfully. + /// The splashscreen. + [HttpGet("Branding/Splashscreen")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesImageFile] + public async Task GetSplashscreen( + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] bool? darken = false) + { + string splashscreenPath; + var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); + if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)) + { + splashscreenPath = brandingOptions.SplashscreenLocation!; + } + else + { + var filename = darken!.Value ? "splashscreen-darken.webp" : "splashscreen.webp"; + splashscreenPath = Path.Combine(_appPaths.DataPath, filename); + + if (!System.IO.File.Exists(splashscreenPath) && _imageGenerator.GetSupportedImages().Contains(GeneratedImages.Splashscreen)) + { + _imageGenerator.GenerateSplashscreen(new SplashscreenOptions(splashscreenPath, darken.Value)); + } + } + + var outputFormats = GetOutputFormats(format); + + TimeSpan? cacheDuration = null; + if (!string.IsNullOrEmpty(tag)) + { + cacheDuration = TimeSpan.FromDays(365); + } + + var options = new ImageProcessingOptions + { + Image = new ItemImageInfo + { + Path = splashscreenPath, + Height = 1080, + Width = 1920 + }, + Height = height, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, + Quality = quality ?? 100, + Width = width, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = outputFormats + }; + return await GetImageResult( + options, + cacheDuration, + new Dictionary(), + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Uploads a custom splashscreen. + /// + /// A indicating success. + /// Error reading the image format. + [HttpPost("Branding/Splashscreen")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [AcceptsImageFile] + public async Task UploadCustomSplashscreen() + { + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + + if (mimeType == null) + { + throw new ArgumentException("Error reading mimetype from uploaded image!"); + } + + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType)); + var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); + brandingOptions.SplashscreenLocation = filePath; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + { + await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); + } + + return NoContent(); + } + private static async Task GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); @@ -1823,25 +1957,35 @@ namespace Jellyfin.Api.Controllers { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } }; + if (!imageInfo.IsLocalFile && item != null) + { + imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false); + } + + var options = new ImageProcessingOptions + { + Height = height, + ImageIndex = imageIndex ?? 0, + Image = imageInfo, + Item = item, + ItemId = itemId, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, + Quality = quality ?? 100, + Width = width, + AddPlayedIndicator = addPlayedIndicator ?? false, + PercentPlayed = percentPlayed ?? 0, + UnplayedCount = unplayedCount, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = outputFormats + }; + return await GetImageResult( - item, - itemId, - imageIndex, - width, - height, - maxWidth, - maxHeight, - fillWidth, - fillHeight, - quality, - addPlayedIndicator, - percentPlayed, - unplayedCount, - blur, - backgroundColor, - foregroundLayer, - imageInfo, - outputFormats, + options, cacheDuration, responseHeaders, isHeadRequest).ConfigureAwait(false); @@ -1922,56 +2066,12 @@ namespace Jellyfin.Api.Controllers } private async Task GetImageResult( - BaseItem? item, - Guid itemId, - int? index, - int? width, - int? height, - int? maxWidth, - int? maxHeight, - int? fillWidth, - int? fillHeight, - int? quality, - bool? addPlayedIndicator, - double? percentPlayed, - int? unplayedCount, - int? blur, - string? backgroundColor, - string? foregroundLayer, - ItemImageInfo imageInfo, - IReadOnlyCollection supportedFormats, + ImageProcessingOptions imageProcessingOptions, TimeSpan? cacheDuration, IDictionary headers, bool isHeadRequest) { - if (!imageInfo.IsLocalFile && item != null) - { - imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false); - } - - var options = new ImageProcessingOptions - { - Height = height, - ImageIndex = index ?? 0, - Image = imageInfo, - Item = item, - ItemId = itemId, - MaxHeight = maxHeight, - MaxWidth = maxWidth, - FillHeight = fillHeight, - FillWidth = fillWidth, - Quality = quality ?? 100, - Width = width, - AddPlayedIndicator = addPlayedIndicator ?? false, - PercentPlayed = percentPlayed ?? 0, - UnplayedCount = unplayedCount, - Blur = blur, - BackgroundColor = backgroundColor, - ForegroundLayer = foregroundLayer, - SupportedOutputFormats = supportedFormats - }; - - var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); + var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false); var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache"); var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index 7f19a5b852..ee182207d4 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -1,3 +1,5 @@ +using System.Xml.Serialization; + #pragma warning disable CS1591 namespace MediaBrowser.Model.Branding @@ -15,5 +17,11 @@ namespace MediaBrowser.Model.Branding /// /// The custom CSS. public string? CustomCss { get; set; } + + /// + /// Gets or sets the splashscreen location on disk. + /// + /// The location of the user splashscreen. + public string? SplashscreenLocation { get; set; } } } From 68db3be0e754d223a290c9276345bc9551eab887 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 18:34:54 +0200 Subject: [PATCH 06/16] Remove darkening filter from Splashscreen Using the foregroundLayer parameter has the same effect --- Jellyfin.Api/Controllers/ImageController.cs | 9 ++---- .../DefaultImageGenerator.cs | 4 +-- Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 18 ++---------- .../Drawing/IImageGenerator.cs | 4 +-- .../Drawing/SplashscreenOptions.cs | 29 ------------------- 5 files changed, 10 insertions(+), 54 deletions(-) delete mode 100644 MediaBrowser.Controller/Drawing/SplashscreenOptions.cs diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 0755b6c44c..d7aac9b9ce 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1717,7 +1717,6 @@ namespace Jellyfin.Api.Controllers /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. - /// Darken the generated image. /// Splashscreen returned successfully. /// The splashscreen. [HttpGet("Branding/Splashscreen")] @@ -1735,8 +1734,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? fillHeight, [FromQuery] int? blur, [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery] bool? darken = false) + [FromQuery] string? foregroundLayer) { string splashscreenPath; var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); @@ -1746,12 +1744,11 @@ namespace Jellyfin.Api.Controllers } else { - var filename = darken!.Value ? "splashscreen-darken.webp" : "splashscreen.webp"; - splashscreenPath = Path.Combine(_appPaths.DataPath, filename); + splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.webp"); if (!System.IO.File.Exists(splashscreenPath) && _imageGenerator.GetSupportedImages().Contains(GeneratedImages.Splashscreen)) { - _imageGenerator.GenerateSplashscreen(new SplashscreenOptions(splashscreenPath, darken.Value)); + _imageGenerator.GenerateSplashscreen(splashscreenPath); } } diff --git a/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs b/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs index 780d0b060c..4d029d9069 100644 --- a/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs +++ b/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs @@ -44,7 +44,7 @@ namespace Jellyfin.Drawing.Skia } /// - public void GenerateSplashscreen(SplashscreenOptions generationOptions) + public void GenerateSplashscreen(string outputPath) { var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); @@ -57,7 +57,7 @@ namespace Jellyfin.Drawing.Skia } var splashBuilder = new SplashscreenBuilder((SkiaEncoder)_imageEncoder); - splashBuilder.GenerateSplash(posters, landscape, generationOptions.OutputPath, generationOptions.ApplyFilter); + splashBuilder.GenerateSplash(posters, landscape, outputPath); } private IReadOnlyList GetItemsWithImageType(ImageType imageType) diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 7cb10bfeef..2056515549 100644 --- a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -36,10 +36,9 @@ namespace Jellyfin.Drawing.Skia /// The poster paths. /// The landscape paths. /// The output path. - /// Whether to apply the darkening filter. - public void GenerateSplash(IReadOnlyList posters, IReadOnlyList backdrop, string outputPath, bool applyFilter) + public void GenerateSplash(IReadOnlyList posters, IReadOnlyList backdrop, string outputPath) { - var wall = GenerateCollage(posters, backdrop, applyFilter); + var wall = GenerateCollage(posters, backdrop); var transformed = Transform3D(wall); using var outputStream = new SKFileWStream(outputPath); @@ -52,9 +51,8 @@ namespace Jellyfin.Drawing.Skia /// /// The poster paths. /// The landscape paths. - /// Whether to apply the darkening filter. /// The created collage as a bitmap. - private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrop, bool applyFilter) + private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrop) { _random = new Random(); @@ -119,16 +117,6 @@ namespace Jellyfin.Drawing.Skia } } - if (applyFilter) - { - var paintColor = new SKPaint - { - Color = SKColors.Black.WithAlpha(0x50), - Style = SKPaintStyle.Fill - }; - canvas.DrawRect(0, 0, WallWidth, WallHeight, paintColor); - } - return bitmap; } diff --git a/MediaBrowser.Controller/Drawing/IImageGenerator.cs b/MediaBrowser.Controller/Drawing/IImageGenerator.cs index 21699c3f0b..4f944ce543 100644 --- a/MediaBrowser.Controller/Drawing/IImageGenerator.cs +++ b/MediaBrowser.Controller/Drawing/IImageGenerator.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Controller.Drawing /// /// Generates a splashscreen. /// - /// The options used to generate the splashscreen. - void GenerateSplashscreen(SplashscreenOptions generationOptions); + /// The path where the splashscreen should be saved. + void GenerateSplashscreen(string outputPath); } } diff --git a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs b/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs deleted file mode 100644 index ba268b8eb7..0000000000 --- a/MediaBrowser.Controller/Drawing/SplashscreenOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace MediaBrowser.Controller.Drawing -{ - /// - /// Options used to generate the splashscreen. - /// - public class SplashscreenOptions - { - /// - /// Initializes a new instance of the class. - /// - /// The output path. - /// Optional. Apply a darkening filter. - public SplashscreenOptions(string outputPath, bool applyFilter = false) - { - OutputPath = outputPath; - ApplyFilter = applyFilter; - } - - /// - /// Gets or sets the output path. - /// - public string OutputPath { get; set; } - - /// - /// Gets or sets a value indicating whether to apply a darkening filter at the end. - /// - public bool ApplyFilter { get; set; } - } -} From 045ef4b72602d682e31eac891c438941ffe16e42 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 18:35:26 +0200 Subject: [PATCH 07/16] Generate Splashscreen during RefreshMediaLibrary scheduled task --- .../Tasks/RefreshMediaLibraryTask.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index f7b3cfedcc..64393669b3 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -2,9 +2,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; @@ -21,16 +24,26 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; + private readonly IImageGenerator _imageGenerator; + private readonly IApplicationPaths _applicationPaths; /// /// Initializes a new instance of the class. /// - /// The library manager. - /// The localization manager. - public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization) + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public RefreshMediaLibraryTask( + ILibraryManager libraryManager, + ILocalizationManager localization, + IImageGenerator imageGenerator, + IApplicationPaths applicationPaths) { _libraryManager = libraryManager; _localization = localization; + _imageGenerator = imageGenerator; + _applicationPaths = applicationPaths; } /// @@ -70,6 +83,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks progress.Report(0); + _imageGenerator.GenerateSplashscreen(Path.Combine(_applicationPaths.DataPath, "splashscreen.webp")); + return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); } } From c70452b4a4046b82e26a2e73b1d67e8f82fe0e32 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 19:18:24 +0200 Subject: [PATCH 08/16] Add missing response code documentation --- Jellyfin.Api/Controllers/ImageController.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index d7aac9b9ce..3b2fc98e79 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1764,9 +1764,7 @@ namespace Jellyfin.Api.Controllers { Image = new ItemImageInfo { - Path = splashscreenPath, - Height = 1080, - Width = 1920 + Path = splashscreenPath }, Height = height, MaxHeight = maxHeight, @@ -1791,11 +1789,15 @@ namespace Jellyfin.Api.Controllers /// Uploads a custom splashscreen. /// /// A indicating success. + /// Sucessfully uploaded new splashscreen. + /// Error reading MimeType from uploaded image. + /// User does not have permission to upload splashscreen.. /// Error reading the image format. [HttpPost("Branding/Splashscreen")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [AcceptsImageFile] public async Task UploadCustomSplashscreen() { @@ -1806,7 +1808,7 @@ namespace Jellyfin.Api.Controllers if (mimeType == null) { - throw new ArgumentException("Error reading mimetype from uploaded image!"); + return BadRequest("Error reading mimetype from uploaded image"); } var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType)); From 6ad020acb2cb94ae577b05d6b018115d69259e26 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 19:39:48 +0200 Subject: [PATCH 09/16] Include Splashscreen url in the branding endpoint --- MediaBrowser.Model/Branding/BrandingOptions.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index ee182207d4..18a177e2db 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using System.Xml.Serialization; #pragma warning disable CS1591 @@ -21,7 +22,17 @@ namespace MediaBrowser.Model.Branding /// /// Gets or sets the splashscreen location on disk. /// - /// The location of the user splashscreen. + /// + /// Not served via the API. + /// Only used to save the custom uploaded user splashscreen in the configuration file. + /// + [JsonIgnore] public string? SplashscreenLocation { get; set; } + + /// + /// Gets the splashscreen url. + /// + [XmlIgnore] + public string SplashscreenUrl => "/Branding/Splashscreen"; } } From 9e23af5636adf7b9c657b125c40c2c9846aef112 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 20:32:52 +0200 Subject: [PATCH 10/16] Add missing xml doc --- MediaBrowser.Controller/Drawing/IImageGenerator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MediaBrowser.Controller/Drawing/IImageGenerator.cs b/MediaBrowser.Controller/Drawing/IImageGenerator.cs index 4f944ce543..f7e7f83b2e 100644 --- a/MediaBrowser.Controller/Drawing/IImageGenerator.cs +++ b/MediaBrowser.Controller/Drawing/IImageGenerator.cs @@ -1,5 +1,8 @@ namespace MediaBrowser.Controller.Drawing { + /// + /// Interface for an image generator. + /// public interface IImageGenerator { /// From 9e0958d8224ef3fa51893fd5a38cc57104f32422 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Wed, 18 Aug 2021 14:22:01 +0200 Subject: [PATCH 11/16] Apply suggestions from code review --- Jellyfin.Api/Controllers/ImageController.cs | 7 +++---- Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 6 ++---- MediaBrowser.Model/Branding/BrandingOptions.cs | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 3b2fc98e79..24059cddd3 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1803,15 +1803,14 @@ namespace Jellyfin.Api.Controllers { await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - // Handle image/png; charset=utf-8 - var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; - if (mimeType == null) + if (!mimeType.HasValue) { return BadRequest("Error reading mimetype from uploaded image"); } - var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType)); + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value)); var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); brandingOptions.SplashscreenLocation = filePath; _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 2056515549..9f801c3208 100644 --- a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -19,8 +19,6 @@ namespace Jellyfin.Drawing.Skia private readonly SkiaEncoder _skiaEncoder; - private Random? _random; - /// /// Initializes a new instance of the class. /// @@ -54,7 +52,7 @@ namespace Jellyfin.Drawing.Skia /// The created collage as a bitmap. private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrop) { - _random = new Random(); + var random = new Random(); var posterIndex = 0; var backdropIndex = 0; @@ -67,7 +65,7 @@ namespace Jellyfin.Drawing.Skia for (int i = 0; i < Rows; i++) { - int imageCounter = _random.Next(0, 5); + int imageCounter = random.Next(0, 5); int currentWidthPos = i * 75; int currentHeight = i * (posterHeight + Spacing); diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index 18a177e2db..56e5a87152 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -33,6 +33,6 @@ namespace MediaBrowser.Model.Branding /// Gets the splashscreen url. /// [XmlIgnore] - public string SplashscreenUrl => "/Branding/Splashscreen"; + public string? SplashscreenUrl => "/Branding/Splashscreen"; } } From 360fd70fc74325008b031c9a1155b9b76724866d Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 4 Jan 2022 08:37:57 -0700 Subject: [PATCH 12/16] Clean up --- .../Tasks/RefreshMediaLibraryTask.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 32 ++--- .../DefaultImageGenerator.cs | 115 +++++++++--------- Jellyfin.Server/CoreAppHost.cs | 2 +- .../Drawing/GeneratedImageType.cs | 12 ++ .../Drawing/GeneratedImages.cs | 13 -- .../Drawing/IImageGenerator.cs | 32 ++--- .../Branding/BrandingOptions.cs | 58 ++++----- 8 files changed, 134 insertions(+), 132 deletions(-) create mode 100644 MediaBrowser.Controller/Drawing/GeneratedImageType.cs delete mode 100644 MediaBrowser.Controller/Drawing/GeneratedImages.cs diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index 64393669b3..c12fc21130 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks progress.Report(0); - _imageGenerator.GenerateSplashscreen(Path.Combine(_applicationPaths.DataPath, "splashscreen.webp")); + _imageGenerator.Generate(GeneratedImageType.Splashscreen, Path.Combine(_applicationPaths.DataPath, "splashscreen.webp")); return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 24059cddd3..6d34ca7708 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -1705,18 +1706,18 @@ namespace Jellyfin.Api.Controllers /// /// Generates or gets the splashscreen. /// - /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. /// The maximum image height to return. /// The fixed image width to return. /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. + /// Blur image. + /// Apply a background color for transparent images. + /// Apply a foreground layer on top of the image. + /// Quality setting, from 0-100. /// Splashscreen returned successfully. /// The splashscreen. [HttpGet("Branding/Splashscreen")] @@ -1729,12 +1730,12 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? maxHeight, [FromQuery] int? width, [FromQuery] int? height, - [FromQuery] int? quality, [FromQuery] int? fillWidth, [FromQuery] int? fillHeight, [FromQuery] int? blur, [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer) + [FromQuery] string? foregroundLayer, + [FromQuery, Range(0, 100)] int quality = 90) { string splashscreenPath; var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); @@ -1746,9 +1747,9 @@ namespace Jellyfin.Api.Controllers { splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.webp"); - if (!System.IO.File.Exists(splashscreenPath) && _imageGenerator.GetSupportedImages().Contains(GeneratedImages.Splashscreen)) + if (!System.IO.File.Exists(splashscreenPath) && _imageGenerator.GetSupportedImages().Contains(GeneratedImageType.Splashscreen)) { - _imageGenerator.GenerateSplashscreen(splashscreenPath); + _imageGenerator.Generate(GeneratedImageType.Splashscreen, splashscreenPath); } } @@ -1771,18 +1772,20 @@ namespace Jellyfin.Api.Controllers MaxWidth = maxWidth, FillHeight = fillHeight, FillWidth = fillWidth, - Quality = quality ?? 100, + Quality = quality, Width = width, Blur = blur, BackgroundColor = backgroundColor, ForegroundLayer = foregroundLayer, SupportedOutputFormats = outputFormats }; + return await GetImageResult( - options, - cacheDuration, - new Dictionary(), - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)); + options, + cacheDuration, + ImmutableDictionary.Empty, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); } /// @@ -1815,7 +1818,6 @@ namespace Jellyfin.Api.Controllers brandingOptions.SplashscreenLocation = filePath; _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); - // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . await using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs b/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs index 4d029d9069..e102b8f49a 100644 --- a/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs +++ b/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs @@ -10,74 +10,73 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; -namespace Jellyfin.Drawing.Skia +namespace Jellyfin.Drawing.Skia; + +/// +/// The default image generator. +/// +public class DefaultImageGenerator : IImageGenerator { + private readonly IImageEncoder _imageEncoder; + private readonly IItemRepository _itemRepository; + private readonly ILogger _logger; + /// - /// The default image generator. + /// Initializes a new instance of the class. /// - public class DefaultImageGenerator : IImageGenerator + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public DefaultImageGenerator( + IImageEncoder imageEncoder, + IItemRepository itemRepository, + ILogger logger) { - private readonly IImageEncoder _imageEncoder; - private readonly IItemRepository _itemRepository; - private readonly ILogger _logger; + _imageEncoder = imageEncoder; + _itemRepository = itemRepository; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public DefaultImageGenerator( - IImageEncoder imageEncoder, - IItemRepository itemRepository, - ILogger logger) + /// + public IReadOnlyList GetSupportedImages() + { + return new[] { GeneratedImageType.Splashscreen }; + } + + /// + public void Generate(GeneratedImageType imageTypeType, string outputPath) + { + var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); + var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); + if (landscape.Count == 0) { - _imageEncoder = imageEncoder; - _itemRepository = itemRepository; - _logger = logger; + // Thumb images fit better because they include the title in the image but are not provided with TMDb. + // Using backdrops as a fallback to generate an image at all + _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen"); + landscape = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); } - /// - public GeneratedImages[] GetSupportedImages() - { - return new[] { GeneratedImages.Splashscreen }; - } + var splashBuilder = new SplashscreenBuilder((SkiaEncoder)_imageEncoder); + splashBuilder.GenerateSplash(posters, landscape, outputPath); + } - /// - public void GenerateSplashscreen(string outputPath) + private IReadOnlyList GetItemsWithImageType(ImageType imageType) + { + // todo make included libraries configurable + return _itemRepository.GetItemList(new InternalItemsQuery { - var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); - var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); - if (landscape.Count == 0) + CollapseBoxSetItems = false, + Recursive = true, + DtoOptions = new DtoOptions(false), + ImageTypes = new[] { imageType }, + Limit = 30, + // todo max parental rating configurable + MaxParentalRating = 10, + OrderBy = new ValueTuple[] { - // Thumb images fit better because they include the title in the image but are not provided with TMDb. - // Using backdrops as a fallback to generate an image at all - _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen."); - landscape = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); - } - - var splashBuilder = new SplashscreenBuilder((SkiaEncoder)_imageEncoder); - splashBuilder.GenerateSplash(posters, landscape, outputPath); - } - - private IReadOnlyList GetItemsWithImageType(ImageType imageType) - { - // todo make included libraries configurable - return _itemRepository.GetItemList(new InternalItemsQuery - { - CollapseBoxSetItems = false, - Recursive = true, - DtoOptions = new DtoOptions(false), - ImageTypes = new ImageType[] { imageType }, - Limit = 30, - // todo max parental rating configurable - MaxParentalRating = 10, - OrderBy = new ValueTuple[] - { - new ValueTuple(ItemSortBy.Random, SortOrder.Ascending) - }, - IncludeItemTypes = new string[] { "Movie", "Series" } - }); - } + new(ItemSortBy.Random, SortOrder.Ascending) + }, + IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series } + }); } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index ac7ab2dee0..b2f9a518a4 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -86,7 +86,7 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(); // TODO search plugins - ServiceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton(); diff --git a/MediaBrowser.Controller/Drawing/GeneratedImageType.cs b/MediaBrowser.Controller/Drawing/GeneratedImageType.cs new file mode 100644 index 0000000000..a8db86d4f6 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/GeneratedImageType.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Controller.Drawing; + +/// +/// Which generated image type the supports. +/// +public enum GeneratedImageType +{ + /// + /// The splashscreen. + /// + Splashscreen = 0 +} diff --git a/MediaBrowser.Controller/Drawing/GeneratedImages.cs b/MediaBrowser.Controller/Drawing/GeneratedImages.cs deleted file mode 100644 index 47b60979b2..0000000000 --- a/MediaBrowser.Controller/Drawing/GeneratedImages.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MediaBrowser.Controller.Drawing -{ - /// - /// Which generated images an supports. - /// - public enum GeneratedImages - { - /// - /// The splashscreen. - /// - Splashscreen - } -} diff --git a/MediaBrowser.Controller/Drawing/IImageGenerator.cs b/MediaBrowser.Controller/Drawing/IImageGenerator.cs index f7e7f83b2e..773db02cb9 100644 --- a/MediaBrowser.Controller/Drawing/IImageGenerator.cs +++ b/MediaBrowser.Controller/Drawing/IImageGenerator.cs @@ -1,20 +1,22 @@ -namespace MediaBrowser.Controller.Drawing +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Drawing; + +/// +/// Interface for an image generator. +/// +public interface IImageGenerator { /// - /// Interface for an image generator. + /// Gets the supported generated images of the image generator. /// - public interface IImageGenerator - { - /// - /// Gets the supported generated images of the image generator. - /// - /// The supported images. - GeneratedImages[] GetSupportedImages(); + /// The supported generated image types. + IReadOnlyList GetSupportedImages(); - /// - /// Generates a splashscreen. - /// - /// The path where the splashscreen should be saved. - void GenerateSplashscreen(string outputPath); - } + /// + /// Generates a splashscreen. + /// + /// The image to generate. + /// The path where the splashscreen should be saved. + void Generate(GeneratedImageType imageTypeType, string outputPath); } diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index 56e5a87152..01db708856 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -1,38 +1,38 @@ using System.Text.Json.Serialization; using System.Xml.Serialization; -#pragma warning disable CS1591 +namespace MediaBrowser.Model.Branding; -namespace MediaBrowser.Model.Branding +/// +/// The branding options. +/// +public class BrandingOptions { - public class BrandingOptions - { - /// - /// Gets or sets the login disclaimer. - /// - /// The login disclaimer. - public string? LoginDisclaimer { get; set; } + /// + /// Gets or sets the login disclaimer. + /// + /// The login disclaimer. + public string? LoginDisclaimer { get; set; } - /// - /// Gets or sets the custom CSS. - /// - /// The custom CSS. - public string? CustomCss { get; set; } + /// + /// Gets or sets the custom CSS. + /// + /// The custom CSS. + public string? CustomCss { get; set; } - /// - /// Gets or sets the splashscreen location on disk. - /// - /// - /// Not served via the API. - /// Only used to save the custom uploaded user splashscreen in the configuration file. - /// - [JsonIgnore] - public string? SplashscreenLocation { get; set; } + /// + /// Gets or sets the splashscreen location on disk. + /// + /// + /// Not served via the API. + /// Only used to save the custom uploaded user splashscreen in the configuration file. + /// + [JsonIgnore] + public string? SplashscreenLocation { get; set; } - /// - /// Gets the splashscreen url. - /// - [XmlIgnore] - public string? SplashscreenUrl => "/Branding/Splashscreen"; - } + /// + /// Gets the splashscreen url. + /// + [XmlIgnore] + public string SplashscreenUrl => "/Branding/Splashscreen"; } From ecb73168b34e3d58dff186b6d90fb4bdd192e24a Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 10 Jan 2022 08:25:46 -0700 Subject: [PATCH 13/16] Suggestions from review --- Emby.Drawing/NullImageEncoder.cs | 6 +++ .../Library/SplashscreenPostScanTask.cs | 51 +++++++++---------- .../Tasks/RefreshMediaLibraryTask.cs | 17 +------ Jellyfin.Api/Controllers/ImageController.cs | 21 ++++---- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 8 +++ Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 14 ++--- Jellyfin.Server/CoreAppHost.cs | 3 -- .../Drawing/IImageEncoder.cs | 7 +++ .../Drawing/IImageGenerator.cs | 22 -------- 9 files changed, 64 insertions(+), 85 deletions(-) rename Jellyfin.Drawing.Skia/DefaultImageGenerator.cs => Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs (66%) delete mode 100644 MediaBrowser.Controller/Drawing/IImageGenerator.cs diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index 1c05aa9161..d0a26b713b 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -43,6 +43,12 @@ namespace Emby.Drawing throw new NotImplementedException(); } + /// + public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) + { + throw new NotImplementedException(); + } + /// public string GetImageBlurHash(int xComp, int yComp, string path) { diff --git a/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs similarity index 66% rename from Jellyfin.Drawing.Skia/DefaultImageGenerator.cs rename to Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index e102b8f49a..65445d3aff 100644 --- a/Jellyfin.Drawing.Skia/DefaultImageGenerator.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -1,68 +1,65 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; -namespace Jellyfin.Drawing.Skia; +namespace Emby.Server.Implementations.Library; /// -/// The default image generator. +/// The splashscreen post scan task. /// -public class DefaultImageGenerator : IImageGenerator +public class SplashscreenPostScanTask : ILibraryPostScanTask { - private readonly IImageEncoder _imageEncoder; private readonly IItemRepository _itemRepository; - private readonly ILogger _logger; + private readonly IImageEncoder _imageEncoder; + private readonly ILogger _logger; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - public DefaultImageGenerator( - IImageEncoder imageEncoder, + /// Instance of the interface. + /// Instance of the interface. + public SplashscreenPostScanTask( IItemRepository itemRepository, - ILogger logger) + IImageEncoder imageEncoder, + ILogger logger) { - _imageEncoder = imageEncoder; _itemRepository = itemRepository; + _imageEncoder = imageEncoder; _logger = logger; } - /// - public IReadOnlyList GetSupportedImages() - { - return new[] { GeneratedImageType.Splashscreen }; - } - - /// - public void Generate(GeneratedImageType imageTypeType, string outputPath) + /// + public Task Run(IProgress progress, CancellationToken cancellationToken) { var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); - var landscape = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); - if (landscape.Count == 0) + var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); + if (backdrops.Count == 0) { // Thumb images fit better because they include the title in the image but are not provided with TMDb. // Using backdrops as a fallback to generate an image at all _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen"); - landscape = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); + backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); } - var splashBuilder = new SplashscreenBuilder((SkiaEncoder)_imageEncoder); - splashBuilder.GenerateSplash(posters, landscape, outputPath); + _imageEncoder.CreateSplashscreen(posters, backdrops); + return Task.CompletedTask; } private IReadOnlyList GetItemsWithImageType(ImageType imageType) { - // todo make included libraries configurable + // TODO make included libraries configurable return _itemRepository.GetItemList(new InternalItemsQuery { CollapseBoxSetItems = false, @@ -70,7 +67,7 @@ public class DefaultImageGenerator : IImageGenerator DtoOptions = new DtoOptions(false), ImageTypes = new[] { imageType }, Limit = 30, - // todo max parental rating configurable + // TODO max parental rating configurable MaxParentalRating = 10, OrderBy = new ValueTuple[] { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index c12fc21130..7c27ae3844 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -2,12 +2,9 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; @@ -24,26 +21,16 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; - private readonly IImageGenerator _imageGenerator; - private readonly IApplicationPaths _applicationPaths; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public RefreshMediaLibraryTask( - ILibraryManager libraryManager, - ILocalizationManager localization, - IImageGenerator imageGenerator, - IApplicationPaths applicationPaths) + public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization) { _libraryManager = libraryManager; _localization = localization; - _imageGenerator = imageGenerator; - _applicationPaths = applicationPaths; } /// @@ -83,8 +70,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks progress.Report(0); - _imageGenerator.Generate(GeneratedImageType.Splashscreen, Path.Combine(_applicationPaths.DataPath, "splashscreen.webp")); - return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 6d34ca7708..b44a21d033 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers private readonly ILogger _logger; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IApplicationPaths _appPaths; - private readonly IImageGenerator _imageGenerator; + private readonly IImageEncoder _imageEncoder; /// /// Initializes a new instance of the class. @@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the interface. public ImageController( IUserManager userManager, ILibraryManager libraryManager, @@ -73,7 +73,7 @@ namespace Jellyfin.Api.Controllers ILogger logger, IServerConfigurationManager serverConfigurationManager, IApplicationPaths appPaths, - IImageGenerator imageGenerator) + IImageEncoder imageEncoder) { _userManager = userManager; _libraryManager = libraryManager; @@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers _logger = logger; _serverConfigurationManager = serverConfigurationManager; _appPaths = appPaths; - _imageGenerator = imageGenerator; + _imageEncoder = imageEncoder; } /// @@ -1737,19 +1737,20 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? foregroundLayer, [FromQuery, Range(0, 100)] int quality = 90) { - string splashscreenPath; var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); - if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)) + string splashscreenPath; + + if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) + && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) { - splashscreenPath = brandingOptions.SplashscreenLocation!; + splashscreenPath = brandingOptions.SplashscreenLocation; } else { splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.webp"); - - if (!System.IO.File.Exists(splashscreenPath) && _imageGenerator.GetSupportedImages().Contains(GeneratedImageType.Splashscreen)) + if (!System.IO.File.Exists(splashscreenPath)) { - _imageGenerator.Generate(GeneratedImageType.Splashscreen, splashscreenPath); + return NotFound(); } } diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 6d0a5ac2b9..c40103f827 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -492,6 +492,14 @@ namespace Jellyfin.Drawing.Skia } } + /// + public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) + { + var splashBuilder = new SplashscreenBuilder(this); + var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.webp"); + splashBuilder.GenerateSplash(posters, backdrops, outputPath); + } + private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) { try diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 9f801c3208..132c35e67c 100644 --- a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -32,12 +32,12 @@ namespace Jellyfin.Drawing.Skia /// Generate a splashscreen. /// /// The poster paths. - /// The landscape paths. + /// The landscape paths. /// The output path. - public void GenerateSplash(IReadOnlyList posters, IReadOnlyList backdrop, string outputPath) + public void GenerateSplash(IReadOnlyList posters, IReadOnlyList backdrops, string outputPath) { - var wall = GenerateCollage(posters, backdrop); - var transformed = Transform3D(wall); + using var wall = GenerateCollage(posters, backdrops); + using var transformed = Transform3D(wall); using var outputStream = new SKFileWStream(outputPath); using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels()); @@ -48,9 +48,9 @@ namespace Jellyfin.Drawing.Skia /// Generates a collage of posters and landscape pictures. /// /// The poster paths. - /// The landscape paths. + /// The landscape paths. /// The created collage as a bitmap. - private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrop) + private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrops) { var random = new Random(); @@ -82,7 +82,7 @@ namespace Jellyfin.Drawing.Skia posterIndex = newPosterIndex; break; default: - currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrop, backdropIndex, out int newBackdropIndex); + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex); backdropIndex = newBackdropIndex; break; } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index b2f9a518a4..67e50b92d9 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -85,9 +85,6 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - // TODO search plugins - serviceCollection.AddSingleton(); - // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 4e67cfee4f..e5c8ebfaf9 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -74,5 +74,12 @@ namespace MediaBrowser.Controller.Drawing /// The options to use when creating the collage. /// Optional. void CreateImageCollage(ImageCollageOptions options, string? libraryName); + + /// + /// Creates a new splashscreen image. + /// + /// The list of poster paths. + /// The list of backdrop paths. + void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops); } } diff --git a/MediaBrowser.Controller/Drawing/IImageGenerator.cs b/MediaBrowser.Controller/Drawing/IImageGenerator.cs deleted file mode 100644 index 773db02cb9..0000000000 --- a/MediaBrowser.Controller/Drawing/IImageGenerator.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; - -namespace MediaBrowser.Controller.Drawing; - -/// -/// Interface for an image generator. -/// -public interface IImageGenerator -{ - /// - /// Gets the supported generated images of the image generator. - /// - /// The supported generated image types. - IReadOnlyList GetSupportedImages(); - - /// - /// Generates a splashscreen. - /// - /// The image to generate. - /// The path where the splashscreen should be saved. - void Generate(GeneratedImageType imageTypeType, string outputPath); -} From 6520ad03f055af5127b44d4afe310f0786bc6275 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 10 Jan 2022 08:30:55 -0700 Subject: [PATCH 14/16] Fix release build --- .../Library/SplashscreenPostScanTask.cs | 2 +- .../Drawing/GeneratedImageType.cs | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 MediaBrowser.Controller/Drawing/GeneratedImageType.cs diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 65445d3aff..aed5711fa6 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -29,7 +29,7 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the interface. public SplashscreenPostScanTask( IItemRepository itemRepository, IImageEncoder imageEncoder, diff --git a/MediaBrowser.Controller/Drawing/GeneratedImageType.cs b/MediaBrowser.Controller/Drawing/GeneratedImageType.cs deleted file mode 100644 index a8db86d4f6..0000000000 --- a/MediaBrowser.Controller/Drawing/GeneratedImageType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MediaBrowser.Controller.Drawing; - -/// -/// Which generated image type the supports. -/// -public enum GeneratedImageType -{ - /// - /// The splashscreen. - /// - Splashscreen = 0 -} From 0d335082c8cf541637f4bcae1dc0399f649d24ce Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 10 Jan 2022 10:59:32 -0700 Subject: [PATCH 15/16] suggestions from review --- .../Library/SplashscreenPostScanTask.cs | 4 ++-- Jellyfin.Api/Controllers/ImageController.cs | 2 +- Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 4 +--- MediaBrowser.Model/Branding/BrandingOptions.cs | 6 ------ 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index aed5711fa6..320685b1f1 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -69,9 +69,9 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask Limit = 30, // TODO max parental rating configurable MaxParentalRating = 10, - OrderBy = new ValueTuple[] + OrderBy = new[] { - new(ItemSortBy.Random, SortOrder.Ascending) + (ItemSortBy.Random, SortOrder.Ascending) }, IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series } }); diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index b44a21d033..7cc526d218 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1793,7 +1793,7 @@ namespace Jellyfin.Api.Controllers /// Uploads a custom splashscreen. /// /// A indicating success. - /// Sucessfully uploaded new splashscreen. + /// Successfully uploaded new splashscreen. /// Error reading MimeType from uploaded image. /// User does not have permission to upload splashscreen.. /// Error reading the image format. diff --git a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs index 132c35e67c..e5fa6c2bd1 100644 --- a/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -52,8 +52,6 @@ namespace Jellyfin.Drawing.Skia /// The created collage as a bitmap. private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList backdrops) { - var random = new Random(); - var posterIndex = 0; var backdropIndex = 0; @@ -65,7 +63,7 @@ namespace Jellyfin.Drawing.Skia for (int i = 0; i < Rows; i++) { - int imageCounter = random.Next(0, 5); + int imageCounter = Random.Shared.Next(0, 5); int currentWidthPos = i * 75; int currentHeight = i * (posterHeight + Spacing); diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index 01db708856..cc42c1718a 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -29,10 +29,4 @@ public class BrandingOptions /// [JsonIgnore] public string? SplashscreenLocation { get; set; } - - /// - /// Gets the splashscreen url. - /// - [XmlIgnore] - public string SplashscreenUrl => "/Branding/Splashscreen"; } From 8a36fe7ed51d1a1da7c629e10ff9eebf81cf9785 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 10 Jan 2022 17:01:17 -0700 Subject: [PATCH 16/16] Use png for storage --- Jellyfin.Api/Controllers/ImageController.cs | 2 +- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 7cc526d218..5997ac549b 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1747,7 +1747,7 @@ namespace Jellyfin.Api.Controllers } else { - splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.webp"); + splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); if (!System.IO.File.Exists(splashscreenPath)) { return NotFound(); diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index c40103f827..1fa8e570da 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -496,7 +496,7 @@ namespace Jellyfin.Drawing.Skia public void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops) { var splashBuilder = new SplashscreenBuilder(this); - var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.webp"); + var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); splashBuilder.GenerateSplash(posters, backdrops, outputPath); }