using System; using System.IO; using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; using API.Entities.Enums; using API.Extensions; using API.Middleware; using API.Services; using API.Services.Tasks.Metadata; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; #nullable enable /// /// Responsible for servicing up images stored in Kavita for entities /// [AllowAnonymous] [SkipDeviceTracking] public class ImageController : BaseApiController { private readonly IUnitOfWork _unitOfWork; private readonly IDirectoryService _directoryService; private readonly ILocalizationService _localizationService; private readonly IReadingListService _readingListService; private readonly ICoverDbService _coverDbService; /// public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILocalizationService localizationService, IReadingListService readingListService, ICoverDbService coverDbService) { _unitOfWork = unitOfWork; _directoryService = directoryService; _localizationService = localizationService; _readingListService = readingListService; _coverDbService = coverDbService; } /// /// Returns cover image for Chapter /// /// /// /// [HttpGet("chapter-cover")] public async Task GetChapterCoverImage(int chapterId, string apiKey) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); return CachedFile(path); } /// /// Returns cover image for Library /// /// /// /// [HttpGet("library-cover")] public async Task GetLibraryCoverImage(int libraryId, string apiKey) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); return CachedFile(path); } /// /// Returns cover image for Volume /// /// /// /// [HttpGet("volume-cover")] public async Task GetVolumeCoverImage(int volumeId, string apiKey) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); return CachedFile(path); } /// /// Returns cover image for Series /// /// Id of Series /// /// [HttpGet("series-cover")] public async Task GetSeriesCoverImage(int seriesId, string apiKey) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); return CachedFile(path); } /// /// Returns cover image for Collection /// /// /// /// [HttpGet("collection-cover")] public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) { // TODO: Streamline this like ReadingList does path = await GenerateCollectionCoverImage(collectionTagId); } return CachedFile(path); } /// /// Returns cover image for a Reading List /// /// /// /// [HttpGet("readinglist-cover")] public async Task GetReadingListCoverImage(int readingListId, string apiKey) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) { path = await _readingListService.GenerateReadingListCoverImage(readingListId); } return CachedFile(path); } private async Task GenerateCollectionCoverImage(int collectionId) { var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId); var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, ImageService.GetCollectionTagFormat(collectionId)); var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); destFile += settings.EncodeMediaAs.GetExtension(); if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; ImageService.CreateMergedImage( covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), settings.CoverImageSize, destFile); // TODO: Refactor this so that collections have a dedicated cover image so we can calculate primary/secondary colors return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; } /// /// Returns image for a given bookmark page /// /// This request is served unauthenticated, but user must be passed via api key to validate /// /// Starts at 0 /// API Key for user. Needed to authenticate request /// Only applicable for Epubs - handles multiple images on one page /// [HttpGet("bookmark")] public async Task GetBookmarkImage(int chapterId, int pageNum, string apiKey, int imageOffset = 0) { var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, imageOffset, UserId); if (bookmark == null) return BadRequest(await _localizationService.Translate(UserId, "bookmark-doesnt-exist")); var bookmarkDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; var path = Path.Join(bookmarkDirectory, bookmark.FileName); return CachedFile(path); } /// /// Returns the image associated with a web-link /// /// /// /// [HttpGet("web-link")] public async Task GetWebLinkImage(string url, string apiKey) { if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(UserId, "must-be-defined", "Url")); var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; // Check if the domain exists var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url, encodeFormat)); if (!_directoryService.FileSystem.File.Exists(domainFilePath)) { // We need to request the favicon and save it try { domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, await _coverDbService.DownloadFaviconAsync(url, encodeFormat)); } catch (Exception) { return BadRequest(await _localizationService.Translate(UserId, "generic-favicon")); } } return CachedFile(domainFilePath); } /// /// Returns the image associated with a publisher /// /// /// /// [HttpGet("publisher")] public async Task GetPublisherImage(string publisherName, string apiKey) { if (string.IsNullOrEmpty(publisherName)) return BadRequest(await _localizationService.Translate(UserId, "must-be-defined", "publisherName")); if (publisherName.Contains("..")) return BadRequest(); var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; // Check if the domain exists var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, ImageService.GetPublisherFormat(publisherName, encodeFormat)); if (!_directoryService.FileSystem.File.Exists(domainFilePath)) { // We need to request the favicon and save it try { domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, await _coverDbService.DownloadPublisherImageAsync(publisherName, encodeFormat)); } catch (Exception) { return BadRequest(await _localizationService.Translate(UserId, "generic-favicon")); } } return CachedFile(domainFilePath); } /// /// Returns cover image for Person /// /// /// /// [HttpGet("person-cover")] public async Task GetPersonCoverImage(int personId, string apiKey) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.UserRepository.GetPersonCoverImageAsync(personId)); return CachedFile(path); } /// /// Returns cover image for User /// /// /// /// [HttpGet("user-cover")] public async Task GetUserCoverImage(int userId, string apiKey) { var filename = await _unitOfWork.UserRepository.GetCoverImageAsync(userId, UserId); var path = Path.Join(_directoryService.CoverImageDirectory, filename); return CachedFile(path); } /// /// Returns a temp coverupload image /// /// Requires Admin Role to perform upload /// Filename of file. This is used with upload/upload-by-url /// /// [HttpGet("cover-upload")] public async Task GetCoverUploadImage(string filename, string apiKey) { if (!UserContext.IsAuthenticated) return Unauthorized(); if (filename.Contains("..")) return BadRequest(await _localizationService.Translate(UserId, "invalid-filename")); var roles = await _unitOfWork.UserRepository.GetRolesByAuthKey(apiKey); if (!roles.Contains(PolicyConstants.AdminRole)) { return Forbid(); } var path = Path.Join(_directoryService.TempDirectory, filename); return CachedFile(path); } }