From 7c9e0eddf9c20af2e61ce8d8d8cdb39df2d9b0b2 Mon Sep 17 00:00:00 2001 From: glax Date: Sun, 29 Jun 2025 21:13:05 +0200 Subject: [PATCH] Metadata-Site Search (Interactive linking) --- API/Controllers/MetadataFetcherController.cs | 84 +++++++++++++++---- .../MetadataFetchers/MetadataFetcher.cs | 13 ++- .../MetadataFetchers/MetadataSearchResult.cs | 3 + API/Schema/MetadataFetchers/MyAnimeList.cs | 19 +++-- 4 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 API/Schema/MetadataFetchers/MetadataSearchResult.cs diff --git a/API/Controllers/MetadataFetcherController.cs b/API/Controllers/MetadataFetcherController.cs index 62c7b86..33f67b0 100644 --- a/API/Controllers/MetadataFetcherController.cs +++ b/API/Controllers/MetadataFetcherController.cs @@ -4,6 +4,7 @@ using API.Schema.MetadataFetchers; using Asp.Versioning; using log4net; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using static Microsoft.AspNetCore.Http.StatusCodes; // ReSharper disable InconsistentNaming @@ -38,45 +39,94 @@ public class MetadataFetcherController(PgsqlContext context, ILog Log) : Control } /// - /// Tries linking a Manga to a Metadata-Provider-Site + /// Searches Metadata-Provider for Manga-Metadata /// + /// Instead of using the Manga for search, use a specific term /// /// Metadata-fetcher with Name does not exist /// Manga with ID not found - /// Could not find Entry on Metadata-Provider for Manga - /// Error during Database Operation - [HttpPost("{MetadataFetcherName}/{MangaId}/TryLink")] - [ProducesResponseType(Status200OK, "application/json")] + [HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")] + [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status404NotFound)] - [ProducesResponseType(Status417ExpectationFailed)] - [ProducesResponseType(Status500InternalServerError, "text/plain")] - public IActionResult LinkMangaToMetadataFetcher(string MangaId, string MetadataFetcherName) + public IActionResult SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null) { if(context.Mangas.Find(MangaId) is not { } manga) return NotFound(); if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) return BadRequest(); - if (!fetcher.TryGetMetadataEntry(manga, out MetadataEntry? entry)) - { - return StatusCode(Status417ExpectationFailed, "Metadata entry not found"); - } + MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm); + return Ok(searchResults); + } + + /// + /// Links Metadata-Provider using Provider-Specific Identifier to Manga + /// + /// + /// Metadata-fetcher with Name does not exist + /// Manga with ID not found + /// Error during Database Operation + [HttpPost("{MetadataFetcherName}/Link/{MangaId}")] + [ProducesResponseType(Status200OK)] + [ProducesResponseType(Status400BadRequest)] + [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status500InternalServerError, "text/plain")] + public IActionResult LinkMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody]string Identifier) + { + if(context.Mangas.Find(MangaId) is not { } manga) + return NotFound(); + if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) + return BadRequest(); + MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier); + try { - //Unlink previous metadata-entries - IQueryable metadataEntries = context.MetadataEntries.Where(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName); - context.MetadataEntries.RemoveRange(metadataEntries); - //Add new metadata-entry context.MetadataEntries.Add(entry); context.SaveChanges(); - return Ok(entry); } catch (Exception e) { Log.Error(e); return StatusCode(500, e.Message); } + return Ok(); + } + + /// + /// Un-Links Metadata-Provider using Provider-Specific Identifier to Manga + /// + /// + /// Metadata-fetcher with Name does not exist + /// Manga with ID not found + /// No Entry linking Manga and Metadata-Provider found + /// Error during Database Operation + [HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")] + [ProducesResponseType(Status200OK)] + [ProducesResponseType(Status400BadRequest)] + [ProducesResponseType(Status404NotFound)] + [ProducesResponseType(Status412PreconditionFailed, "text/plain")] + [ProducesResponseType(Status500InternalServerError, "text/plain")] + public IActionResult UnlinkMangaMetadata(string MangaId, string MetadataFetcherName) + { + if(context.Mangas.Find(MangaId) is not { } manga) + return NotFound(); + if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) + return BadRequest(); + MetadataEntry? entry = context.MetadataEntries.FirstOrDefault(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName); + if (entry is null) + return StatusCode(Status412PreconditionFailed, "No entry found"); + try + { + context.MetadataEntries.Remove(entry); + context.SaveChanges(); + } + catch (Exception e) + { + Log.Error(e); + return StatusCode(500, e.Message); + } + return Ok(); } /// diff --git a/API/Schema/MetadataFetchers/MetadataFetcher.cs b/API/Schema/MetadataFetchers/MetadataFetcher.cs index 48b7c07..9952f41 100644 --- a/API/Schema/MetadataFetchers/MetadataFetcher.cs +++ b/API/Schema/MetadataFetchers/MetadataFetcher.cs @@ -22,14 +22,13 @@ public abstract class MetadataFetcher { this.MetadataFetcherName = metadataFetcherName; } - - public abstract MetadataEntry? FindLinkedMetadataEntry(Manga manga); - public bool TryGetMetadataEntry(Manga manga, [NotNullWhen(true)] out MetadataEntry? metadataEntry) - { - metadataEntry = FindLinkedMetadataEntry(manga); - return metadataEntry != null; - } + internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) => + new (this, manga, identifier); + + public abstract MetadataSearchResult[] SearchMetadataEntry(Manga manga); + + public abstract MetadataSearchResult[] SearchMetadataEntry(string searchTerm); /// /// Updates the Manga linked in the MetadataEntry diff --git a/API/Schema/MetadataFetchers/MetadataSearchResult.cs b/API/Schema/MetadataFetchers/MetadataSearchResult.cs new file mode 100644 index 0000000..abdbb33 --- /dev/null +++ b/API/Schema/MetadataFetchers/MetadataSearchResult.cs @@ -0,0 +1,3 @@ +namespace API.Schema.MetadataFetchers; + +public record MetadataSearchResult(string Identifier, string Name, string Url, string? Description = null, string? CoverUrl = null); \ No newline at end of file diff --git a/API/Schema/MetadataFetchers/MyAnimeList.cs b/API/Schema/MetadataFetchers/MyAnimeList.cs index 2497c98..a350adb 100644 --- a/API/Schema/MetadataFetchers/MyAnimeList.cs +++ b/API/Schema/MetadataFetchers/MyAnimeList.cs @@ -10,7 +10,7 @@ public class MyAnimeList : MetadataFetcher private static readonly Jikan Jikan = new (); private static readonly Regex GetIdFromUrl = new(@"https?:\/\/myanimelist\.net\/manga\/([0-9]+)\/?.*"); - public override MetadataEntry? FindLinkedMetadataEntry(Manga manga) + public override MetadataSearchResult[] SearchMetadataEntry(Manga manga) { if (manga.Links.Any(link => link.LinkProvider.Equals("MyAnimeList", StringComparison.InvariantCultureIgnoreCase))) { @@ -19,14 +19,23 @@ public class MyAnimeList : MetadataFetcher if (m.Success && m.Groups[1].Success) { long id = long.Parse(m.Groups[1].Value); - return new MetadataEntry(this, manga, id.ToString()!); + JikanDotNet.Manga data = Jikan.GetMangaAsync(id).Result.Data; + return [new MetadataSearchResult(id.ToString(), data.Titles.First().Title, data.Url, data.Synopsis)]; } } - ICollection resultData = Jikan.SearchMangaAsync(manga.Name).Result.Data; + return SearchMetadataEntry(manga.Name); + } + + public override MetadataSearchResult[] SearchMetadataEntry(string searchTerm) + { + + ICollection resultData = Jikan.SearchMangaAsync(searchTerm).Result.Data; if (resultData.Count < 1) - return null; - return new MetadataEntry(this, manga, resultData.First().MalId.ToString()); + return []; + return resultData.Select(data => + new MetadataSearchResult(data.MalId.ToString(), data.Titles.First().Title, data.Url, data.Synopsis)) + .ToArray(); } ///