using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Kavita.API.Attributes; using Kavita.API.Database; using Kavita.API.Services; using Kavita.API.Services.Metadata; using Kavita.API.Services.Reading; using Kavita.Models.Constants; using Kavita.Models.Entities.Enums; using Kavita.Models.Extensions; using Kavita.Server.Attributes; using Kavita.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Kavita.Server.Controllers; /// /// Responsible for servicing up images stored in Kavita for entities /// /// [SkipDeviceTracking] public class ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILocalizationService localizationService, IReadingListService readingListService, ICoverDbService coverDbService, ICollectionTagService collectionTagService) : BaseApiController { /// /// Returns cover image for Chapter /// /// /// /// [ChapterAccess] [HttpGet("chapter-cover")] public async Task GetChapterCoverImage(int chapterId, string apiKey) { var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); return PhysicalFile(path); } /// /// Returns cover image for Library /// /// /// /// [LibraryAccess] [HttpGet("library-cover")] public async Task GetLibraryCoverImage(int libraryId, string apiKey) { var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); return PhysicalFile(path); } /// /// Returns cover image for Volume /// /// /// /// [VolumeAccess] [HttpGet("volume-cover")] public async Task GetVolumeCoverImage(int volumeId, string apiKey) { var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); return PhysicalFile(path); } /// /// Returns cover image for Series /// /// Id of Series /// /// [SeriesAccess] [HttpGet("series-cover")] public async Task GetSeriesCoverImage(int seriesId, string apiKey) { var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); return PhysicalFile(path); } /// /// Returns cover image for Collection /// /// /// /// [HttpGet("collection-cover")] public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) { var collectionTag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionTagId, ct: HttpContext.RequestAborted); if (collectionTag == null || (collectionTag.AppUserId != UserId && !collectionTag.Promoted)) return NotFound(); var path = Path.Join(directoryService.CoverImageDirectory, collectionTag.CoverImage); if (string.IsNullOrEmpty(path) || !directoryService.FileSystem.File.Exists(path)) { path = await collectionTagService.GenerateCollectionCoverImage(collectionTagId); } return PhysicalFile(path); } /// /// Returns cover image for a Reading List /// /// /// /// [HttpGet("readinglist-cover")] public async Task GetReadingListCoverImage(int readingListId, string apiKey) { var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId, ct: HttpContext.RequestAborted); if (readingList == null || (readingList.AppUserId != UserId && !readingList.Promoted)) return NotFound(); var path = Path.Join(directoryService.CoverImageDirectory, readingList.CoverImage); if (string.IsNullOrEmpty(path) || !directoryService.FileSystem.File.Exists(path)) { path = await readingListService.GenerateReadingListCoverImage(readingListId); } return PhysicalFile(path); } /// /// 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 /// [ChapterAccess] [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 PhysicalFile(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 PhysicalFile(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 /// /// /// /// [PersonAccess] [HttpGet("person-cover")] public async Task GetPersonCoverImage(int personId, string apiKey) { var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.UserRepository.GetPersonCoverImageAsync(personId)); return PhysicalFile(path); } /// /// Returns cover image for User /// /// /// /// [HttpGet("user-cover")] public async Task GetUserCoverImage(int userId, string apiKey) { var filename = await unitOfWork.UserRepository.GetCoverImageAsync(userId); if (filename == null) return NotFound(); 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")] [Authorize(PolicyConstants.AdminRole)] public async Task GetCoverUploadImage(string filename, string apiKey) { if (filename.Contains("..")) return BadRequest(await localizationService.Translate(UserId, "invalid-filename")); var path = Path.Join(directoryService.TempDirectory, filename); return PhysicalFile(path); } }