More project structure changes.
This commit is contained in:
73
MangaReader.Core/Sources/NatoManga/Api/INatoMangaClient.cs
Normal file
73
MangaReader.Core/Sources/NatoManga/Api/INatoMangaClient.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace MangaReader.Core.Sources.NatoManga.Search;
|
namespace MangaReader.Core.Sources.NatoManga.Api;
|
||||||
|
|
||||||
public record NatoMangaSearchResult
|
public record NatoMangaSearchResult
|
||||||
{
|
{
|
||||||
@@ -1,65 +1,30 @@
|
|||||||
using MangaReader.Core.HttpService;
|
using MangaReader.Core.Search;
|
||||||
using MangaReader.Core.Search;
|
using MangaReader.Core.Sources.NatoManga.Api;
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace MangaReader.Core.Sources.NatoManga.Search;
|
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";
|
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))
|
MangaSearchResult mangaSearchResult = new()
|
||||||
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,
|
Title = searchResult.Name,
|
||||||
Thumbnail = searchResult.Thumb,
|
Thumbnail = searchResult.Thumb,
|
||||||
Url = searchResult.Url
|
Url = searchResult.Url
|
||||||
});
|
};
|
||||||
|
|
||||||
|
mangaSearchResults.Add(mangaSearchResult);
|
||||||
|
}
|
||||||
|
|
||||||
return [.. mangaSearchResults];
|
return [.. mangaSearchResults];
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"[^a-z0-9]+")]
|
|
||||||
private static partial Regex NonAlphaNumericCharactersRegex();
|
|
||||||
|
|
||||||
[GeneratedRegex("_{2,}")]
|
|
||||||
private static partial Regex ExtendedUnderscoresRegex();
|
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Search\MangaDex\SampleSearchResult.json" />
|
|
||||||
<None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
|
<None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
|
||||||
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
|
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
|
||||||
<None Remove="Search\NatoManga\SampleSearchResult.json" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -28,7 +26,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
|
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
|
||||||
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-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-Feed-Response.json" />
|
||||||
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
|
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -56,8 +54,4 @@
|
|||||||
<Using Include="Xunit" />
|
<Using Include="Xunit" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Search\MangaDex\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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 Can’t 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);
|
|
||||||
}
|
|
||||||
@@ -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 Can’t 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 Can’t 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 Can’t 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user