Finished MangaDexMetadataProvider logic.

This commit is contained in:
2025-05-27 23:49:38 -04:00
parent df1e8a2360
commit 4e5be6c910
15 changed files with 563 additions and 93 deletions

View File

@@ -4,9 +4,8 @@ public class SourceManga
{
public required string Title { get; set; }
public string? Description { get; set; }
public List<string> AlternateTitles { get; set; } = [];
public List<string> Authors { get; set; } = [];
public List<string> Artists { get; set; } = [];
public List<SourceMangaTitle> AlternateTitles { get; set; } = [];
public SourceMangaContributor[] Contributors { get; set; } = [];
public MangaStatus Status { get; set; } = MangaStatus.Unknown;
public List<string> Genres { get; set; } = [];
public DateTime? UpdateDate { get; set; }

View File

@@ -4,7 +4,7 @@ public class SourceMangaChapter
{
public int? Volume { get; set; }
public required float Number { get; set; }
public string? Name { get; set; }
public string? Title { get; set; }
public required string Url { get; set; }
public long? Views { get; set; }
public DateTime? UploadDate { get; set; }

View File

@@ -0,0 +1,7 @@
namespace MangaReader.Core.Metadata;
public class SourceMangaContributor
{
public required string Name { get; set; }
public SourceMangaContributorRole Role { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace MangaReader.Core.Metadata;
public enum SourceMangaContributorRole
{
Unknown,
Author,
Artist
}

View File

@@ -0,0 +1,9 @@
namespace MangaReader.Core.Metadata;
public enum SourceMangaLanguage
{
Unknown,
Japanese,
Romanji,
English
}

View File

@@ -0,0 +1,7 @@
namespace MangaReader.Core.Metadata;
public class SourceMangaTitle
{
public required string Title { get; set; }
public SourceMangaLanguage Language { get; set; }
}

View File

@@ -11,7 +11,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
{
Manga manga = await GetOrAddMangaAsync(sourceManga);
foreach (string alternateTitle in sourceManga.AlternateTitles)
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
{
await AddTitleAsync(manga, alternateTitle);
}
@@ -62,9 +62,10 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
[GeneratedRegex(@"\s+")]
private static partial Regex RemoveSpacesWithDashRegex();
private async Task AddTitleAsync(Manga manga, string title)
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle)
{
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => mt.Manga == manga && mt.TitleEntry == title);
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt =>
mt.Manga == manga && mt.TitleEntry == sourceMangaTitle.Title);
if (mangaTitle != null)
return;
@@ -72,7 +73,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
mangaTitle = new()
{
Manga = manga,
TitleEntry = title,
TitleEntry = sourceMangaTitle.Title,
};
context.MangaTitles.Add(mangaTitle);
@@ -123,9 +124,9 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
mangaChapter.VolumeNumber = sourceeMangaChapter.Volume;
}
if (mangaChapter.Title is null && sourceeMangaChapter.Name is not null)
if (mangaChapter.Title is null && sourceeMangaChapter.Title is not null)
{
mangaChapter.Title = sourceeMangaChapter.Name;
mangaChapter.Title = sourceeMangaChapter.Title;
}
}

View File

@@ -12,10 +12,27 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
Guid mangaGuid = GetSourceMangaGuid(url);
MangaDexResponse? mangaDexResponse = await mangaDexClient.GetMangaAsync(mangaGuid, cancellationToken);
if (mangaDexResponse == null)
if (mangaDexResponse == null || mangaDexResponse is not MangaDexEntityResponse mangaDexEntityResponse)
return null;
throw new NotImplementedException();
if (mangaDexEntityResponse.Data == null || mangaDexEntityResponse.Data is not MangaEntity mangaEntity)
return null;
if (mangaEntity.Attributes == null)
return null;
MangaAttributes mangaAttributes = mangaEntity.Attributes;
List<MangaDexEntity> mangaRelationships = mangaEntity.Relationships;
MangaDexResponse? mangaDexFeedResponse = await mangaDexClient.GetFeedAsync(mangaGuid, cancellationToken);
return new SourceManga()
{
Title = GetTitle(mangaAttributes),
AlternateTitles = GetAlternateTitles(mangaAttributes),
Genres = GetGenres(mangaAttributes),
Contributors = GetContributors(mangaRelationships),
Chapters = GetChapters(mangaDexFeedResponse)
};
}
private static Guid GetSourceMangaGuid(string url)
@@ -29,4 +46,139 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
return mangaGuid;
}
private static string GetTitle(MangaAttributes attributes)
{
if (attributes.Title.TryGetValue("en", out string? title))
return title;
return string.Empty;
}
private static List<SourceMangaTitle> GetAlternateTitles(MangaAttributes attributes)
{
if (attributes.AltTitles == null || attributes.AltTitles.Count == 0)
return [];
Dictionary<string, SourceMangaLanguage> languageIdMap = new()
{
{ "en", SourceMangaLanguage.English },
{ "ja", SourceMangaLanguage.Japanese },
{ "ja-ro", SourceMangaLanguage.Romanji },
};
List<SourceMangaTitle> sourceMangaTitles = [];
foreach (Dictionary<string, string> alternateTitle in attributes.AltTitles)
{
foreach (string alternateTitleKey in alternateTitle.Keys)
{
if (languageIdMap.TryGetValue(alternateTitleKey, out SourceMangaLanguage language) == false)
continue;
SourceMangaTitle sourceMangaTitle = new()
{
Title = alternateTitle[alternateTitleKey],
Language = language
};
sourceMangaTitles.Add(sourceMangaTitle);
}
}
return sourceMangaTitles;
}
private static List<string> GetGenres(MangaAttributes attributes)
{
if (attributes.Tags == null || attributes.Tags.Count == 0)
return [];
List<string> tags = [];
foreach (TagEntity tagEntity in attributes.Tags)
{
if (tagEntity.Attributes == null)
continue;
if (tagEntity.Attributes.Name == null || tagEntity.Attributes.Name.Count == 0)
continue;
tags.Add(tagEntity.Attributes.Name.FirstOrDefault().Value);
}
return tags;
}
private static SourceMangaContributor[] GetContributors(List<MangaDexEntity> relationships)
{
List<SourceMangaContributor> contributors = [];
AuthorEntity[] authorEntities = [.. relationships.Where(entity => entity is AuthorEntity).Cast<AuthorEntity>()];
ArtistEntity[] artistEntities = [.. relationships.Where(entity => entity is ArtistEntity).Cast<ArtistEntity>()];
foreach (AuthorEntity authorEntity in authorEntities)
{
if (authorEntity.Attributes == null)
continue;
SourceMangaContributor contributor = new()
{
Name = authorEntity.Attributes.Name,
Role = SourceMangaContributorRole.Author
};
contributors.Add(contributor);
}
foreach (ArtistEntity artistEntity in artistEntities)
{
if (artistEntity.Attributes == null)
continue;
SourceMangaContributor contributor = new()
{
Name = artistEntity.Attributes.Name,
Role = SourceMangaContributorRole.Artist
};
contributors.Add(contributor);
}
return [.. contributors];
}
private static List<SourceMangaChapter> GetChapters(MangaDexResponse? mangaDexFeedResponse)
{
if (mangaDexFeedResponse == null || mangaDexFeedResponse is not MangaDexCollectionResponse collectionResponse)
return [];
//https://mangadex.org/chapter/46084762-855c-46dd-a7b6-66e5cd15604d
List<SourceMangaChapter> chapters = [];
ChapterEntity[] chapterEntities = [.. collectionResponse.Data.Where(entity => entity is ChapterEntity).Cast<ChapterEntity>()];
foreach (ChapterEntity chapterEntity in chapterEntities)
{
if (chapterEntity.Attributes == null || chapterEntity.Attributes.TranslatedLanguage != "en")
continue;
int? volume = int.TryParse(chapterEntity.Attributes.Volume, out var temp) ? temp : null;
if (float.TryParse(chapterEntity.Attributes.Chapter, out float chapterNumber) == false)
continue;
SourceMangaChapter chapter = new()
{
Volume = volume,
Number = chapterNumber,
Title = chapterEntity.Attributes.Title,
Url = $"https://mangadex.org/chapter/{chapterEntity.Id}"
};
chapters.Add(chapter);
}
return chapters;
}
}

View File

@@ -18,7 +18,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
{
Title = node.TitleNode?.InnerText ?? string.Empty,
AlternateTitles = GetAlternateTitles(node.AlternateTitlesNode),
Authors = GetAuthors(node.AuthorsNode),
Contributors = GetContributors(node.AuthorsNode),
Status = GetStatus(node.StatusNode),
Genres = GetGenres(node.GenresNode),
UpdateDate = GetUpdateDate(node.UpdateDateNode),
@@ -32,20 +32,50 @@ public class MangaNatoWebCrawler : MangaWebCrawler
return manga;
}
private static List<string> GetAlternateTitles(HtmlNode? node)
private static List<SourceMangaTitle> GetAlternateTitles(HtmlNode? node)
{
if (node == null)
return [];
return [.. node.InnerText.Split(';').Select(x => x.Trim())];
List<SourceMangaTitle> sourceMangaTitles = [];
string[] titles = [.. node.InnerText.Split(';').Select(x => x.Trim())];
foreach (string title in titles)
{
SourceMangaTitle sourceMangaTitle = new()
{
Title = title,
Language = SourceMangaLanguage.Unknown
};
sourceMangaTitles.Add(sourceMangaTitle);
}
return [.. sourceMangaTitles];
}
private static List<string> GetAuthors(HtmlNode? node)
private static SourceMangaContributor[] GetContributors(HtmlNode? node)
{
if (node == null)
return [];
return [.. node.InnerText.Split('-').Select(x => x.Trim())];
List<SourceMangaContributor> contributors = [];
string[] names = [.. node.InnerText.Split('-').Select(x => x.Trim())];
foreach (string name in names)
{
SourceMangaContributor contributor = new()
{
Name = name,
Role = SourceMangaContributorRole.Author
};
contributors.Add(contributor);
}
return [.. contributors];
}
private static MangaStatus GetStatus(HtmlNode? node)
@@ -138,7 +168,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
SourceMangaChapter chapter = new()
{
Number = GetChapterNumber(chapterNameNode),
Name = chapterNameNode?.InnerText ?? string.Empty,
Title = chapterNameNode?.InnerText ?? string.Empty,
Url = chapterNameNode?.Attributes["href"].Value ?? string.Empty,
Views = GetViews(chapterViewNode),
UploadDate = chapterTimeNode != null ? DateTime.Parse(chapterTimeNode.Attributes["title"].Value) : null

View File

@@ -1,73 +1,6 @@
using MangaReader.Core.HttpService;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace MangaReader.Core.Sources.NatoManga.Api;
namespace MangaReader.Core.Sources.NatoManga.Api;
public interface INatoMangaClient
{
Task<NatoMangaSearchResult[]> SearchAsync(string searchWord, CancellationToken cancellationToken);
}
public partial class NatoMangaClient(IHttpService httpService) : INatoMangaClient
{
[GeneratedRegex(@"[^a-z0-9]+")]
private static partial Regex NonAlphaNumericCharactersRegex();
[GeneratedRegex("_{2,}")]
private static partial Regex ExtendedUnderscoresRegex();
private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
public async Task<NatoMangaSearchResult[]> SearchAsync(string searchWord, CancellationToken cancellationToken)
{
string formattedSearchWord = GetFormattedSearchWord(searchWord);
string url = $"https://www.natomanga.com/home/search/json?searchword={formattedSearchWord}";
string response = await httpService.GetStringAsync(url, cancellationToken);
return JsonSerializer.Deserialize<NatoMangaSearchResult[]>(response, _jsonSerializerOptions) ?? [];
}
protected string GetSearchUrl(string keyword)
{
string formattedSeachWord = GetFormattedSearchWord(keyword);
return $"https://www.natomanga.com/home/search/json?searchword={formattedSeachWord}";
}
private static string GetFormattedSearchWord(string input)
{
if (string.IsNullOrWhiteSpace(input))
return string.Empty;
// Convert to lowercase and normalize to decompose accents
string normalized = input.ToLowerInvariant()
.Normalize(NormalizationForm.FormD);
// Remove diacritics
var sb = new StringBuilder();
foreach (var c in normalized)
{
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
sb.Append(c);
}
// Replace non-alphanumeric characters with underscores
string cleaned = NonAlphaNumericCharactersRegex().Replace(sb.ToString(), "_");
// Trim and collapse underscores
cleaned = ExtendedUnderscoresRegex().Replace(cleaned, "_").Trim('_');
return cleaned;
}
}

View File

@@ -0,0 +1,64 @@
using MangaReader.Core.HttpService;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace MangaReader.Core.Sources.NatoManga.Api;
public partial class NatoMangaClient(IHttpService httpService) : INatoMangaClient
{
[GeneratedRegex(@"[^a-z0-9]+")]
private static partial Regex NonAlphaNumericCharactersRegex();
[GeneratedRegex("_{2,}")]
private static partial Regex ExtendedUnderscoresRegex();
private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
public async Task<NatoMangaSearchResult[]> SearchAsync(string searchWord, CancellationToken cancellationToken)
{
string url = GetSearchUrl(searchWord);
string response = await httpService.GetStringAsync(url, cancellationToken);
return JsonSerializer.Deserialize<NatoMangaSearchResult[]>(response, _jsonSerializerOptions) ?? [];
}
protected string GetSearchUrl(string searchWord)
{
string formattedSeachWord = GetFormattedSearchWord(searchWord);
return $"https://www.natomanga.com/home/search/json?searchword={formattedSeachWord}";
}
private static string GetFormattedSearchWord(string searchWord)
{
if (string.IsNullOrWhiteSpace(searchWord))
return string.Empty;
// Convert to lowercase and normalize to decompose accents
string normalized = searchWord.ToLowerInvariant()
.Normalize(NormalizationForm.FormD);
// Remove diacritics
var sb = new StringBuilder();
foreach (var c in normalized)
{
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
sb.Append(c);
}
// Replace non-alphanumeric characters with underscores
string cleaned = NonAlphaNumericCharactersRegex().Replace(sb.ToString(), "_");
// Trim and collapse underscores
cleaned = ExtendedUnderscoresRegex().Replace(cleaned, "_").Trim('_');
return cleaned;
}
}

View File

@@ -96,7 +96,7 @@ public class NatoMangaWebCrawler : MangaWebCrawler
SourceMangaChapter chapter = new()
{
Number = GetChapterNumber(chapterNameNode),
Name = chapterNameNode.InnerText,
Title = chapterNameNode.InnerText,
Url = chapterNameNode.Attributes["href"].Value,
Views = GetViews(chapterViewNode),
UploadDate = DateTime.Parse(chapterTimeNode.Attributes["title"].Value)