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/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
new file mode 100644
index 0000000000..320685b1f1
--- /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;
+
+///
+/// The splashscreen post scan task.
+///
+public class SplashscreenPostScanTask : ILibraryPostScanTask
+{
+ private readonly IItemRepository _itemRepository;
+ private readonly IImageEncoder _imageEncoder;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public SplashscreenPostScanTask(
+ IItemRepository itemRepository,
+ IImageEncoder imageEncoder,
+ ILogger logger)
+ {
+ _itemRepository = itemRepository;
+ _imageEncoder = imageEncoder;
+ _logger = logger;
+ }
+
+ ///
+ public Task Run(IProgress 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 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 f7b3cfedcc..7c27ae3844 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
///
/// Initializes a new instance of the class.
///
- /// The library manager.
- /// The localization manager.
+ /// Instance of the interface.
+ /// Instance of the interface.
public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization)
{
_libraryManager = libraryManager;
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index e72589cfae..aafffc2a1d 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 _logger;
private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IImageEncoder _imageEncoder;
///
/// Initializes a new instance of the class.
@@ -56,6 +61,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 +71,9 @@ namespace Jellyfin.Api.Controllers
IFileSystem fileSystem,
IAuthorizationContext authContext,
ILogger 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;
}
///
@@ -1692,6 +1703,130 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
}
+ ///
+ /// Generates or gets the splashscreen.
+ ///
+ /// 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.
+ /// Width of box to fill.
+ /// Height of box to fill.
+ /// 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")]
+ [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? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery, Range(0, 100)] int quality = 90)
+ {
+ var brandingOptions = _serverConfigurationManager.GetConfiguration("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.Empty,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Uploads a custom splashscreen.
+ ///
+ /// A indicating success.
+ /// Successfully 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.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [AcceptsImageFile]
+ public async Task 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("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 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 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/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 6d0a5ac2b9..1fa8e570da 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.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 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..e5fa6c2bd1
--- /dev/null
+++ b/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia
+{
+ ///
+ /// Used to build the splashscreen.
+ ///
+ 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;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The SkiaEncoder.
+ public SplashscreenBuilder(SkiaEncoder skiaEncoder)
+ {
+ _skiaEncoder = skiaEncoder;
+ }
+
+ ///
+ /// Generate a splashscreen.
+ ///
+ /// The poster paths.
+ /// The landscape paths.
+ /// The output path.
+ public void GenerateSplash(IReadOnlyList posters, IReadOnlyList 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);
+ }
+
+ ///
+ /// Generates a collage of posters and landscape pictures.
+ ///
+ /// The poster paths.
+ /// The landscape paths.
+ /// The created collage as a bitmap.
+ private SKBitmap GenerateCollage(IReadOnlyList posters, IReadOnlyList 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;
+ }
+
+ ///
+ /// 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..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.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs
index 7f19a5b852..cc42c1718a 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;
+
+///
+/// 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; }
}