More project structure changes.

This commit is contained in:
2025-05-26 22:35:26 -04:00
parent 4feae6aae3
commit c73209ed36
8 changed files with 189 additions and 106 deletions

View File

@@ -0,0 +1,73 @@
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;
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

@@ -1,4 +1,4 @@
namespace MangaReader.Core.Sources.NatoManga.Search;
namespace MangaReader.Core.Sources.NatoManga.Api;
public record NatoMangaSearchResult
{

View File

@@ -1,65 +1,30 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Search;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using MangaReader.Core.Search;
using MangaReader.Core.Sources.NatoManga.Api;
namespace MangaReader.Core.Sources.NatoManga.Search;
public partial class NatoMangaSearchProvider(IHttpService httpService) : MangaSearchProviderBase<NatoMangaSearchResult[]>(httpService), IMangaSourceComponent
public partial class NatoMangaSearchProvider(INatoMangaClient natoMangaClient) : IMangaSearchProvider, IMangaSourceComponent
{
public string SourceId => "NatoManga";
protected override string GetSearchUrl(string keyword)
public async Task<MangaSearchResult[]> SearchAsync(string keyword, CancellationToken cancellationToken)
{
string formattedSeachWord = GetFormattedSearchWord(keyword);
NatoMangaSearchResult[] searchResults = await natoMangaClient.SearchAsync(keyword, cancellationToken);
return $"https://www.natomanga.com/home/search/json?searchword={formattedSeachWord}";
}
List<MangaSearchResult> mangaSearchResults = [];
private static string GetFormattedSearchWord(string input)
foreach (NatoMangaSearchResult searchResult in searchResults)
{
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()
MangaSearchResult mangaSearchResult = new()
{
Title = searchResult.Name,
Thumbnail = searchResult.Thumb,
Url = searchResult.Url
});
};
mangaSearchResults.Add(mangaSearchResult);
}
return [.. mangaSearchResults];
}
[GeneratedRegex(@"[^a-z0-9]+")]
private static partial Regex NonAlphaNumericCharactersRegex();
[GeneratedRegex("_{2,}")]
private static partial Regex ExtendedUnderscoresRegex();
}

View File

@@ -10,10 +10,8 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Search\MangaDex\SampleSearchResult.json" />
<None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
<None Remove="Search\NatoManga\SampleSearchResult.json" />
</ItemGroup>
<ItemGroup>
@@ -28,7 +26,7 @@
<ItemGroup>
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Search\NatoManga\SampleSearchResult.json" />
<EmbeddedResource Include="Sources\NatoManga\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
</ItemGroup>
@@ -56,8 +54,4 @@
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<Folder Include="Search\MangaDex\" />
</ItemGroup>
</Project>

View File

@@ -1,51 +0,0 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Search;
using MangaReader.Core.Sources.NatoManga.Search;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Search.NatoManga;
public class NatoMangaWebSearchTests
{
[Fact]
public void Get_Search_Url()
{
// Arrange
IHttpService httpService = Substitute.For<IHttpService>();
NatoMangaSearchProviderTestWrapper searchProvider = new(httpService);
// Act
string url = searchProvider.Test_GetSearchUrl("Gal can't be kind");
// Assert
url.ShouldBe("https://www.natomanga.com/home/search/json?searchword=gal_can_t_be_kind");
}
[Fact]
public async Task Get_Search_Result()
{
string resourceName = "MangaReader.Tests.Search.NatoManga.SampleSearchResult.json";
string searchResultJson = await ResourceHelper.ReadJsonResourceAsync(resourceName);
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
NatoMangaSearchProvider searchProvider = new(httpService);
MangaSearchResult[] searchResult = await searchProvider.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
searchResult.Length.ShouldBe(2);
searchResult[0].Title.ShouldBe("Gal Can't Be Kind to Otaku!");
searchResult[1].Title.ShouldBe("Gal Cant Be Kind to Otaku!?");
searchResult[0].Url.ShouldBe("https://www.natomanga.com/manga/gal-can-t-be-kind-to-otaku");
searchResult[1].Url.ShouldBe("https://www.natomanga.com/manga/gal-cant-be-kind-to-otaku");
}
}
internal class NatoMangaSearchProviderTestWrapper(IHttpService httpService) : NatoMangaSearchProvider(httpService)
{
internal string Test_GetSearchUrl(string keyword) => GetSearchUrl(keyword);
}

View File

@@ -0,0 +1,55 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Sources.NatoManga.Api;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Sources.NatoManga.Api;
public class NatoMangaClientTests
{
class TestNatoMangaClient(IHttpService httpService) : NatoMangaClient(httpService)
{
internal string Test_GetSearchUrl(string keyword) => GetSearchUrl(keyword);
}
[Fact]
public void Get_Search_Url()
{
IHttpService httpService = Substitute.For<IHttpService>();
TestNatoMangaClient searchProvider = new(httpService);
string url = searchProvider.Test_GetSearchUrl("Gal can't be kind");
url.ShouldBe("https://www.natomanga.com/home/search/json?searchword=gal_can_t_be_kind");
}
[Fact]
public async Task Search_Manga()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Search-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
NatoMangaClient natoMangaClient = new(httpService);
NatoMangaSearchResult[] searchResults = await natoMangaClient.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
searchResults.Length.ShouldBe(2);
searchResults[0].Name.ShouldBe("Gal Can't Be Kind to Otaku!");
searchResults[0].Url.ShouldBe("https://www.natomanga.com/manga/gal-can-t-be-kind-to-otaku");
searchResults[0].Thumb.ShouldBe("https://img-r1.2xstorage.com/thumb/gal-can-t-be-kind-to-otaku.webp");
searchResults[1].Name.ShouldBe("Gal Cant Be Kind to Otaku!?");
searchResults[1].Url.ShouldBe("https://www.natomanga.com/manga/gal-cant-be-kind-to-otaku");
searchResults[1].Thumb.ShouldBe("https://img-r1.2xstorage.com/thumb/gal-cant-be-kind-to-otaku.webp");
}
private static async Task<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.NatoManga.Api.{resourceName}");
}
}

View File

@@ -0,0 +1,47 @@
using MangaReader.Core.Search;
using MangaReader.Core.Sources.NatoManga.Api;
using MangaReader.Core.Sources.NatoManga.Search;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Sources.NatoManga.Search;
public class NatoMangaSearchTests
{
[Fact]
public async Task Get_Search_Result()
{
NatoMangaSearchResult[] searchResults =
[
new()
{
Name = "Gal Can't Be Kind to Otaku!",
Url = "https://www.natomanga.com/manga/gal-can-t-be-kind-to-otaku",
Thumb = "https://img-r1.2xstorage.com/thumb/gal-can-t-be-kind-to-otaku.webp"
},
new()
{
Name = "Gal Cant Be Kind to Otaku!?",
Url = "https://www.natomanga.com/manga/gal-cant-be-kind-to-otaku",
Thumb = "https://img-r1.2xstorage.com/thumb/gal-cant-be-kind-to-otaku.webp"
}
];
INatoMangaClient natoMangaClient = Substitute.For<INatoMangaClient>();
natoMangaClient.SearchAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResults));
NatoMangaSearchProvider searchProvider = new(natoMangaClient);
MangaSearchResult[] searchResult = await searchProvider.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
searchResult.Length.ShouldBe(2);
searchResult[0].Title.ShouldBe("Gal Can't Be Kind to Otaku!");
searchResult[0].Url.ShouldBe("https://www.natomanga.com/manga/gal-can-t-be-kind-to-otaku");
searchResult[0].Thumbnail.ShouldBe("https://img-r1.2xstorage.com/thumb/gal-can-t-be-kind-to-otaku.webp");
searchResult[1].Title.ShouldBe("Gal Cant Be Kind to Otaku!?");
searchResult[1].Url.ShouldBe("https://www.natomanga.com/manga/gal-cant-be-kind-to-otaku");
searchResult[1].Thumbnail.ShouldBe("https://img-r1.2xstorage.com/thumb/gal-cant-be-kind-to-otaku.webp");
}
}