using API.Controllers.DTOs;
using API.Schema.MangaContext;
using API.Workers;
using API.Workers.MangaDownloadWorkers;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes;
using AltTitle = API.Controllers.DTOs.AltTitle;
using Author = API.Controllers.DTOs.Author;
using Link = API.Controllers.DTOs.Link;
using Manga = API.Controllers.DTOs.Manga;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MangaController(MangaContext context) : Controller
{
///
/// Returns all cached
///
/// exert of . Use for more information
/// Error during Database Operation
[HttpGet]
[ProducesResponseType>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task>, InternalServerError>> GetAllManga ()
{
if (await context.Mangas.Include(m => m.MangaConnectorIds)
.OrderBy(m => m.Name)
.ToArrayAsync(HttpContext.RequestAborted) is not
{ } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
}).ToList());
}
///
/// Returns all that are being downloaded from at least one
///
/// exert of . Use for more information
/// Error during Database Operation
[HttpGet("Downloading")]
[ProducesResponseType(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task>, InternalServerError>> GetMangaDownloading()
{
if (await context.Mangas
.Include(m => m.MangaConnectorIds)
.Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload))
.OrderBy(m => m.Name)
.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
}).ToList());
}
///
/// Return with
///
/// .Key
///
/// with not found
[HttpGet("{MangaId}")]
[ProducesResponseType(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound, "text/plain")]
public async Task, NotFound>> GetManga (string MangaId)
{
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
IEnumerable ids = manga.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
IEnumerable authors = manga.Authors.Select(a => new Author(a.Key, a.AuthorName));
IEnumerable tags = manga.MangaTags.Select(t => t.Tag);
IEnumerable links = manga.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
IEnumerable altTitles = manga.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
Manga result = new (manga.Key, manga.Name, manga.Description, manga.ReleaseStatus, ids, manga.IgnoreChaptersBefore, manga.Year, manga.OriginalLanguage, manga.ChapterIds, authors, tags, links, altTitles, manga.LibraryId);
return TypedResults.Ok(result);
}
///
/// Delete with
///
/// .Key
///
/// with not found
/// Error during Database Operation
[HttpDelete("{MangaId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound, "text/plain")]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public async Task, InternalServerError>> DeleteManga (string MangaId)
{
if(await context.Mangas.Where(m => m.Key == MangaId).ExecuteDeleteAsync(HttpContext.RequestAborted) < 1)
return TypedResults.NotFound(nameof(MangaId));
if(await context.Sync(HttpContext.RequestAborted, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.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
[HttpPost("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound, "text/plain")]
public async Task>> MergeIntoManga (string MangaIdFrom, string MangaIdInto)
{
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from)
return TypedResults.NotFound(nameof(MangaIdFrom));
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdInto, HttpContext.RequestAborted) is not { } into)
return TypedResults.NotFound(nameof(MangaIdInto));
BaseWorker[] newJobs = into.MergeFrom(from, context);
Tranga.AddWorkers(newJobs);
return TypedResults.Ok();
}
///
/// Returns Cover of with
///
/// .Key
/// Size of the cover returned
///
-
///
-
///
-
///
/// JPEG Image
/// Cover not loaded
/// with not found
/// Retry later, downloading cover
[HttpGet("{MangaId}/Cover/{CoverSize?}")]
[ProducesResponseType(Status200OK,"image/jpeg")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound, "text/plain")]
[ProducesResponseType(Status503ServiceUnavailable)]
public async Task, StatusCodeHttpResult>> GetCover (string MangaId, CoverSize? CoverSize = null)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
string cache = CoverSize switch
{
MangaController.CoverSize.Small => TrangaSettings.CoverImageCacheSmall,
MangaController.CoverSize.Medium => TrangaSettings.CoverImageCacheMedium,
MangaController.CoverSize.Large => TrangaSettings.CoverImageCacheLarge,
_ => TrangaSettings.CoverImageCacheOriginal
};
if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data)
{
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
{
Response.Headers.Append("Retry-After","2");
return TypedResults.StatusCode(Status503ServiceUnavailable);
}
return TypedResults.NoContent();
}
DateTime lastModified = data.fileInfo.LastWriteTime;
EntityTagHeaderValue entityTagHeaderValue = EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\"");
if(HttpContext.Request.Headers.ETag.Equals(entityTagHeaderValue.Tag.Value))
return TypedResults.StatusCode(Status304NotModified);
HttpContext.Response.Headers.CacheControl = "public";
return TypedResults.Bytes(data.stream.ToArray(), "image/jpeg", lastModified: new DateTimeOffset(lastModified), entityTag: entityTagHeaderValue);
}
public enum CoverSize { Original, Large, Medium, Small }
///
/// Move to different
///
/// .Key
/// .Key
/// Folder is going to be moved
/// or not found
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound, "text/plain")]
public async Task>> ChangeLibrary(string MangaId, string LibraryId)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library)
return TypedResults.NotFound(nameof(LibraryId));
if(manga.LibraryId == library.Key)
return TypedResults.Ok();
MoveMangaLibraryWorker moveLibrary = new(manga, library);
Tranga.AddWorkers([moveLibrary]);
return TypedResults.Ok();
}
///
/// (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
[HttpPatch("{MangaId}/DownloadFrom/{MangaConnectorName}/{IsRequested}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound, "text/plain")]
[ProducesResponseType(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType(Status428PreconditionRequired, "text/plain")]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public async Task, StatusCodeHttpResult, InternalServerError>> MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _)
return TypedResults.NotFound(nameof(MangaId));
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out API.MangaConnectors.MangaConnector? _))
return TypedResults.NotFound(nameof(MangaConnectorName));
if (context.MangaConnectorToManga
.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId)
is not { } mcId)
{
if(IsRequested)
return TypedResults.StatusCode(Status428PreconditionRequired);
else
return TypedResults.StatusCode(Status412PreconditionFailed);
}
mcId.UseForDownload = IsRequested;
if(await context.Sync(HttpContext.RequestAborted, GetType(), System.Reflection.MethodBase.GetCurrentMethod()?.Name) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
Tranga.AddWorkers([downloadCover, retrieveChapters]);
return TypedResults.Ok();
}
///
/// Initiate a search for on a different
///
/// with
/// .Name
/// exert of
/// with Name not found
/// with Name is disabled
[HttpGet("{MangaId}/OnMangaConnector/{MangaConnectorName}")]
[ProducesResponseType>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound, "text/plain")]
[ProducesResponseType(Status406NotAcceptable)]
public async Task>, NotFound, StatusCodeHttpResult>> SearchOnDifferentConnector (string MangaId, string MangaConnectorName)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
return new SearchController(context).SearchManga(MangaConnectorName, manga.Name);
}
///
/// Returns all which where Authored by with
///
/// .Key
///
/// with
/// /// Error during Database Operation
[HttpGet("WithAuthorId/{AuthorId}")]
[ProducesResponseType>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound, "text/plain")]
public async Task>, NotFound, InternalServerError>> GetMangaWithAuthorIds (string AuthorId)
{
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } _)
return TypedResults.NotFound(nameof(AuthorId));
if (await context.MangaIncludeAll()
.Where(m => m.Authors.Any(a => a.Key == AuthorId))
.OrderBy(m => m.Name)
.ToListAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
IEnumerable tags = m.MangaTags.Select(t => t.Tag);
IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
}).ToList());
}
///
/// Returns all with
///
/// .Tag
///
/// not found
/// Error during Database Operation
[HttpGet("WithTag/{Tag}")]
[ProducesResponseType(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound, "text/plain")]
[ProducesResponseType(Status500InternalServerError)]
public async Task>, NotFound, InternalServerError>> GetMangasWithTag (string Tag)
{
if (await context.MangaIncludeAll()
.Where(m => m.MangaTags.Any(t => t.Tag.Equals(Tag, StringComparison.InvariantCultureIgnoreCase)))
.OrderBy(m => m.Name)
.ToListAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
IEnumerable tags = m.MangaTags.Select(t => t.Tag);
IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
}).ToList());
}
///
/// Returns with names similar to (identified by )
///
/// Key of
///
/// with not found
/// Error during Database Operation
[HttpGet("WithSimilarName/{MangaId}")]
[ProducesResponseType>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound, "text/plain")]
[ProducesResponseType(Status500InternalServerError)]
public async Task>, NotFound, InternalServerError>> GetSimilarManga (string MangaId)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
string name = manga.Name;
if (await context.Mangas.Where(m => m.Key != MangaId)
.ToDictionaryAsync(m => m.Key, m => m.Name, HttpContext.RequestAborted) is not { } mangaNames)
return TypedResults.InternalServerError();
List similarIds = mangaNames
.Where(kv => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(name, kv.Value) > 0.8)
.Select(kv => kv.Key)
.ToList();
return TypedResults.Ok(similarIds);
}
///
/// Returns the with .Key
///
/// Key of
///
/// with not found
[HttpGet("ConnectorId/{MangaConnectorIdId}")]
[ProducesResponseType(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound, "text/plain")]
public async Task, NotFound>> GetMangaMangaConnectorId (string MangaConnectorIdId)
{
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdManga)
return TypedResults.NotFound(nameof(MangaConnectorIdId));
MangaConnectorId result = new (mcIdManga.Key, mcIdManga.MangaConnectorName, mcIdManga.ObjId, mcIdManga.WebsiteUrl, mcIdManga.UseForDownload);
return TypedResults.Ok(result);
}
}