using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers { /// /// Remote Images Controller. /// [Route("")] public class RemoteImageController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; private readonly IServerApplicationPaths _applicationPaths; private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public RemoteImageController( IProviderManager providerManager, IServerApplicationPaths applicationPaths, IHttpClientFactory httpClientFactory, ILibraryManager libraryManager) { _providerManager = providerManager; _applicationPaths = applicationPaths; _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; } /// /// Gets available remote images for an item. /// /// Item Id. /// The image type. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. /// Optional. The image provider to use. /// Optional. Include all languages. /// Remote Images returned. /// Item not found. /// Remote Image Result. [HttpGet("Items/{itemId}/RemoteImages")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetRemoteImages( [FromRoute, Required] Guid itemId, [FromQuery] ImageType? type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? providerName, [FromQuery] bool includeAllLanguages = false) { var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); } var images = await _providerManager.GetAvailableRemoteImages( item, new RemoteImageQuery(providerName ?? string.Empty) { IncludeAllLanguages = includeAllLanguages, IncludeDisabledProviders = true, ImageType = type }, CancellationToken.None) .ConfigureAwait(false); var imageArray = images.ToArray(); var allProviders = _providerManager.GetRemoteImageProviderInfo(item); if (type.HasValue) { allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value)); } var result = new RemoteImageResult { TotalRecordCount = imageArray.Length, Providers = allProviders.Select(o => o.Name) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() }; if (startIndex.HasValue) { imageArray = imageArray.Skip(startIndex.Value).ToArray(); } if (limit.HasValue) { imageArray = imageArray.Take(limit.Value).ToArray(); } result.Images = imageArray; return result; } /// /// Gets available remote image providers for an item. /// /// Item Id. /// Returned remote image providers. /// Item not found. /// List of remote image providers. [HttpGet("Items/{itemId}/RemoteImages/Providers")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetRemoteImageProviders([FromRoute, Required] Guid itemId) { var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); } return Ok(_providerManager.GetRemoteImageProviderInfo(item)); } /// /// Gets a remote image. /// /// The image url. /// Remote image returned. /// Remote image not found. /// Image Stream. [HttpGet("Images/Remote")] [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetRemoteImage([FromQuery, Required] string imageUrl) { var urlHash = imageUrl.GetMD5(); var pointerCachePath = GetFullCachePath(urlHash.ToString()); string? contentPath = null; var hasFile = false; try { contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); if (System.IO.File.Exists(contentPath)) { hasFile = true; } } catch (FileNotFoundException) { // The file isn't cached yet } catch (IOException) { // The file isn't cached yet } if (!hasFile) { await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); } if (string.IsNullOrEmpty(contentPath)) { return NotFound(); } var contentType = MimeTypes.GetMimeType(contentPath); return PhysicalFile(contentPath, contentType); } /// /// Downloads a remote image for an item. /// /// Item Id. /// The image type. /// The image url. /// Remote image downloaded. /// Remote image not found. /// Download status. [HttpPost("Items/{itemId}/RemoteImages/Download")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DownloadRemoteImage( [FromRoute, Required] Guid itemId, [FromQuery, Required] ImageType type, [FromQuery] string? imageUrl) { var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); } await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) .ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Gets the full cache path. /// /// The filename. /// System.String. private string GetFullCachePath(string filename) { return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } /// /// Downloads the image. /// /// The URL. /// The URL hash. /// The pointer cache path. /// Task. private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) { var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); using var response = await httpClient.GetAsync(url).ConfigureAwait(false); var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1]; var fullCachePath = GetFullCachePath(urlHash + "." + ext); Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None) .ConfigureAwait(false); } } }