using API.Schema; using API.Schema.Jobs; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using static Microsoft.AspNetCore.Http.StatusCodes; namespace API.Controllers; [ApiVersion(2)] [ApiController] [Route("v{v:apiVersion}/[controller]")] public class MangaController(PgsqlContext context) : Controller { /// /// Returns all cached Manga /// /// [HttpGet] [ProducesResponseType(Status200OK, "application/json")] public IActionResult GetAllManga() { Manga[] ret = context.Mangas.ToArray(); return Ok(ret); } /// /// Returns all cached Manga with IDs /// /// Array of Manga-IDs /// [HttpPost("WithIDs")] [ProducesResponseType(Status200OK, "application/json")] public IActionResult GetManga([FromBody]string[] ids) { Manga[] ret = context.Mangas.Where(m => ids.Contains(m.MangaId)).ToArray(); return Ok(ret); } /// /// Return Manga with ID /// /// Manga-ID /// /// Manga with ID not found [HttpGet("{MangaId}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] public IActionResult GetManga(string MangaId) { Manga? ret = context.Mangas.Find(MangaId); if (ret is null) return NotFound(); return Ok(ret); } /// /// Delete Manga with ID /// /// Manga-ID /// /// Manga with ID not found /// Error during Database Operation [HttpDelete("{MangaId}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult DeleteManga(string MangaId) { try { Manga? ret = context.Mangas.Find(MangaId); if (ret is null) return NotFound(); context.Remove(ret); context.SaveChanges(); return Ok(); } catch (Exception e) { return StatusCode(500, e.Message); } } /// /// Returns Cover of Manga /// /// Manga-ID /// If width is provided, height needs to also be provided /// If height is provided, width needs to also be provided /// JPEG Image /// Cover not loaded /// The formatting-request was invalid /// Manga with ID not found [HttpGet("{MangaId}/Cover")] [ProducesResponseType(Status200OK,"image/jpeg")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status404NotFound)] public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height) { Manga? m = context.Mangas.Find(MangaId); if (m is null) return NotFound(); if (!System.IO.File.Exists(m.CoverFileNameInCache)) { bool coverIsBeingDownloaded = false; do { coverIsBeingDownloaded = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).AsEnumerable() .Any(j => j is DownloadMangaCoverJob dmcj && dmcj.MangaId == MangaId); Thread.Sleep(100); } while (coverIsBeingDownloaded); if (!System.IO.File.Exists(m.CoverFileNameInCache)) return NoContent(); } Image image = Image.Load(m.CoverFileNameInCache); if (width is { } w && height is { } h) { if (width < 10 || height < 10 || width > 65535 || height > 65535) return BadRequest(); image.Mutate(i => i.ApplyProcessor(new ResizeProcessor(new ResizeOptions() { Mode = ResizeMode.Max, Size = new Size(w, h) }, image.Size))); } using MemoryStream ms = new(); image.Save(ms, new JpegEncoder(){Quality = 100}); return File(ms.GetBuffer(), "image/jpeg"); } /// /// Returns all Chapters of Manga /// /// Manga-ID /// /// Manga with ID not found [HttpGet("{MangaId}/Chapters")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] public IActionResult GetChapters(string MangaId) { Manga? m = context.Mangas.Find(MangaId); if (m is null) return NotFound(); Chapter[] ret = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToArray(); return Ok(ret); } /// /// Returns all downloaded Chapters for Manga with ID /// /// Manga-ID /// /// No available chapters /// Manga with ID not found. [HttpGet("{MangaId}/Chapters/Downloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound)] public IActionResult GetChaptersDownloaded(string MangaId) { Manga? m = context.Mangas.Find(MangaId); if (m is null) return NotFound(); List chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList(); if (chapters.Count == 0) return NoContent(); return Ok(chapters); } /// /// Returns all Chapters not downloaded for Manga with ID /// /// Manga-ID /// /// No available chapters /// Manga with ID not found. [HttpGet("{MangaId}/Chapters/NotDownloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound)] public IActionResult GetChaptersNotDownloaded(string MangaId) { Manga? m = context.Mangas.Find(MangaId); if (m is null) return NotFound(); List chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == false).ToList(); if (chapters.Count == 0) return NoContent(); return Ok(chapters); } /// /// Returns the latest Chapter of requested Manga available on Website /// /// Manga-ID /// /// No available chapters /// Manga with ID not found. /// Could not retrieve the maximum chapter-number [HttpGet("{MangaId}/Chapter/LatestAvailable")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult GetLatestChapter(string MangaId) { Manga? m = context.Mangas.Find(MangaId); if (m is null) return NotFound(); List chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList(); if (chapters.Count == 0) return NoContent(); Chapter? max = chapters.Max(); if (max is null) return StatusCode(500, "Max chapter could not be found"); return Ok(max); } /// /// Returns the latest Chapter of requested Manga that is downloaded /// /// Manga-ID /// /// No available chapters /// Manga with ID not found. /// Could not retrieve the maximum chapter-number [HttpGet("{MangaId}/Chapter/LatestDownloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult GetLatestChapterDownloaded(string MangaId) { Manga? m = context.Mangas.Find(MangaId); if (m is null) return NotFound(); List chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId && c.Downloaded == true).ToList(); if (chapters.Count == 0) return NoContent(); Chapter? max = chapters.Max(); if (max is null) return StatusCode(500, "Max chapter could not be found"); return Ok(max); } /// /// Configure the cut-off for Manga /// /// Manga-ID /// /// Manga with ID not found. /// Error during Database Operation [HttpPatch("{MangaId}/IgnoreChaptersBefore")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold) { Manga? m = context.Mangas.Find(MangaId); if (m is null) return NotFound(); try { m.IgnoreChapterBefore = chapterThreshold; context.SaveChanges(); return Ok(); } catch (Exception e) { return StatusCode(500, e.Message); } } /// /// Move Manga to different Library /// /// Manga-ID /// Library-Id /// Folder is going to be moved /// MangaId or LibraryId not found /// Error during Database Operation [HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")] [ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult MoveFolder(string MangaId, string LibraryId) { Manga? manga = context.Mangas.Find(MangaId); if (manga is null) return NotFound(); LocalLibrary? library = context.LocalLibraries.Find(LibraryId); if (library is null) return NotFound(); MoveMangaLibraryJob dep = new (MangaId, LibraryId); UpdateFilesDownloadedJob up = new (0, manga.MangaId, null, [dep.JobId]); try { context.Jobs.AddRange([dep, up]); context.SaveChanges(); return Accepted(); } catch (Exception e) { return StatusCode(500, e.Message); } } }