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