diff --git a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs index 7487800..4d2bb89 100644 --- a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs +++ b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,12 +1,6 @@ using MangaReader.Core.Search; -using MangaReader.Core.Search.MangaDex; +using MangaReader.Core.Sources.MangaDex.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 namespace Microsoft.Extensions.DependencyInjection; diff --git a/MangaReader.Core/Sources/MangaDex/Api/ArtistEntity.cs b/MangaReader.Core/Sources/MangaDex/Api/ArtistEntity.cs index d19bcd3..25311f7 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/ArtistEntity.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/ArtistEntity.cs @@ -2,5 +2,5 @@ public class ArtistEntity : MangaDexEntity { - public required ArtistAttributes Attributes { get; set; } + public ArtistAttributes? Attributes { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/AuthorEntity.cs b/MangaReader.Core/Sources/MangaDex/Api/AuthorEntity.cs index 5231ccd..20d342b 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/AuthorEntity.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/AuthorEntity.cs @@ -2,5 +2,5 @@ public class AuthorEntity : MangaDexEntity { - public required AuthorAttributes Attributes { get; set; } + public AuthorAttributes? Attributes { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/ChapterAttributes.cs b/MangaReader.Core/Sources/MangaDex/Api/ChapterAttributes.cs index e4dfa48..a4e080f 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/ChapterAttributes.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/ChapterAttributes.cs @@ -2,8 +2,15 @@ public class ChapterAttributes { + public string? Volume { get; set; } public string? Chapter { 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 ReadableAt { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public int Pages { get; set; } + public int Version { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/ChapterEntity.cs b/MangaReader.Core/Sources/MangaDex/Api/ChapterEntity.cs index fc6fbeb..e9a2059 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/ChapterEntity.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/ChapterEntity.cs @@ -2,5 +2,5 @@ public class ChapterEntity : MangaDexEntity { - public required ChapterAttributes Attributes { get; set; } + public ChapterAttributes? Attributes { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/CoverArtEntity.cs b/MangaReader.Core/Sources/MangaDex/Api/CoverArtEntity.cs index 051d1ed..7f736a2 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/CoverArtEntity.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/CoverArtEntity.cs @@ -2,5 +2,5 @@ public class CoverArtEntity : MangaDexEntity { - public required CoverArtAttributes Attributes { get; set; } + public CoverArtAttributes? Attributes { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs b/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs index b0e8dc2..9a2a9e5 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs @@ -2,6 +2,10 @@ public interface IMangaDexClient { - Task GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken); - Task GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken); + Task SearchMangaByTitleAsync(string title, CancellationToken cancellationToken); + Task SearchMangaByAuthorAsync(string author, CancellationToken cancellationToken); + Task SearchMangaByGroupAsync(string group, CancellationToken cancellationToken); + Task GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken); + Task GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken); + Task GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/MangaDexChapter.cs b/MangaReader.Core/Sources/MangaDex/Api/MangaDexChapter.cs new file mode 100644 index 0000000..a926108 --- /dev/null +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaDexChapter.cs @@ -0,0 +1,8 @@ +namespace MangaReader.Core.Sources.MangaDex.Api; + +public class MangaDexChapter +{ + public required string Hash { get; set; } + public List Data { get; set; } = []; + public List DataSaver { get; set; } = []; +} \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/MangaDexChapterResponse.cs b/MangaReader.Core/Sources/MangaDex/Api/MangaDexChapterResponse.cs new file mode 100644 index 0000000..c905bed --- /dev/null +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaDexChapterResponse.cs @@ -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; } +} \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs b/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs index 82287ab..af5d3e7 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs @@ -1,4 +1,5 @@ using MangaReader.Core.HttpService; +using System.Text; using System.Text.Json; namespace MangaReader.Core.Sources.MangaDex.Api @@ -18,22 +19,55 @@ namespace MangaReader.Core.Sources.MangaDex.Api _jsonSerializerOptions.Converters.Add(new MangaDexEntityConverter()); } - private async Task GetAsync(string url, CancellationToken cancellationToken) + private async Task GetAsync(string url, CancellationToken cancellationToken) { string response = await httpService.GetStringAsync(url, cancellationToken); - return JsonSerializer.Deserialize(response, _jsonSerializerOptions) - ?? new() { Response = "failed", Result = "unknown" }; + return JsonSerializer.Deserialize(response, _jsonSerializerOptions); } - public async Task GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken) + public async Task 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 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 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 GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken) { return await GetAsync($"https://api.mangadex.org/manga/{mangaGuid}?includes[]=artist&includes[]=author&includes[]=cover_art", cancellationToken); } - public async Task GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken) + public async Task 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); } + + public async Task 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(response, _jsonSerializerOptions); + } } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/MangaDexEntityConverter.cs b/MangaReader.Core/Sources/MangaDex/Api/MangaDexEntityConverter.cs index c86c08f..de5c2e4 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/MangaDexEntityConverter.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaDexEntityConverter.cs @@ -22,6 +22,7 @@ public class MangaDexEntityConverter : JsonConverter "chapter" => JsonSerializer.Deserialize(root.GetRawText(), options), "scanlation_group" => JsonSerializer.Deserialize(root.GetRawText(), options), "cover_art" => JsonSerializer.Deserialize(root.GetRawText(), options), + "user" => JsonSerializer.Deserialize(root.GetRawText(), options), _ => throw new NotSupportedException($"Unknown type '{type}'") }; } diff --git a/MangaReader.Core/Sources/MangaDex/Api/MangaEntity.cs b/MangaReader.Core/Sources/MangaDex/Api/MangaEntity.cs index 6347cc8..0ac704b 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/MangaEntity.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaEntity.cs @@ -2,5 +2,5 @@ public class MangaEntity : MangaDexEntity { - public required MangaAttributes Attributes { get; set; } + public MangaAttributes? Attributes { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/PersonAttributes.cs b/MangaReader.Core/Sources/MangaDex/Api/PersonAttributes.cs index f19f6a5..c7f38dd 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/PersonAttributes.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/PersonAttributes.cs @@ -2,6 +2,7 @@ public class PersonAttributes { + public required string Name { get; set; } public string? ImageUrl { get; set; } public Dictionary Biography { get; set; } = []; public string? Twitter { get; set; } diff --git a/MangaReader.Core/Sources/MangaDex/Api/TagEntity.cs b/MangaReader.Core/Sources/MangaDex/Api/TagEntity.cs index 2e1dada..ebfe918 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/TagEntity.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/TagEntity.cs @@ -2,5 +2,5 @@ public class TagEntity : MangaDexEntity { - public required TagAttributes Attributes { get; set; } + public TagAttributes? Attributes { get; set; } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/UserAttributes.cs b/MangaReader.Core/Sources/MangaDex/Api/UserAttributes.cs new file mode 100644 index 0000000..69df318 --- /dev/null +++ b/MangaReader.Core/Sources/MangaDex/Api/UserAttributes.cs @@ -0,0 +1,8 @@ +namespace MangaReader.Core.Sources.MangaDex.Api; + +public class UserAttributes +{ + public required string UserName { get; set; } + public List Roles { get; set; } = []; + public int Version { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/UserEntity.cs b/MangaReader.Core/Sources/MangaDex/Api/UserEntity.cs new file mode 100644 index 0000000..00db5bd --- /dev/null +++ b/MangaReader.Core/Sources/MangaDex/Api/UserEntity.cs @@ -0,0 +1,6 @@ +namespace MangaReader.Core.Sources.MangaDex.Api; + +public class UserEntity : MangaDexEntity +{ + public UserAttributes? Attributes { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs index a312610..bbd8d04 100644 --- a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs @@ -1,12 +1,10 @@ -using MangaReader.Core.HttpService; -using MangaReader.Core.Sources; -using MangaReader.Core.Sources.MangaDex.Search; -using System.Text; +using MangaReader.Core.Search; +using MangaReader.Core.Sources.MangaDex.Api; using System.Text.RegularExpressions; -namespace MangaReader.Core.Search.MangaDex; +namespace MangaReader.Core.Sources.MangaDex.Search; -public partial class MangaDexSearchProvider(IHttpService httpService) : MangaSearchProviderBase(httpService), IMangaSourceComponent +public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IMangaSearchProvider, IMangaSourceComponent { [GeneratedRegex(@"[^a-z0-9\s-]")] private static partial Regex InvalidSlugCharactersRegex(); @@ -16,35 +14,50 @@ public partial class MangaDexSearchProvider(IHttpService httpService) : MangaSea public string SourceId => "MangaDex"; - protected override string GetSearchUrl(string keyword) + public async Task 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 mangaSearchResults = []; - foreach (MangaDexSearchResultData searchResultData in searchResult.Data) + foreach (MangaDexEntity entity in collectionResponse.Data) { - string title = searchResultData.Attributes.Title.FirstOrDefault().Value; - string slug = GenerateSlug(title); + MangaSearchResult? mangaSearchResult = GetMangaSearchResult(entity); - MangaSearchResult mangaSearchResult = new() - { - Title = title, - Url = $"https://mangadex.org/title/{searchResultData.Id}/{slug}", - Thumbnail = GetThumbnail(searchResultData) - }; + if (mangaSearchResult == null) + continue; mangaSearchResults.Add(mangaSearchResult); + } 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) { // title.ToLowerInvariant().Normalize(NormalizationForm.FormD); @@ -57,19 +70,19 @@ public partial class MangaDexSearchProvider(IHttpService httpService) : MangaSea 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; - if (coverArtRelationship.Attributes.TryGetValue("fileName", out object? fileNameValue) == false) + string? fileName = coverArtEntity.Attributes?.FileName; + + if (string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName)) return null; - if (fileNameValue == null) - return null; - - return $"https://mangadex.org/covers/{searchResultData.Id}/{fileNameValue}"; + return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}"; } } \ No newline at end of file diff --git a/MangaReader.Tests/MangaReader.Tests.csproj b/MangaReader.Tests/MangaReader.Tests.csproj index 4534f2a..9416704 100644 --- a/MangaReader.Tests/MangaReader.Tests.csproj +++ b/MangaReader.Tests/MangaReader.Tests.csproj @@ -11,8 +11,7 @@ - - + @@ -27,10 +26,11 @@ - + + - - + + @@ -56,4 +56,8 @@ + + + + diff --git a/MangaReader.Tests/Search/MangaDex/MangaDexSearchTests.cs b/MangaReader.Tests/Search/MangaDex/MangaDexSearchTests.cs deleted file mode 100644 index 2cb170f..0000000 --- a/MangaReader.Tests/Search/MangaDex/MangaDexSearchTests.cs +++ /dev/null @@ -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(); - 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(); - - httpService.GetStringAsync(Arg.Any(), 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 Can’t 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"); - } -} \ No newline at end of file diff --git a/MangaReader.Tests/Sources/MangaDex/Api/Manga-Chapter-Response.json b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Chapter-Response.json new file mode 100644 index 0000000..e6a0ace --- /dev/null +++ b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Chapter-Response.json @@ -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" ] + } +} \ No newline at end of file diff --git a/MangaReader.Tests/WebCrawlers/MangaDex/MetadataSample-Feed.json b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Feed-Response.json similarity index 100% rename from MangaReader.Tests/WebCrawlers/MangaDex/MetadataSample-Feed.json rename to MangaReader.Tests/Sources/MangaDex/Api/Manga-Feed-Response.json diff --git a/MangaReader.Tests/WebCrawlers/MangaDex/MetadataSample.json b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Response.json similarity index 100% rename from MangaReader.Tests/WebCrawlers/MangaDex/MetadataSample.json rename to MangaReader.Tests/Sources/MangaDex/Api/Manga-Response.json diff --git a/MangaReader.Tests/Search/MangaDex/SampleSearchResult.json b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Search-Response.json similarity index 100% rename from MangaReader.Tests/Search/MangaDex/SampleSearchResult.json rename to MangaReader.Tests/Sources/MangaDex/Api/Manga-Search-Response.json diff --git a/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs new file mode 100644 index 0000000..d3f3444 --- /dev/null +++ b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs @@ -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(); + + httpService.GetStringAsync(Arg.Any(), 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(); + + httpService.GetStringAsync(Arg.Any(), 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 = mangaDexResponse as MangaDexEntityResponse; + mangaDexEntityResponse.ShouldNotBeNull(); + mangaDexEntityResponse.Data.ShouldNotBeNull(); + mangaDexEntityResponse.Data.ShouldBeOfType(); + + MangaEntity? mangaEntity = mangaDexEntityResponse.Data as MangaEntity; + mangaEntity.ShouldNotBeNull(); + + mangaEntity.Attributes.ShouldNotBeNull(); + mangaEntity.Attributes.Title.ShouldContainKey("en"); + mangaEntity.Attributes.Title["en"].ShouldBe("Gals Can’t 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(); + mangaEntity.Relationships[1].ShouldBeOfType(); + mangaEntity.Relationships[2].ShouldBeOfType(); + mangaEntity.Relationships[3].ShouldBeOfType(); + + 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(); + + httpService.GetStringAsync(Arg.Any(), 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 mangaDexEntityResponse = (mangaDexResponse as MangaDexCollectionResponse)!; + + List chapterEntities = [.. mangaDexEntityResponse.Data.FindAll(entity => entity is ChapterEntity).Cast()]; + 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(); + + httpService.GetStringAsync(Arg.Any(), 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 ReadJsonResourceAsync(string resourceName) + { + return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.MangaDex.Api.{resourceName}"); + } +} \ No newline at end of file diff --git a/MangaReader.Tests/Sources/MangaDex/Search/MangaDexSearchTests.cs b/MangaReader.Tests/Sources/MangaDex/Search/MangaDexSearchTests.cs new file mode 100644 index 0000000..013b5c5 --- /dev/null +++ b/MangaReader.Tests/Sources/MangaDex/Search/MangaDexSearchTests.cs @@ -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 Can’t 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(); + + mangaDexClient.SearchMangaByTitleAsync(Arg.Any(), 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 Can’t 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"); + } +} \ No newline at end of file diff --git a/MangaReader.Tests/WebCrawlers/MangaDex/MangaDexMetadataTests.cs b/MangaReader.Tests/WebCrawlers/MangaDex/MangaDexMetadataTests.cs deleted file mode 100644 index 1a70512..0000000 --- a/MangaReader.Tests/WebCrawlers/MangaDex/MangaDexMetadataTests.cs +++ /dev/null @@ -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(); - - httpService.GetStringAsync(Arg.Any(), 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 = mangaDexResponse as MangaDexEntityResponse; - mangaDexEntityResponse.ShouldNotBeNull(); - mangaDexEntityResponse.Data.ShouldNotBeNull(); - mangaDexEntityResponse.Data.ShouldBeOfType(); - - MangaEntity? mangaEntity = mangaDexEntityResponse.Data as MangaEntity; - mangaEntity.ShouldNotBeNull(); - mangaEntity.Attributes.Title.ShouldContainKey("en"); - mangaEntity.Attributes.Title["en"].ShouldBe("Gals Can’t 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"); - } -} \ No newline at end of file