From e026ba84c5e0d0a761cb1cae403e6f601e6fa2e0 Mon Sep 17 00:00:00 2001 From: David Ullmer Date: Tue, 17 Aug 2021 18:12:45 +0200 Subject: [PATCH] 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; } } }