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();
}
}