Metadata-Site Search (Interactive linking)

This commit is contained in:
2025-06-29 21:13:05 +02:00
parent ae0c6c8240
commit 7c9e0eddf9
4 changed files with 90 additions and 29 deletions

View File

@ -4,6 +4,7 @@ using API.Schema.MetadataFetchers;
using Asp.Versioning; using Asp.Versioning;
using log4net; using log4net;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -38,45 +39,94 @@ public class MetadataFetcherController(PgsqlContext context, ILog Log) : Control
} }
/// <summary> /// <summary>
/// Tries linking a Manga to a Metadata-Provider-Site /// Searches Metadata-Provider for Manga-Metadata
/// </summary> /// </summary>
/// <param name="searchTerm">Instead of using the Manga for search, use a specific term</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response> /// <response code="400">Metadata-fetcher with Name does not exist</response>
/// <response code="404">Manga with ID not found</response> /// <response code="404">Manga with ID not found</response>
/// <response code="417">Could not find Entry on Metadata-Provider for Manga</response> [HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
/// <response code="500">Error during Database Operation</response> [ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
[HttpPost("{MetadataFetcherName}/{MangaId}/TryLink")]
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status417ExpectationFailed)] public IActionResult SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult LinkMangaToMetadataFetcher(string MangaId, string MetadataFetcherName)
{ {
if(context.Mangas.Find(MangaId) is not { } manga) if(context.Mangas.Find(MangaId) is not { } manga)
return NotFound(); return NotFound();
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher) if(Tranga.MetadataFetchers.FirstOrDefault(f => f.MetadataFetcherName == MetadataFetcherName) is not { } fetcher)
return BadRequest(); return BadRequest();
if (!fetcher.TryGetMetadataEntry(manga, out MetadataEntry? entry))
{ MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
return StatusCode(Status417ExpectationFailed, "Metadata entry not found"); return Ok(searchResults);
} }
/// <summary>
/// Links Metadata-Provider using Provider-Specific Identifier to Manga
/// </summary>
/// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response>
/// <response code="404">Manga with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(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 try
{ {
//Unlink previous metadata-entries
IQueryable<MetadataEntry> metadataEntries = context.MetadataEntries.Where(e => e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName);
context.MetadataEntries.RemoveRange(metadataEntries);
//Add new metadata-entry
context.MetadataEntries.Add(entry); context.MetadataEntries.Add(entry);
context.SaveChanges(); context.SaveChanges();
return Ok(entry);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e); Log.Error(e);
return StatusCode(500, e.Message); return StatusCode(500, e.Message);
} }
return Ok();
}
/// <summary>
/// Un-Links Metadata-Provider using Provider-Specific Identifier to Manga
/// </summary>
/// <response code="200"></response>
/// <response code="400">Metadata-fetcher with Name does not exist</response>
/// <response code="404">Manga with ID not found</response>
/// <response code="412">No Entry linking Manga and Metadata-Provider found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(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();
} }
/// <summary> /// <summary>

View File

@ -23,13 +23,12 @@ public abstract class MetadataFetcher
this.MetadataFetcherName = metadataFetcherName; this.MetadataFetcherName = metadataFetcherName;
} }
public abstract MetadataEntry? FindLinkedMetadataEntry(Manga manga); internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) =>
new (this, manga, identifier);
public bool TryGetMetadataEntry(Manga manga, [NotNullWhen(true)] out MetadataEntry? metadataEntry) public abstract MetadataSearchResult[] SearchMetadataEntry(Manga manga);
{
metadataEntry = FindLinkedMetadataEntry(manga); public abstract MetadataSearchResult[] SearchMetadataEntry(string searchTerm);
return metadataEntry != null;
}
/// <summary> /// <summary>
/// Updates the Manga linked in the MetadataEntry /// Updates the Manga linked in the MetadataEntry

View File

@ -0,0 +1,3 @@
namespace API.Schema.MetadataFetchers;
public record MetadataSearchResult(string Identifier, string Name, string Url, string? Description = null, string? CoverUrl = null);

View File

@ -10,7 +10,7 @@ public class MyAnimeList : MetadataFetcher
private static readonly Jikan Jikan = new (); private static readonly Jikan Jikan = new ();
private static readonly Regex GetIdFromUrl = new(@"https?:\/\/myanimelist\.net\/manga\/([0-9]+)\/?.*"); 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))) 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) if (m.Success && m.Groups[1].Success)
{ {
long id = long.Parse(m.Groups[1].Value); 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<JikanDotNet.Manga> resultData = Jikan.SearchMangaAsync(manga.Name).Result.Data; return SearchMetadataEntry(manga.Name);
}
public override MetadataSearchResult[] SearchMetadataEntry(string searchTerm)
{
ICollection<JikanDotNet.Manga> resultData = Jikan.SearchMangaAsync(searchTerm).Result.Data;
if (resultData.Count < 1) if (resultData.Count < 1)
return null; return [];
return new MetadataEntry(this, manga, resultData.First().MalId.ToString()); return resultData.Select(data =>
new MetadataSearchResult(data.MalId.ToString(), data.Titles.First().Title, data.Url, data.Synopsis))
.ToArray();
} }
/// <summary> /// <summary>