diff options
| -rw-r--r-- | Emby.Drawing/NullImageEncoder.cs | 6 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs | 79 | ||||
| -rw-r--r-- | Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs | 4 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ImageController.cs | 231 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/SkiaEncoder.cs | 8 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/SkiaHelper.cs | 37 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/SplashscreenBuilder.cs | 148 | ||||
| -rw-r--r-- | Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 32 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Drawing/IImageEncoder.cs | 7 | ||||
| -rw-r--r-- | MediaBrowser.Model/Branding/BrandingOptions.cs | 43 |
10 files changed, 483 insertions, 112 deletions
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index 1c05aa916..d0a26b713 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -44,6 +44,12 @@ namespace Emby.Drawing } /// <inheritdoc /> + public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> public string GetImageBlurHash(int xComp, int yComp, string path) { throw new NotImplementedException(); diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs new file mode 100644 index 000000000..320685b1f --- /dev/null +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -0,0 +1,79 @@ +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 Emby.Server.Implementations.Library; + +/// <summary> +/// The splashscreen post scan task. +/// </summary> +public class SplashscreenPostScanTask : ILibraryPostScanTask +{ + private readonly IItemRepository _itemRepository; + private readonly IImageEncoder _imageEncoder; + private readonly ILogger<SplashscreenPostScanTask> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="SplashscreenPostScanTask"/> class. + /// </summary> + /// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param> + /// <param name="imageEncoder">Instance of the <see cref="IImageEncoder"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{SplashscreenPostScanTask}"/> interface.</param> + public SplashscreenPostScanTask( + IItemRepository itemRepository, + IImageEncoder imageEncoder, + ILogger<SplashscreenPostScanTask> logger) + { + _itemRepository = itemRepository; + _imageEncoder = imageEncoder; + _logger = logger; + } + + /// <inheritdoc /> + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); + 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"); + backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); + } + + _imageEncoder.CreateSplashscreen(posters, backdrops); + return Task.CompletedTask; + } + + private IReadOnlyList<BaseItem> GetItemsWithImageType(ImageType imageType) + { + // TODO make included libraries configurable + return _itemRepository.GetItemList(new InternalItemsQuery + { + CollapseBoxSetItems = false, + Recursive = true, + DtoOptions = new DtoOptions(false), + ImageTypes = new[] { imageType }, + Limit = 30, + // TODO max parental rating configurable + MaxParentalRating = 10, + OrderBy = new[] + { + (ItemSortBy.Random, SortOrder.Ascending) + }, + IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series } + }); + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index f7b3cfedc..7c27ae384 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -25,8 +25,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="localization">The localization manager.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization) { _libraryManager = libraryManager; diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index e72589cfa..aafffc2a1 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; @@ -11,12 +12,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 +47,8 @@ namespace Jellyfin.Api.Controllers private readonly IAuthorizationContext _authContext; private readonly ILogger<ImageController> _logger; private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IApplicationPaths _appPaths; + private readonly IImageEncoder _imageEncoder; /// <summary> /// Initializes a new instance of the <see cref="ImageController"/> class. @@ -56,6 +61,8 @@ namespace Jellyfin.Api.Controllers /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> + /// <param name="imageEncoder">Instance of the <see cref="IImageEncoder"/> interface.</param> public ImageController( IUserManager userManager, ILibraryManager libraryManager, @@ -64,7 +71,9 @@ namespace Jellyfin.Api.Controllers IFileSystem fileSystem, IAuthorizationContext authContext, ILogger<ImageController> logger, - IServerConfigurationManager serverConfigurationManager) + IServerConfigurationManager serverConfigurationManager, + IApplicationPaths appPaths, + IImageEncoder imageEncoder) { _userManager = userManager; _libraryManager = libraryManager; @@ -74,6 +83,8 @@ namespace Jellyfin.Api.Controllers _authContext = authContext; _logger = logger; _serverConfigurationManager = serverConfigurationManager; + _appPaths = appPaths; + _imageEncoder = imageEncoder; } /// <summary> @@ -1692,6 +1703,130 @@ namespace Jellyfin.Api.Controllers .ConfigureAwait(false); } + /// <summary> + /// Generates or gets the splashscreen. + /// </summary> + /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param> + /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param> + /// <param name="maxWidth">The maximum image width to return.</param> + /// <param name="maxHeight">The maximum image height to return.</param> + /// <param name="width">The fixed image width to return.</param> + /// <param name="height">The fixed image height to return.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> + /// <param name="blur">Blur image.</param> + /// <param name="backgroundColor">Apply a background color for transparent images.</param> + /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param> + /// <param name="quality">Quality setting, from 0-100.</param> + /// <response code="200">Splashscreen returned successfully.</response> + /// <returns>The splashscreen.</returns> + [HttpGet("Branding/Splashscreen")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesImageFile] + public async Task<ActionResult> GetSplashscreen( + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery, Range(0, 100)] int quality = 90) + { + var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + string splashscreenPath; + + if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) + && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) + { + splashscreenPath = brandingOptions.SplashscreenLocation; + } + else + { + splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + if (!System.IO.File.Exists(splashscreenPath)) + { + return NotFound(); + } + } + + 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 = height, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, + Quality = quality, + Width = width, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = outputFormats + }; + + return await GetImageResult( + options, + cacheDuration, + ImmutableDictionary<string, string>.Empty, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// <summary> + /// Uploads a custom splashscreen. + /// </summary> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + /// <response code="204">Successfully uploaded new splashscreen.</response> + /// <response code="400">Error reading MimeType from uploaded image.</response> + /// <response code="403">User does not have permission to upload splashscreen..</response> + /// <exception cref="ArgumentException">Error reading the image format.</exception> + [HttpPost("Branding/Splashscreen")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [AcceptsImageFile] + public async Task<ActionResult> UploadCustomSplashscreen() + { + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + + var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; + + if (!mimeType.HasValue) + { + return BadRequest("Error reading mimetype from uploaded image"); + } + + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value)); + var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + brandingOptions.SplashscreenLocation = filePath; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + + 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<MemoryStream> GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); @@ -1823,25 +1958,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); @@ -1921,56 +2066,12 @@ namespace Jellyfin.Api.Controllers } private async Task<ActionResult> 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<ImageFormat> supportedFormats, + ImageProcessingOptions imageProcessingOptions, TimeSpan? cacheDuration, IDictionary<string, string> 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/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 6d0a5ac2b..1fa8e570d 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -492,6 +492,14 @@ namespace Jellyfin.Drawing.Skia } } + /// <inheritdoc /> + public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops) + { + var splashBuilder = new SplashscreenBuilder(this); + var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + splashBuilder.GenerateSplash(posters, backdrops, outputPath); + } + 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 f9c79c855..35dcebdab 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); } } + + /// <summary> + /// Gets the next valid image as a bitmap. + /// </summary> + /// <param name="skiaEncoder">The current skia encoder.</param> + /// <param name="paths">The list of image paths.</param> + /// <param name="currentIndex">The current checked indes.</param> + /// <param name="newIndex">The new index.</param> + /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns> + public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex) + { + var imagesTested = new Dictionary<int, int>(); + 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 000000000..e5fa6c2bd --- /dev/null +++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using SkiaSharp; + +namespace Jellyfin.Drawing.Skia +{ + /// <summary> + /// Used to build the splashscreen. + /// </summary> + 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; + + /// <summary> + /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class. + /// </summary> + /// <param name="skiaEncoder">The SkiaEncoder.</param> + public SplashscreenBuilder(SkiaEncoder skiaEncoder) + { + _skiaEncoder = skiaEncoder; + } + + /// <summary> + /// Generate a splashscreen. + /// </summary> + /// <param name="posters">The poster paths.</param> + /// <param name="backdrops">The landscape paths.</param> + /// <param name="outputPath">The output path.</param> + public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath) + { + 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()); + pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90); + } + + /// <summary> + /// Generates a collage of posters and landscape pictures. + /// </summary> + /// <param name="posters">The poster paths.</param> + /// <param name="backdrops">The landscape paths.</param> + /// <returns>The created collage as a bitmap.</returns> + private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops) + { + var posterIndex = 0; + var backdropIndex = 0; + + var bitmap = new SKBitmap(WallWidth, WallHeight); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); + + int posterHeight = WallHeight / 6; + + for (int i = 0; i < Rows; i++) + { + int imageCounter = Random.Shared.Next(0, 5); + int currentWidthPos = i * 75; + int currentHeight = i * (posterHeight + Spacing); + + while (currentWidthPos < WallWidth) + { + SKBitmap? currentImage; + + switch (imageCounter) + { + case 0: + case 2: + case 3: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex); + posterIndex = newPosterIndex; + break; + default: + currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, 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++; + } + } + } + + return bitmap; + } + + /// <summary> + /// Transform the collage in 3D space. + /// </summary> + /// <param name="input">The bitmap to transform.</param> + /// <returns>The transformed image.</returns> + 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 d1cc2255d..6bece9db6 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<string> paths, int currentIndex, out int newIndex) - { - var imagesTested = new Dictionary<int, int>(); - 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<string> 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 4e67cfee4..e5c8ebfaf 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -74,5 +74,12 @@ namespace MediaBrowser.Controller.Drawing /// <param name="options">The options to use when creating the collage.</param> /// <param name="libraryName">Optional. </param> void CreateImageCollage(ImageCollageOptions options, string? libraryName); + + /// <summary> + /// Creates a new splashscreen image. + /// </summary> + /// <param name="posters">The list of poster paths.</param> + /// <param name="backdrops">The list of backdrop paths.</param> + void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops); } } diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index 7f19a5b85..cc42c1718 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -1,19 +1,32 @@ -#pragma warning disable CS1591 +using System.Text.Json.Serialization; +using System.Xml.Serialization; -namespace MediaBrowser.Model.Branding +namespace MediaBrowser.Model.Branding; + +/// <summary> +/// The branding options. +/// </summary> +public class BrandingOptions { - public class BrandingOptions - { - /// <summary> - /// Gets or sets the login disclaimer. - /// </summary> - /// <value>The login disclaimer.</value> - public string? LoginDisclaimer { get; set; } + /// <summary> + /// Gets or sets the login disclaimer. + /// </summary> + /// <value>The login disclaimer.</value> + public string? LoginDisclaimer { get; set; } + + /// <summary> + /// Gets or sets the custom CSS. + /// </summary> + /// <value>The custom CSS.</value> + public string? CustomCss { get; set; } - /// <summary> - /// Gets or sets the custom CSS. - /// </summary> - /// <value>The custom CSS.</value> - public string? CustomCss { get; set; } - } + /// <summary> + /// Gets or sets the splashscreen location on disk. + /// </summary> + /// <remarks> + /// Not served via the API. + /// Only used to save the custom uploaded user splashscreen in the configuration file. + /// </remarks> + [JsonIgnore] + public string? SplashscreenLocation { get; set; } } |
