Added MangaDex Api. Updated project structure.

This commit is contained in:
2025-05-26 17:16:25 -04:00
parent 648aa95f32
commit ea8b4a36ff
61 changed files with 4937 additions and 197 deletions

View File

@@ -0,0 +1,21 @@
using HtmlAgilityPack;
namespace MangaReader.Core.Sources.NatoManga.Metadata;
public class NatoMangaHtmlDocument
{
public HtmlNode? MangaInfoTextNode { get; }
public HtmlNode? TitleNode { get; }
public HtmlNode? GenresNode { get; }
public HtmlNode? ChapterListNode { get; }
public HtmlNodeCollection? ChapterNodes { get; }
public NatoMangaHtmlDocument(HtmlDocument document)
{
MangaInfoTextNode = document.DocumentNode.SelectSingleNode(".//ul[@class='manga-info-text']");
TitleNode = MangaInfoTextNode?.SelectSingleNode(".//li//h1");
GenresNode = MangaInfoTextNode?.SelectSingleNode(".//li[@class='genres']");
ChapterListNode = document.DocumentNode.SelectSingleNode(".//div[@class='chapter-list']");
ChapterNodes = ChapterListNode?.SelectNodes(".//div[@class='row']");
}
}

View File

@@ -0,0 +1,123 @@
using HtmlAgilityPack;
using MangaReader.Core.Metadata;
namespace MangaReader.Core.Sources.NatoManga.Metadata;
public class NatoMangaWebCrawler : MangaWebCrawler, IMangaSourceComponent
{
public string SourceId => "NatoManga";
public override SourceManga GetManga(string url)
{
HtmlDocument document = GetHtmlDocument(url);
NatoMangaHtmlDocument node = new(document);
SourceManga manga = new()
{
Title = node.TitleNode?.InnerText ?? string.Empty,
Genres = GetGenres(node.GenresNode),
Chapters = GetChapters(node.ChapterNodes)
};
return manga;
}
private static List<string> GetGenres(HtmlNode? node)
{
if (node == null)
return [];
HtmlNodeCollection? genreNodes = node.SelectNodes(".//a");
if (genreNodes == null)
return [];
return [.. genreNodes.Select(genreNode => genreNode.InnerText.Trim())];
}
private static long GetViews(HtmlNode node)
{
string text = node.InnerText.Trim();
if (int.TryParse(text, out int number))
return number;
if (double.TryParse(text, out double doubleNumber))
return (int)doubleNumber;
ReadOnlySpan<char> shortText = text.AsSpan(0, text.Length - 1);
if (double.TryParse(shortText, out double formattedNumber) == false)
return 0;
char suffix = text[^1];
//if (char.GetNumericValue(suffix) > -1)
// return (int)formattedNumber;
long multiplier = GetMultiplier(suffix);
return (int)(formattedNumber * multiplier);
}
private static long GetMultiplier(char c)
{
return c switch
{
'K' => 1_000,
'M' => 1_000_000,
'B' => 1_000_000_000,
'T' => 1_000_000_000_000,
_ => 0,
};
}
private static List<SourceMangaChapter> GetChapters(HtmlNodeCollection? chapterNodes)
{
List<SourceMangaChapter> chapters = [];
if (chapterNodes == null)
return chapters;
foreach (var node in chapterNodes)
{
HtmlNodeCollection? chapterPropertyNodes = node.SelectNodes(".//span");
if (chapterPropertyNodes == null || chapterPropertyNodes.Count < 3)
continue;
HtmlNode? chapterNameNode = chapterPropertyNodes[0].SelectSingleNode(".//a");
HtmlNode chapterViewNode = chapterPropertyNodes[1];
HtmlNode chapterTimeNode = chapterPropertyNodes[2];
if (chapterNameNode == null)
continue;
SourceMangaChapter chapter = new()
{
Number = GetChapterNumber(chapterNameNode),
Name = chapterNameNode.InnerText,
Url = chapterNameNode.Attributes["href"].Value,
Views = GetViews(chapterViewNode),
UploadDate = DateTime.Parse(chapterTimeNode.Attributes["title"].Value)
};
chapters.Add(chapter);
}
return chapters;
}
private static float GetChapterNumber(HtmlNode chapterNameNode)
{
string url = chapterNameNode.Attributes["href"].Value;
int index = url.IndexOf("/chapter-");
if (index == -1)
return 0;
string chapterNumber = url[(index + "/chapter-".Length)..].Replace('-', '.');
return float.Parse(chapterNumber);
}
}

View File

@@ -0,0 +1,65 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Search;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace MangaReader.Core.Sources.NatoManga.Search;
public partial class NatoMangaSearchProvider(IHttpService httpService) : MangaSearchProviderBase<NatoMangaSearchResult[]>(httpService), IMangaSourceComponent
{
public string SourceId => "NatoManga";
protected override 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;
}
protected override MangaSearchResult[] GetSearchResult(NatoMangaSearchResult[] searchResult)
{
IEnumerable<MangaSearchResult> mangaSearchResults = searchResult.Select(searchResult =>
new MangaSearchResult()
{
Title = searchResult.Name,
Thumbnail = searchResult.Thumb,
Url = searchResult.Url
});
return [.. mangaSearchResults];
}
[GeneratedRegex(@"[^a-z0-9]+")]
private static partial Regex NonAlphaNumericCharactersRegex();
[GeneratedRegex("_{2,}")]
private static partial Regex ExtendedUnderscoresRegex();
}

View File

@@ -0,0 +1,12 @@
namespace MangaReader.Core.Sources.NatoManga.Search;
public record NatoMangaSearchResult
{
public int Id { get; init; }
public string? Author { get; init; }
public required string Name { get; init; }
public string? ChapterLatest { get; init; }
public required string Url { get; init; }
public string? Thumb { get; init; }
public string? Slug { get; init; }
}