Project restructuring.

This commit is contained in:
2025-05-26 22:03:08 -04:00
parent ea8b4a36ff
commit 6accb373cd
26 changed files with 421 additions and 156 deletions

View File

@@ -1,12 +1,6 @@
using MangaReader.Core.Search; using MangaReader.Core.Search;
using MangaReader.Core.Search.MangaDex; using MangaReader.Core.Sources.MangaDex.Search;
using MangaReader.Core.Sources.NatoManga.Search; using MangaReader.Core.Sources.NatoManga.Search;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
#pragma warning disable IDE0130 // Namespace does not match folder structure #pragma warning disable IDE0130 // Namespace does not match folder structure
namespace Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection;

View File

@@ -2,5 +2,5 @@
public class ArtistEntity : MangaDexEntity public class ArtistEntity : MangaDexEntity
{ {
public required ArtistAttributes Attributes { get; set; } public ArtistAttributes? Attributes { get; set; }
} }

View File

@@ -2,5 +2,5 @@
public class AuthorEntity : MangaDexEntity public class AuthorEntity : MangaDexEntity
{ {
public required AuthorAttributes Attributes { get; set; } public AuthorAttributes? Attributes { get; set; }
} }

View File

@@ -2,8 +2,15 @@
public class ChapterAttributes public class ChapterAttributes
{ {
public string? Volume { get; set; }
public string? Chapter { get; set; } public string? Chapter { get; set; }
public string? Title { get; set; } public string? Title { get; set; }
public int Pages { get; set; } public string? TranslatedLanguage { get; set; }
public string? ExternalUrl { get; set; }
public DateTime PublishAt { get; set; } public DateTime PublishAt { get; set; }
public DateTime ReadableAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public int Pages { get; set; }
public int Version { get; set; }
} }

View File

@@ -2,5 +2,5 @@
public class ChapterEntity : MangaDexEntity public class ChapterEntity : MangaDexEntity
{ {
public required ChapterAttributes Attributes { get; set; } public ChapterAttributes? Attributes { get; set; }
} }

View File

@@ -2,5 +2,5 @@
public class CoverArtEntity : MangaDexEntity public class CoverArtEntity : MangaDexEntity
{ {
public required CoverArtAttributes Attributes { get; set; } public CoverArtAttributes? Attributes { get; set; }
} }

View File

@@ -2,6 +2,10 @@
public interface IMangaDexClient public interface IMangaDexClient
{ {
Task<MangaDexResponse> GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken); Task<MangaDexResponse?> SearchMangaByTitleAsync(string title, CancellationToken cancellationToken);
Task<MangaDexResponse> GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken); Task<MangaDexResponse?> SearchMangaByAuthorAsync(string author, CancellationToken cancellationToken);
Task<MangaDexResponse?> SearchMangaByGroupAsync(string group, CancellationToken cancellationToken);
Task<MangaDexResponse?> GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken);
Task<MangaDexResponse?> GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken);
Task<MangaDexChapterResponse?> GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken);
} }

View File

@@ -0,0 +1,8 @@
namespace MangaReader.Core.Sources.MangaDex.Api;
public class MangaDexChapter
{
public required string Hash { get; set; }
public List<string> Data { get; set; } = [];
public List<string> DataSaver { get; set; } = [];
}

View File

@@ -0,0 +1,8 @@
namespace MangaReader.Core.Sources.MangaDex.Api;
public class MangaDexChapterResponse
{
public required string Result { get; set; }
public required string BaseUrl { get; set; }
public MangaDexChapter? Chapter { get; set; }
}

View File

@@ -1,4 +1,5 @@
using MangaReader.Core.HttpService; using MangaReader.Core.HttpService;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace MangaReader.Core.Sources.MangaDex.Api namespace MangaReader.Core.Sources.MangaDex.Api
@@ -18,22 +19,55 @@ namespace MangaReader.Core.Sources.MangaDex.Api
_jsonSerializerOptions.Converters.Add(new MangaDexEntityConverter()); _jsonSerializerOptions.Converters.Add(new MangaDexEntityConverter());
} }
private async Task<MangaDexResponse> GetAsync(string url, CancellationToken cancellationToken) private async Task<MangaDexResponse?> GetAsync(string url, CancellationToken cancellationToken)
{ {
string response = await httpService.GetStringAsync(url, cancellationToken); string response = await httpService.GetStringAsync(url, cancellationToken);
return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions) return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions);
?? new() { Response = "failed", Result = "unknown" };
} }
public async Task<MangaDexResponse> GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken) public async Task<MangaDexResponse?> SearchMangaByTitleAsync(string title, CancellationToken cancellationToken)
{
string normalizedKeyword = GetNormalizedKeyword(title);
return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5", cancellationToken);
}
public async Task<MangaDexResponse?> SearchMangaByAuthorAsync(string author, CancellationToken cancellationToken)
{
string normalizedKeyword = GetNormalizedKeyword(author);
return await GetAsync($"https://api.mangadex.org/manga?author={normalizedKeyword}&limit=5", cancellationToken);
}
public async Task<MangaDexResponse?> SearchMangaByGroupAsync(string group, CancellationToken cancellationToken)
{
string normalizedKeyword = GetNormalizedKeyword(group);
return await GetAsync($"https://api.mangadex.org/manga?group={normalizedKeyword}&limit=5", cancellationToken);
}
protected static string GetNormalizedKeyword(string keyword)
{
return keyword.ToLowerInvariant().Normalize(NormalizationForm.FormD);
}
public async Task<MangaDexResponse?> GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken)
{ {
return await GetAsync($"https://api.mangadex.org/manga/{mangaGuid}?includes[]=artist&includes[]=author&includes[]=cover_art", cancellationToken); return await GetAsync($"https://api.mangadex.org/manga/{mangaGuid}?includes[]=artist&includes[]=author&includes[]=cover_art", cancellationToken);
} }
public async Task<MangaDexResponse> GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken) public async Task<MangaDexResponse?> GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken)
{ {
return await GetAsync($"https://api.mangadex.org/manga/{mangaGuid}/feed?translatedLanguage[]=en&limit=96&includes[]=scanlation_group&includes[]=user&order[volume]=desc&order[chapter]=desc&offset=0&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic", cancellationToken); return await GetAsync($"https://api.mangadex.org/manga/{mangaGuid}/feed?translatedLanguage[]=en&limit=96&includes[]=scanlation_group&includes[]=user&order[volume]=desc&order[chapter]=desc&offset=0&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic", cancellationToken);
} }
public async Task<MangaDexChapterResponse?> GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken)
{
string url = $"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false";
string response = await httpService.GetStringAsync(url, cancellationToken);
return JsonSerializer.Deserialize<MangaDexChapterResponse>(response, _jsonSerializerOptions);
}
} }
} }

View File

@@ -22,6 +22,7 @@ public class MangaDexEntityConverter : JsonConverter<MangaDexEntity>
"chapter" => JsonSerializer.Deserialize<ChapterEntity>(root.GetRawText(), options), "chapter" => JsonSerializer.Deserialize<ChapterEntity>(root.GetRawText(), options),
"scanlation_group" => JsonSerializer.Deserialize<ScanlationGroupEntity>(root.GetRawText(), options), "scanlation_group" => JsonSerializer.Deserialize<ScanlationGroupEntity>(root.GetRawText(), options),
"cover_art" => JsonSerializer.Deserialize<CoverArtEntity>(root.GetRawText(), options), "cover_art" => JsonSerializer.Deserialize<CoverArtEntity>(root.GetRawText(), options),
"user" => JsonSerializer.Deserialize<UserEntity>(root.GetRawText(), options),
_ => throw new NotSupportedException($"Unknown type '{type}'") _ => throw new NotSupportedException($"Unknown type '{type}'")
}; };
} }

View File

@@ -2,5 +2,5 @@
public class MangaEntity : MangaDexEntity public class MangaEntity : MangaDexEntity
{ {
public required MangaAttributes Attributes { get; set; } public MangaAttributes? Attributes { get; set; }
} }

View File

@@ -2,6 +2,7 @@
public class PersonAttributes public class PersonAttributes
{ {
public required string Name { get; set; }
public string? ImageUrl { get; set; } public string? ImageUrl { get; set; }
public Dictionary<string, string> Biography { get; set; } = []; public Dictionary<string, string> Biography { get; set; } = [];
public string? Twitter { get; set; } public string? Twitter { get; set; }

View File

@@ -2,5 +2,5 @@
public class TagEntity : MangaDexEntity public class TagEntity : MangaDexEntity
{ {
public required TagAttributes Attributes { get; set; } public TagAttributes? Attributes { get; set; }
} }

View File

@@ -0,0 +1,8 @@
namespace MangaReader.Core.Sources.MangaDex.Api;
public class UserAttributes
{
public required string UserName { get; set; }
public List<string> Roles { get; set; } = [];
public int Version { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace MangaReader.Core.Sources.MangaDex.Api;
public class UserEntity : MangaDexEntity
{
public UserAttributes? Attributes { get; set; }
}

View File

@@ -1,12 +1,10 @@
using MangaReader.Core.HttpService; using MangaReader.Core.Search;
using MangaReader.Core.Sources; using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Core.Sources.MangaDex.Search;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace MangaReader.Core.Search.MangaDex; namespace MangaReader.Core.Sources.MangaDex.Search;
public partial class MangaDexSearchProvider(IHttpService httpService) : MangaSearchProviderBase<MangaDexSearchResult>(httpService), IMangaSourceComponent public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IMangaSearchProvider, IMangaSourceComponent
{ {
[GeneratedRegex(@"[^a-z0-9\s-]")] [GeneratedRegex(@"[^a-z0-9\s-]")]
private static partial Regex InvalidSlugCharactersRegex(); private static partial Regex InvalidSlugCharactersRegex();
@@ -16,35 +14,50 @@ public partial class MangaDexSearchProvider(IHttpService httpService) : MangaSea
public string SourceId => "MangaDex"; public string SourceId => "MangaDex";
protected override string GetSearchUrl(string keyword) public async Task<MangaSearchResult[]> SearchAsync(string keyword, CancellationToken cancellationToken)
{ {
string normalizedKeyword = keyword.ToLowerInvariant().Normalize(NormalizationForm.FormD); MangaDexResponse? response = await mangaDexClient.SearchMangaByTitleAsync(keyword, cancellationToken);
return $"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5"; if (response == null || (response is not MangaDexCollectionResponse collectionResponse))
} return [];
protected override MangaSearchResult[] GetSearchResult(MangaDexSearchResult searchResult)
{
List<MangaSearchResult> mangaSearchResults = []; List<MangaSearchResult> mangaSearchResults = [];
foreach (MangaDexSearchResultData searchResultData in searchResult.Data) foreach (MangaDexEntity entity in collectionResponse.Data)
{ {
string title = searchResultData.Attributes.Title.FirstOrDefault().Value; MangaSearchResult? mangaSearchResult = GetMangaSearchResult(entity);
string slug = GenerateSlug(title);
MangaSearchResult mangaSearchResult = new() if (mangaSearchResult == null)
{ continue;
Title = title,
Url = $"https://mangadex.org/title/{searchResultData.Id}/{slug}",
Thumbnail = GetThumbnail(searchResultData)
};
mangaSearchResults.Add(mangaSearchResult); mangaSearchResults.Add(mangaSearchResult);
} }
return [.. mangaSearchResults]; return [.. mangaSearchResults];
} }
private static MangaSearchResult? GetMangaSearchResult(MangaDexEntity entity)
{
if (entity is not MangaEntity mangaEntity)
return null;
if (mangaEntity.Attributes == null)
return null;
string title = mangaEntity.Attributes.Title.FirstOrDefault().Value;
string slug = GenerateSlug(title);
MangaSearchResult mangaSearchResult = new()
{
Title = title,
Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}",
Thumbnail = GetThumbnail(mangaEntity)
};
return mangaSearchResult;
}
public static string GenerateSlug(string title) public static string GenerateSlug(string title)
{ {
// title.ToLowerInvariant().Normalize(NormalizationForm.FormD); // title.ToLowerInvariant().Normalize(NormalizationForm.FormD);
@@ -57,19 +70,19 @@ public partial class MangaDexSearchProvider(IHttpService httpService) : MangaSea
return title.Trim('-'); return title.Trim('-');
} }
private static string? GetThumbnail(MangaDexSearchResultData searchResultData) private static string? GetThumbnail(MangaDexEntity mangaDexEntity)
{ {
var coverArtRelationship = searchResultData.Relationships.FirstOrDefault(x => x.Type == "cover_art"); CoverArtEntity? coverArtEntity = (CoverArtEntity?)mangaDexEntity.Relationships.FirstOrDefault(entity =>
entity is CoverArtEntity);
if (coverArtRelationship == null) if (coverArtEntity == null || string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName))
return null; return null;
if (coverArtRelationship.Attributes.TryGetValue("fileName", out object? fileNameValue) == false) string? fileName = coverArtEntity.Attributes?.FileName;
if (string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName))
return null; return null;
if (fileNameValue == null) return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}";
return null;
return $"https://mangadex.org/covers/{searchResultData.Id}/{fileNameValue}";
} }
} }

View File

@@ -11,8 +11,7 @@
<ItemGroup> <ItemGroup>
<None Remove="Search\MangaDex\SampleSearchResult.json" /> <None Remove="Search\MangaDex\SampleSearchResult.json" />
<None Remove="WebCrawlers\MangaDex\MetadataSample-Feed.json" /> <None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<None Remove="WebCrawlers\MangaDex\MetadataSample.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" /> <None Remove="Search\NatoManga\SampleSearchResult.json" />
</ItemGroup> </ItemGroup>
@@ -27,10 +26,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Search\MangaDex\SampleSearchResult.json" /> <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="Search\NatoManga\SampleSearchResult.json" />
<EmbeddedResource Include="WebCrawlers\MangaDex\MetadataSample-Feed.json" /> <EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" />
<EmbeddedResource Include="WebCrawlers\MangaDex\MetadataSample.json" /> <EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -56,4 +56,8 @@
<Using Include="Xunit" /> <Using Include="Xunit" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Search\MangaDex\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,50 +0,0 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Search;
using MangaReader.Core.Search.MangaDex;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Search.MangaDex;
public class MangaDexSearchTests
{
class MangaDexSearchProviderTestWrapper(IHttpService httpService) : MangaDexSearchProvider(httpService)
{
internal string Test_GetSearchUrl(string keyword) => GetSearchUrl(keyword);
}
[Fact]
public void Get_Search_Url()
{
// Arrange
IHttpService httpService = Substitute.For<IHttpService>();
MangaDexSearchProviderTestWrapper searchProvider = new(httpService);
// Act
string url = searchProvider.Test_GetSearchUrl("Gal can't be");
// Assert
url.ShouldBe("https://api.mangadex.org/manga?title=gal can't be&limit=5");
}
[Fact]
public async Task Get_Search_Result()
{
string resourceName = "MangaReader.Tests.Search.MangaDex.SampleSearchResult.json";
string searchResultJson = await ResourceHelper.ReadJsonResourceAsync(resourceName);
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexSearchProvider searchProvider = new(httpService);
MangaSearchResult[] searchResult = await searchProvider.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
searchResult.Length.ShouldBe(3);
searchResult[0].Title.ShouldBe("Gals Cant Be Kind to Otaku!?");
searchResult[0].Url.ShouldBe("https://mangadex.org/title/ee96e2b7-9af2-4864-9656-649f4d3b6fec/gals-can-t-be-kind-to-otaku");
searchResult[0].Thumbnail.ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
}
}

View File

@@ -0,0 +1,9 @@
{
"result": "ok",
"baseUrl": "https:\/\/cmdxd98sb0x3yprd.mangadex.network",
"chapter": {
"hash": "f867bd09bc8b19a37cf5486134acdda1",
"data": [ "1-5ae1738e10f5440a74f11832cf6203be5bd938f72d5a80d42cd149ee21287901.png", "2-e65c0795407b284664a83fba4c80ed5efceece5e74c4b0d5e0539890aa0d6dcc.png", "3-7506a36de3ec1f02bf5f85350a4b1676da89d095cfb98e88cd9a5840c9b72f3f.png", "4-3fb99ff194103ab51ece97f4a02a130742c7fa9413926ac1437992fc881d5d26.png", "5-21111c18ae1c04294a8ded2b0ef5395ae14a3d8d9e49b32a8187f71d12e5c4e4.png", "6-1168409fbb61f5c58746b324d6e9fe6225437f86b29da25e5c9bc9c6921f5afd.png", "7-1cf3170f68e0e1e8baf187635c721783c809ed2af2842ae194ad88b5ffcd8863.png", "8-b36237f62aadb2d6e9cb21ed65723d0e020e7509143c9186049e79591b2cc7b0.png", "9-d438f6d05eccfdf827b825e78432dcff0632df6193ce10288ce049774327b9ad.png", "10-ad8e53b6d8276bee6a05fefb41eb84ddb19a88ae3d7481f9ded7eaf43907cb7a.png", "11-219faea5a79409d92a6c02a97a5ef84e34c046ba3e5e079c87d1ecb6fc9b90a4.png", "12-dd32f11cdc417859593250c0315da7cad42e458f44ee17753794deca9b0c4d6f.png", "13-b6b8e15b5abd7e53af32cbfa1ac6dc1cb48fb314f2b5bb7412c9e393cf78224e.png" ],
"dataSaver": [ "1-7b0a40f35edd75f14d0aa9c0369f8bb05e41687165d97368b572f3c3c5b3db31.jpg", "2-9034c103958951d81c7ef3aedd43ad7ce58be33b85d6bfa72fd5e9cd3b2645b0.jpg", "3-62889381cc52713e234d2423d5da8a8da35d69f2b0a5a2671770922a91a86bdd.jpg", "4-864816f3e007c7a0cf7bd8f8fb4ee4bda5f3df04b32806737e323ab6a29f1c75.jpg", "5-b4beb6f2d40c13c96c9520de77ae00b24cea5357bf6e5d1eb1b04cc4e28f6abe.jpg", "6-ea57e27e2802b112a600a450f78e397f54377ffa8065c5eee9cb155e630df162.jpg", "7-ff7f9b9018b804c4edadaa32993585f1dab48919a6a51ccd7244020458f6bc4e.jpg", "8-100943bb7543c35265d8b05ba5a14aab8e2c92897e6fedd6ae5252d555a6c5a7.jpg", "9-2e618c48e4f98a8641b9b480e9da5ebb41dac59420500b7375454dfe643fabec.jpg", "10-327e4bb775e9f0c0964d5f7a35ed5c1f44feb09d5adf9eaad835b1e313422707.jpg", "11-6657ffa04caba29942063836f75f69979c4a7415243a8340bedaa3fdf07fc72b.jpg", "12-550cc7aac42468711ad9233392cf437576f6bfc54f067eb7493578813ceb6609.jpg", "13-b886b4ed986a473478e3db7bb18fe2faea567a1ad5e520408967410dcf8838d1.jpg" ]
}
}

View File

@@ -0,0 +1,161 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Sources.MangaDex.Api;
public class MangaDexClientTests
{
[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));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse? mangaDexResponse = await mangaDexClient.SearchMangaByTitleAsync("Some random text", CancellationToken.None);
// Testing here
}
[Fact]
public async Task Get_Manga_Metadata()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse? mangaDexResponse = await mangaDexClient.GetMangaAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexResponse.ShouldNotBeNull();
mangaDexResponse.Response.ShouldBe("entity");
mangaDexResponse.ShouldBeOfType<MangaDexEntityResponse>();
MangaDexEntityResponse? mangaDexEntityResponse = mangaDexResponse as MangaDexEntityResponse;
mangaDexEntityResponse.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldBeOfType<MangaEntity>();
MangaEntity? mangaEntity = mangaDexEntityResponse.Data as MangaEntity;
mangaEntity.ShouldNotBeNull();
mangaEntity.Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Title.ShouldContainKey("en");
mangaEntity.Attributes.Title["en"].ShouldBe("Gals Cant Be Kind to Otaku!?");
mangaEntity.Attributes.Description.ShouldContainKey("en");
mangaEntity.Attributes.Description["en"].ShouldBe("Takuya Seo is an otaku who likes \"anime for girls\" and can't say he likes it out loud. One day, he hooks up with two gals from his class, Amane and Ijichi, but it seems that Amane is also an otaku...");
mangaEntity.Attributes.Tags.Count.ShouldBe(5);
mangaEntity.Attributes.Tags[0].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[0].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[0].Attributes!.Name["en"].ShouldBe("Romance");
mangaEntity.Attributes.Tags[1].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[1].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[1].Attributes!.Name["en"].ShouldBe("Comedy");
mangaEntity.Attributes.Tags[2].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[2].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[2].Attributes!.Name["en"].ShouldBe("School Life");
mangaEntity.Attributes.Tags[3].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[3].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[3].Attributes!.Name["en"].ShouldBe("Slice of Life");
mangaEntity.Attributes.Tags[4].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[4].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[4].Attributes!.Name["en"].ShouldBe("Gyaru");
mangaEntity.Relationships.Count.ShouldBe(4);
mangaEntity.Relationships[0].ShouldBeOfType<AuthorEntity>();
mangaEntity.Relationships[1].ShouldBeOfType<ArtistEntity>();
mangaEntity.Relationships[2].ShouldBeOfType<CoverArtEntity>();
mangaEntity.Relationships[3].ShouldBeOfType<CreatorEntity>();
AuthorEntity authorEntity = (mangaEntity.Relationships[0] as AuthorEntity)!;
authorEntity.Attributes.ShouldNotBeNull();
authorEntity.Attributes.Name.ShouldBe("Norishiro-chan");
ArtistEntity artistEntity = (mangaEntity.Relationships[1] as ArtistEntity)!;
artistEntity.Attributes.ShouldNotBeNull();
artistEntity.Attributes.Name.ShouldBe("Sakana Uozimi");
CoverArtEntity coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!;
coverArtEntity.Attributes.ShouldNotBeNull();
coverArtEntity.Attributes.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
}
[Fact]
public async Task Get_Manga_Feed()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Feed-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse? mangaDexResponse = await mangaDexClient.GetFeedAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexResponse.ShouldNotBeNull();
mangaDexResponse.Response.ShouldBe("collection");
mangaDexResponse.ShouldBeOfType<MangaDexCollectionResponse>();
MangaDexCollectionResponse mangaDexEntityResponse = (mangaDexResponse as MangaDexCollectionResponse)!;
List<ChapterEntity> chapterEntities = [.. mangaDexEntityResponse.Data.FindAll(entity => entity is ChapterEntity).Cast<ChapterEntity>()];
chapterEntities.Count.ShouldBe(82);
chapterEntities[0].Attributes.ShouldNotBeNull();
chapterEntities[0].Attributes!.Volume.ShouldBeNull();
chapterEntities[0].Attributes!.Chapter.ShouldBe("69");
chapterEntities[0].Attributes!.Title.ShouldBe("Otaku & Gyaru & Playing Couple");
chapterEntities[1].Attributes.ShouldNotBeNull();
chapterEntities[1].Attributes!.Volume.ShouldBe("9");
chapterEntities[1].Attributes!.Chapter.ShouldBe("68");
chapterEntities[1].Attributes!.Title.ShouldBe("Otaku & Gyaru & A Couple Date");
}
[Fact]
public async Task Get_Chapters()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Chapter-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexChapterResponse? mangaDexChapterResponse = await mangaDexClient.GetChapterAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexChapterResponse.ShouldNotBeNull();
mangaDexChapterResponse.Chapter.ShouldNotBeNull();
mangaDexChapterResponse.Chapter.Hash.ShouldBe("f867bd09bc8b19a37cf5486134acdda1");
mangaDexChapterResponse.Chapter.Data.Count.ShouldBe(13);
mangaDexChapterResponse.Chapter.Data[0].ShouldBe("1-5ae1738e10f5440a74f11832cf6203be5bd938f72d5a80d42cd149ee21287901.png");
mangaDexChapterResponse.Chapter.Data[12].ShouldBe("13-b6b8e15b5abd7e53af32cbfa1ac6dc1cb48fb314f2b5bb7412c9e393cf78224e.png");
mangaDexChapterResponse.Chapter.DataSaver.Count.ShouldBe(13);
mangaDexChapterResponse.Chapter.DataSaver[0].ShouldBe("1-7b0a40f35edd75f14d0aa9c0369f8bb05e41687165d97368b572f3c3c5b3db31.jpg");
mangaDexChapterResponse.Chapter.DataSaver[12].ShouldBe("13-b886b4ed986a473478e3db7bb18fe2faea567a1ad5e520408967410dcf8838d1.jpg");
}
private static async Task<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.MangaDex.Api.{resourceName}");
}
}

View File

@@ -0,0 +1,108 @@
using MangaReader.Core.Search;
using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Core.Sources.MangaDex.Search;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Sources.MangaDex.Search;
public class MangaDexSearchTests
{
[Fact]
public async Task Get_Search_Result()
{
MangaDexCollectionResponse collectionResponse = new()
{
Result = "ok",
Response = "collection",
Data =
[
new MangaEntity()
{
Id = new Guid("ee96e2b7-9af2-4864-9656-649f4d3b6fec"),
Type = "manga",
Attributes = new()
{
Title = new()
{
{ "en", "Gals Cant Be Kind to Otaku!?" }
}
},
Relationships =
[
new CoverArtEntity()
{
Id = new Guid("a06943fd-6309-49a8-a66a-8df0f6dc41eb"),
Type = "cover_art",
Attributes = new()
{
FileName = "6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg"
}
}
]
},
new MangaEntity()
{
Id = new Guid("16c34950-954c-4f0d-808e-d8278a546339"),
Type = "manga",
Attributes = new()
{
Title = new()
{
{ "en", "Suufungo no Mirai ga Wakaru You ni Natta Kedo, Onnagokoro wa Wakaranai." }
}
},
Relationships =
[
new CoverArtEntity()
{
Id = new Guid("ee8588b5-145f-4eee-981a-eb604856fbd2"),
Type = "cover_art",
Attributes = new()
{
FileName = "7d301e1e-642b-4b7d-b65b-9777b36e80bf.jpg"
}
}
]
},
new MangaEntity()
{
Id = new Guid("f395bfc6-e52f-4f64-9cfb-87037215d214"),
Type = "manga",
Attributes = new()
{
Title = new()
{
{ "en", "Ienai Himitsu No Aishikata" }
}
},
Relationships =
[
new CoverArtEntity()
{
Id = new Guid("40df2d2e-b786-4aa9-9218-e3ed168cd96e"),
Type = "cover_art",
Attributes = new()
{
FileName = "c00a33cd-b26b-4554-a9f0-d6885c81eb36.jpg"
}
}
]
}
]
};
IMangaDexClient mangaDexClient = Substitute.For<IMangaDexClient>();
mangaDexClient.SearchMangaByTitleAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(collectionResponse);
MangaDexSearchProvider searchProvider = new(mangaDexClient);
MangaSearchResult[] searchResult = await searchProvider.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
searchResult.Length.ShouldBe(3);
searchResult[0].Title.ShouldBe("Gals Cant Be Kind to Otaku!?");
searchResult[0].Url.ShouldBe("https://mangadex.org/title/ee96e2b7-9af2-4864-9656-649f4d3b6fec/gals-can-t-be-kind-to-otaku");
searchResult[0].Thumbnail.ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
}
}

View File

@@ -1,51 +0,0 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.WebCrawlers.MangaDex;
public class MangaDexMetadataTests
{
[Fact]
public async Task Get_Manga_Metadata()
{
string resourceName = "MangaReader.Tests.WebCrawlers.MangaDex.MetadataSample.json";
string searchResultJson = await ResourceHelper.ReadJsonResourceAsync(resourceName);
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse mangaDexResponse = await mangaDexClient.GetMangaAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexResponse.Response.ShouldBe("entity");
mangaDexResponse.ShouldBeOfType<MangaDexEntityResponse>();
MangaDexEntityResponse? mangaDexEntityResponse = mangaDexResponse as MangaDexEntityResponse;
mangaDexEntityResponse.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldBeOfType<MangaEntity>();
MangaEntity? mangaEntity = mangaDexEntityResponse.Data as MangaEntity;
mangaEntity.ShouldNotBeNull();
mangaEntity.Attributes.Title.ShouldContainKey("en");
mangaEntity.Attributes.Title["en"].ShouldBe("Gals Cant Be Kind to Otaku!?");
mangaEntity.Attributes.Description.ShouldContainKey("en");
mangaEntity.Attributes.Description["en"].ShouldBe("Takuya Seo is an otaku who likes \"anime for girls\" and can't say he likes it out loud. One day, he hooks up with two gals from his class, Amane and Ijichi, but it seems that Amane is also an otaku...");
mangaEntity.Attributes.Tags.Count.ShouldBe(5);
mangaEntity.Attributes.Tags[0].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[0].Attributes.Name["en"].ShouldBe("Romance");
mangaEntity.Attributes.Tags[1].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[1].Attributes.Name["en"].ShouldBe("Comedy");
mangaEntity.Attributes.Tags[2].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[2].Attributes.Name["en"].ShouldBe("School Life");
mangaEntity.Attributes.Tags[3].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[3].Attributes.Name["en"].ShouldBe("Slice of Life");
mangaEntity.Attributes.Tags[4].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[4].Attributes.Name["en"].ShouldBe("Gyaru");
}
}