using API.Schema; using API.Schema.Jobs; using API.Schema.MangaConnectors; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static Microsoft.AspNetCore.Http.StatusCodes; namespace API.Controllers; [ApiVersion(2)] [ApiController] [Route("v{v:apiVersion}/[controller]")] public class SearchController(PgsqlContext context) : Controller { /// /// Initiate a search for a Manga on all Connectors /// /// Name/Title of the Manga /// /// Error during Database Operation [HttpPost("Name")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult SearchMangaGlobal([FromBody]string name) { List<(Manga, List?, List?, List?, List?)> allManga = new(); foreach (MangaConnector contextMangaConnector in context.MangaConnectors.Where(connector => connector.Enabled)) allManga.AddRange(contextMangaConnector.GetManga(name)); List retMangas = new(); foreach ((Manga? manga, List? authors, List? tags, List? links, List? altTitles) in allManga) { try { Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles); if(add is not null) retMangas.Add(add); } catch (Exception e) { return StatusCode(500, e); } } return Ok(retMangas.ToArray()); } /// /// Initiate a search for a Manga on a specific Connector /// /// Manga-Connector-ID /// Name/Title of the Manga /// /// MangaConnector with ID not found /// MangaConnector with ID is disabled /// Error during Database Operation [HttpPost("{MangaConnectorName}/{SearchName}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult SearchManga(string MangaConnectorName, string SearchName) { MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName); if (connector is null) return NotFound(); else if (connector.Enabled is false) return StatusCode(406); (Manga, List?, List?, List?, List?)[] mangas = connector.GetManga(SearchName); List retMangas = new(); foreach ((Manga? manga, List? authors, List? tags, List? links, List? altTitles) in mangas) { try { Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles); if(add is not null) retMangas.Add(add); } catch (Exception e) { return StatusCode(500, e.Message); } } return Ok(retMangas.ToArray()); } /// /// Returns Manga from MangaConnector associated with URL /// /// Manga-Page URL /// /// Multiple connectors found for URL /// No Manga at URL /// No connector found for URL /// Error during Database Operation [HttpPost("Url")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status300MultipleChoices)] [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult GetMangaFromUrl([FromBody]string url) { List connectors = context.MangaConnectors.AsEnumerable().Where(c => c.ValidateUrl(url)).ToList(); if (connectors.Count == 0) return NotFound(); else if (connectors.Count > 1) return StatusCode(Status300MultipleChoices); (Manga manga, List? authors, List? tags, List? links, List? altTitles)? x = connectors.First().GetMangaFromUrl(url); if (x is null) return BadRequest(); try { Manga? add = AddMangaToContext(x.Value.manga, x.Value.authors, x.Value.tags, x.Value.links, x.Value.altTitles); if (add is not null) return Ok(add); return StatusCode(500); } catch (Exception e) { return StatusCode(500, e.Message); } } private Manga? AddMangaToContext(Manga? manga, List? authors, List? tags, List? links, List? altTitles) { if (manga is null) return null; Manga? existing = context.Manga.FirstOrDefault(m => m.MangaId == manga.MangaId); if (tags is not null) { IEnumerable mergedTags = tags.Select(mt => { MangaTag? inDb = context.Tags.FirstOrDefault(t => t.Equals(mt)); return inDb ?? mt; }); manga.MangaTags = mergedTags.ToList(); IEnumerable newTags = manga.MangaTags.Where(mt => !context.Tags.Any(t => t.Tag.Equals(mt.Tag))); context.Tags.AddRange(newTags); } if (authors is not null) { IEnumerable mergedAuthors = authors.Select(ma => { Author? inDb = context.Authors.FirstOrDefault(a => a.AuthorName == ma.AuthorName); return inDb ?? ma; }); manga.Authors = mergedAuthors.ToList(); IEnumerable newAuthors = manga.Authors.Where(ma => !context.Authors.Any(a => a.AuthorName == ma.AuthorName)); context.Authors.AddRange(newAuthors); } if (links is not null) { IEnumerable mergedLinks = links.Select(ml => { Link? inDb = context.Link.FirstOrDefault(l => l.LinkProvider == ml.LinkProvider && l.LinkUrl == ml.LinkUrl); return inDb ?? ml; }); manga.Links = mergedLinks.ToList(); IEnumerable newLinks = manga.Links.Where(ml => !context.Link.Any(l => l.LinkProvider == ml.LinkProvider && l.LinkUrl == ml.LinkUrl)); context.Link.AddRange(newLinks); } if (altTitles is not null) { IEnumerable mergedAltTitles = altTitles.Select(mat => { MangaAltTitle? inDb = context.AltTitles.FirstOrDefault(at => at.Language == mat.Language && at.Title == mat.Title); return inDb ?? mat; }); manga.AltTitles = mergedAltTitles.ToList(); IEnumerable newAltTitles = manga.AltTitles.Where(mat => !context.AltTitles.Any(at => at.Language == mat.Language && at.Title == mat.Title)); context.AltTitles.AddRange(newAltTitles); } existing?.UpdateWithInfo(manga); if(existing is not null) context.Manga.Update(existing); else context.Manga.Add(manga); context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId)); context.Jobs.Add(new RetrieveChaptersJob(0, manga.MangaId)); context.SaveChanges(); return existing ?? manga; } }