using API.Schema.MangaContext; using API.Schema.MangaContext.MangaConnectors; using API.Workers; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using static Microsoft.AspNetCore.Http.StatusCodes; // ReSharper disable InconsistentNaming namespace API.Controllers; [ApiVersion(2)] [ApiController] [Route("v{v:apiVersion}/[controller]")] public class MangaController(MangaContext context) : Controller { /// /// Returns all cached /// /// [HttpGet] [ProducesResponseType(Status200OK, "application/json")] public IActionResult GetAllManga() { Manga[] ret = context.Mangas.ToArray(); return Ok(ret); } /// /// Returns all cached with /// /// Array of <.Key /// [HttpPost("WithIDs")] [ProducesResponseType(Status200OK, "application/json")] public IActionResult GetManga([FromBody]string[] MangaIds) { Manga[] ret = context.Mangas.Where(m => MangaIds.Contains(m.Key)).ToArray(); return Ok(ret); } /// /// Return with /// /// .Key /// /// with not found [HttpGet("{MangaId}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] public IActionResult GetManga(string MangaId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); return Ok(manga); } /// /// Delete with /// /// .Key /// /// < with not found /// Error during Database Operation [HttpDelete("{MangaId}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult DeleteManga(string MangaId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); context.Mangas.Remove(manga); if(context.Sync() is { success: false } result) return StatusCode(Status500InternalServerError, result.exceptionMessage); return Ok(); } /// /// Merge two into one. THIS IS NOT REVERSIBLE! /// /// .Key of merging data from (getting deleted) /// .Key of merging data into /// /// with or not found [HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")] [ProducesResponseType(Status200OK,"image/jpeg")] [ProducesResponseType(Status404NotFound)] public IActionResult MergeIntoManga(string MangaIdFrom, string MangaIdInto) { if (context.Mangas.Find(MangaIdFrom) is not { } from) return NotFound(nameof(MangaIdFrom)); if (context.Mangas.Find(MangaIdInto) is not { } into) return NotFound(nameof(MangaIdInto)); BaseWorker[] newJobs = into.MergeFrom(from, context); Tranga.AddWorkers(newJobs); return Ok(); } /// /// Returns Cover of with /// /// .Key /// If is provided, needs to also be provided /// If is provided, needs to also be provided /// JPEG Image /// Cover not loaded /// The formatting-request was invalid /// with not found /// Retry later, downloading cover [HttpGet("{MangaId}/Cover")] [ProducesResponseType(Status200OK,"image/jpeg")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status503ServiceUnavailable, "text/plain")] public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); if (!System.IO.File.Exists(manga.CoverFileNameInCache)) { if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && w.MangaConnectorId.ObjId == MangaId)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000); } else return NoContent(); } Image image = Image.Load(manga.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}); DateTime lastModified = new FileInfo(manga.CoverFileNameInCache).LastWriteTime; HttpContext.Response.Headers.CacheControl = "public"; return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\"")); } /// /// Returns all of with /// /// .Key /// /// with not found [HttpGet("{MangaId}/Chapters")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] public IActionResult GetChapters(string MangaId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); Chapter[] chapters = manga.Chapters.ToArray(); return Ok(chapters); } /// /// Returns all downloaded for with /// /// .Key /// /// No available chapters /// with not found. [HttpGet("{MangaId}/Chapters/Downloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound)] public IActionResult GetChaptersDownloaded(string MangaId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); List chapters = manga.Chapters.Where(c => c.Downloaded).ToList(); if (chapters.Count == 0) return NoContent(); return Ok(chapters); } /// /// Returns all not downloaded for with /// /// .Key /// /// No available chapters /// with not found. [HttpGet("{MangaId}/Chapters/NotDownloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound)] public IActionResult GetChaptersNotDownloaded(string MangaId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); List chapters = manga.Chapters.Where(c => c.Downloaded == false).ToList(); if (chapters.Count == 0) return NoContent(); return Ok(chapters); } /// /// Returns the latest of requested available on /// /// .Key /// /// No available chapters /// with not found. /// Could not retrieve the maximum chapter-number /// Retry after timeout, updating value [HttpGet("{MangaId}/Chapter/LatestAvailable")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] [ProducesResponseType(Status503ServiceUnavailable, "text/plain")] public IActionResult GetLatestChapter(string MangaId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); List chapters = manga.Chapters.ToList(); if (chapters.Count == 0) { if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && w.MangaConnectorId.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000); }else return Ok(0); } Chapter? max = chapters.Max(); if (max is null) return StatusCode(Status500InternalServerError, "Max chapter could not be found"); return Ok(max); } /// /// Returns the latest of requested that is downloaded /// /// .Key /// /// No available chapters /// with not found. /// Could not retrieve the maximum chapter-number /// Retry after timeout, updating value [HttpGet("{MangaId}/Chapter/LatestDownloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status412PreconditionFailed, "text/plain")] [ProducesResponseType(Status503ServiceUnavailable, "text/plain")] public IActionResult GetLatestChapterDownloaded(string MangaId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); List chapters = manga.Chapters.ToList(); if (chapters.Count == 0) { if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && w.MangaConnectorId.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); return StatusCode(Status503ServiceUnavailable, Tranga.Settings.WorkCycleTimeoutMs * 2/ 1000); }else return NoContent(); } Chapter? max = chapters.Max(); if (max is null) return StatusCode(Status412PreconditionFailed, "Max chapter could not be found"); return Ok(max); } /// /// Configure the cut-off for /// /// .Key /// Threshold ( ChapterNumber) /// /// with not found. /// Error during Database Operation [HttpPatch("{MangaId}/IgnoreChaptersBefore")] [ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(); manga.IgnoreChaptersBefore = chapterThreshold; if(context.Sync() is { success: false } result) return StatusCode(Status500InternalServerError, result.exceptionMessage); return Accepted(); } /// /// Move to different /// /// .Key /// .Key /// Folder is going to be moved /// or not found [HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")] [ProducesResponseType(Status202Accepted)] [ProducesResponseType(Status404NotFound)] public IActionResult MoveFolder(string MangaId, string LibraryId) { if (context.Mangas.Find(MangaId) is not { } manga) return NotFound(nameof(MangaId)); if(context.FileLibraries.Find(LibraryId) is not { } library) return NotFound(nameof(LibraryId)); MoveMangaLibraryWorker moveLibrary = new(manga, library); Tranga.AddWorkers([moveLibrary]); return Accepted(); } /// /// (Un-)Marks as requested for Download from /// /// with /// with /// true to mark as requested, false to mark as not-requested /// /// or not found /// was not linked to , so nothing changed /// is not linked to yet. Search for on first (to create a ). /// Error during Database Operation [HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsIsRequested}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status412PreconditionFailed, "text/plain")] [ProducesResponseType(Status428PreconditionRequired, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested) { if (context.Mangas.Find(MangaId) is null) return NotFound(nameof(MangaId)); if(context.MangaConnectors.Find(MangaConnectorName) is null) return NotFound(nameof(MangaConnectorName)); if (context.MangaConnectorToManga.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId) is not { } mcId) if(IsRequested) return StatusCode(Status428PreconditionRequired, "Don't know how to download this Manga from MangaConnector"); else return StatusCode(Status412PreconditionFailed, "Not linked anyways."); mcId.UseForDownload = IsRequested; if(context.Sync() is { success: false } result) return StatusCode(Status500InternalServerError, result.exceptionMessage); DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId); RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage); Tranga.AddWorkers([downloadCover, retrieveChapters]); return Ok(); } }