diff --git a/MangaReader.Core/Metadata/SourceManga.cs b/MangaReader.Core/Metadata/SourceManga.cs index b0add23..3a5bbcc 100644 --- a/MangaReader.Core/Metadata/SourceManga.cs +++ b/MangaReader.Core/Metadata/SourceManga.cs @@ -13,4 +13,5 @@ public class SourceManga public float? RatingPercent { get; set; } public int? Votes { get; set; } public List Chapters { get; set; } = []; + public string[] CoverArt { 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 9a2a9e5..733b321 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs @@ -8,4 +8,5 @@ public interface IMangaDexClient Task GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken); Task GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken); Task GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken); + Task GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken); } \ 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 af5d3e7..f9fc9b2 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs @@ -69,5 +69,10 @@ namespace MangaReader.Core.Sources.MangaDex.Api return JsonSerializer.Deserialize(response, _jsonSerializerOptions); } + + public async Task GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken) + { + return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuid}&limit=100&offset=0", cancellationToken); + } } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs index 2dd573c..a9e662e 100644 --- a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs @@ -24,6 +24,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe MangaAttributes mangaAttributes = mangaEntity.Attributes; List mangaRelationships = mangaEntity.Relationships; MangaDexResponse? mangaDexFeedResponse = await mangaDexClient.GetFeedAsync(mangaGuid, cancellationToken); + MangaDexResponse? coverArtResponse = await mangaDexClient.GetCoverArtAsync(mangaGuid, cancellationToken); return new SourceManga() { @@ -31,7 +32,8 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe AlternateTitles = GetAlternateTitles(mangaAttributes), Genres = GetGenres(mangaAttributes), Contributors = GetContributors(mangaRelationships), - Chapters = GetChapters(mangaDexFeedResponse) + Chapters = GetChapters(mangaDexFeedResponse), + CoverArt = GetCoverArt(mangaGuid, coverArtResponse) }; } @@ -181,4 +183,25 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe return chapters; } + + private static string[] GetCoverArt(Guid mangaGuid, MangaDexResponse? coverArtResponse) + { + if (coverArtResponse == null || coverArtResponse is not MangaDexCollectionResponse collectionResponse) + return []; + + List coverArtUrls = []; + CoverArtEntity[] coverArtEntities = [.. collectionResponse.Data.Where(entity => entity is CoverArtEntity).Cast()]; + + foreach (CoverArtEntity coverArtEntity in coverArtEntities) + { + if (coverArtEntity.Attributes == null || string.IsNullOrWhiteSpace(coverArtEntity.Attributes.FileName)) + continue; + + string url = $"https://mangadex.org/covers/{mangaGuid}/{coverArtEntity.Attributes.FileName}"; + + coverArtUrls.Add(url); + } + + return [.. coverArtUrls]; + } } \ No newline at end of file diff --git a/MangaReader.Tests/MangaReader.Tests.csproj b/MangaReader.Tests/MangaReader.Tests.csproj index c36a84a..9e94d5e 100644 --- a/MangaReader.Tests/MangaReader.Tests.csproj +++ b/MangaReader.Tests/MangaReader.Tests.csproj @@ -11,6 +11,7 @@ + @@ -25,6 +26,7 @@ + diff --git a/MangaReader.Tests/Sources/MangaDex/Api/Manga-Cover-Art-Response.json b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Cover-Art-Response.json new file mode 100644 index 0000000..2cc2275 --- /dev/null +++ b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Cover-Art-Response.json @@ -0,0 +1,216 @@ +{ + "result": "ok", + "response": "collection", + "data": [ + { + "id": "0045f243-5625-4f0f-9066-a6c3a95d84d3", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "1", + "fileName": "2569ffd8-4ba1-4030-8d08-b7a21333a7a6.jpg", + "locale": "ja", + "createdAt": "2024-06-18T14:42:11+00:00", + "updatedAt": "2024-06-18T14:42:11+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "a81ad8d3-ba2c-4003-9126-fbd9d28e3732", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "2", + "fileName": "d2314e9b-4287-4e65-8045-b713d97c0b28.jpg", + "locale": "ja", + "createdAt": "2024-06-18T14:42:15+00:00", + "updatedAt": "2024-06-18T14:42:15+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "0ca43181-8ae1-4e5f-934f-4ea407e05913", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "3", + "fileName": "ed4715de-fc1b-4f50-9e12-9d2ba99e044f.jpg", + "locale": "ja", + "createdAt": "2024-06-18T14:42:15+00:00", + "updatedAt": "2024-06-18T14:42:15+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "1dce1a43-86fb-4db6-86ca-fbc4b6c5cfab", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "4", + "fileName": "b4c335cc-0e4d-4407-86ff-61e41f817e83.jpg", + "locale": "ja", + "createdAt": "2024-06-18T14:42:14+00:00", + "updatedAt": "2024-06-18T14:42:14+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "c8151575-1aac-4464-a99f-6cafa1f962c5", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "5", + "fileName": "66323609-ba33-4ade-8c64-3b08c346e6da.jpg", + "locale": "ja", + "createdAt": "2024-06-18T14:42:11+00:00", + "updatedAt": "2024-06-18T14:42:11+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "dbac6a58-4d97-4ec9-85e3-fb4a4d904590", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "6", + "fileName": "fc9d5eb5-3179-4543-ae6b-88d3231fca5b.jpg", + "locale": "ja", + "createdAt": "2024-06-18T14:42:15+00:00", + "updatedAt": "2024-06-18T14:42:15+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "d7158641-029d-4763-b621-fbdaa83ed3c4", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "7", + "fileName": "61f990f0-103a-4967-ac64-01dc9938cb5c.jpg", + "locale": "ja", + "createdAt": "2024-06-18T14:42:12+00:00", + "updatedAt": "2024-06-18T14:42:12+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "9cbbbcfc-e82d-4c83-9d82-de692f52faf1", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "8", + "fileName": "4b87e456-0243-4dfe-abda-d0f41c91141a.jpg", + "locale": "ja", + "createdAt": "2024-10-16T10:40:16+00:00", + "updatedAt": "2024-10-16T10:40:16+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "f73d34f6-f678-4f8b-9dff-5c759d5fc767", + "type": "user" + } + ] + }, + { + "id": "a06943fd-6309-49a8-a66a-8df0f6dc41eb", + "type": "cover_art", + "attributes": { + "description": "Volume 9 Cover from BookLive", + "volume": "9", + "fileName": "6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg", + "locale": "ja", + "createdAt": "2025-02-20T11:59:45+00:00", + "updatedAt": "2025-02-20T11:59:45+00:00", + "version": 1 + }, + "relationships": [ + { + "id": "ee96e2b7-9af2-4864-9656-649f4d3b6fec", + "type": "manga" + }, + { + "id": "27bde0e8-71b0-4bf2-8e25-2902a7b2dd4b", + "type": "user" + } + ] + } + ], + "limit": 100, + "offset": 0, + "total": 9 +} \ No newline at end of file diff --git a/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs index d3f3444..0beaefa 100644 --- a/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs +++ b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs @@ -22,6 +22,52 @@ public class MangaDexClientTests MangaDexResponse? mangaDexResponse = await mangaDexClient.SearchMangaByTitleAsync("Some random text", CancellationToken.None); // Testing here + mangaDexResponse.ShouldNotBeNull(); + mangaDexResponse.Response.ShouldBe("collection"); + mangaDexResponse.ShouldBeOfType(); + + MangaDexCollectionResponse mangaDexCollectionResponse = (mangaDexResponse as MangaDexCollectionResponse)!; + mangaDexCollectionResponse.Data.Count.ShouldBe(3); + + mangaDexCollectionResponse.Data[0].ShouldBeOfType(); + MangaEntity mangaEntity = (mangaDexCollectionResponse.Data[0] as MangaEntity)!; + + 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[2].ShouldBeOfType(); + + CoverArtEntity coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!; + coverArtEntity.Attributes.ShouldNotBeNull(); + coverArtEntity.Attributes.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg"); } [Fact] @@ -154,6 +200,35 @@ public class MangaDexClientTests mangaDexChapterResponse.Chapter.DataSaver[12].ShouldBe("13-b886b4ed986a473478e3db7bb18fe2faea567a1ad5e520408967410dcf8838d1.jpg"); } + [Fact] + public async Task Get_Cover_Art() + { + string searchResultJson = await ReadJsonResourceAsync("Manga-Cover-Art-Response.json"); + + IHttpService httpService = Substitute.For(); + + httpService.GetStringAsync(Arg.Any(), CancellationToken.None) + .Returns(Task.FromResult(searchResultJson)); + + MangaDexClient mangaDexClient = new(httpService); + MangaDexResponse? mangaDexResponse = await mangaDexClient.GetCoverArtAsync(Guid.NewGuid(), CancellationToken.None); + + mangaDexResponse.ShouldNotBeNull(); + mangaDexResponse.Response.ShouldBe("collection"); + mangaDexResponse.ShouldBeOfType(); + + MangaDexCollectionResponse mangaDexEntityResponse = (mangaDexResponse as MangaDexCollectionResponse)!; + + List coverArtEntities = [.. mangaDexEntityResponse.Data.Where(entity => entity is CoverArtEntity).Cast()]; + coverArtEntities.Count.ShouldBe(9); + + coverArtEntities[0].Attributes.ShouldNotBeNull(); + coverArtEntities[0].Attributes!.FileName.ShouldBe("2569ffd8-4ba1-4030-8d08-b7a21333a7a6.jpg"); + + coverArtEntities[8].Attributes.ShouldNotBeNull(); + coverArtEntities[8].Attributes!.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg"); + } + private static async Task ReadJsonResourceAsync(string resourceName) { return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.MangaDex.Api.{resourceName}"); diff --git a/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs index 0b8e642..35f7307 100644 --- a/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs +++ b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs @@ -11,7 +11,7 @@ public class MangaDexMetadataTests [Fact] public async Task Get_Manga() { - MangaDexEntityResponse entityResponse = new() + MangaDexEntityResponse mangaEntityResponse = new() { Result = "ok", Response = "entity", @@ -141,7 +141,7 @@ public class MangaDexMetadataTests } }; - MangaDexCollectionResponse collectionResponse = new() + MangaDexCollectionResponse feedCollectionResponse = new() { Result = "ok", Response = "collection", @@ -186,13 +186,43 @@ public class MangaDexMetadataTests ] }; + MangaDexCollectionResponse coverArtCollectionResponse = new() + { + Result = "ok", + Response = "collection", + Data = + [ + new CoverArtEntity() + { + Id = new Guid("0045f243-5625-4f0f-9066-a6c3a95d84d3"), + Type = "cover_art", + Attributes = new() + { + FileName = "2569ffd8-4ba1-4030-8d08-b7a21333a7a6.jpg" + } + }, + new CoverArtEntity() + { + Id = new Guid("a81ad8d3-ba2c-4003-9126-fbd9d28e3732"), + Type = "cover_art", + Attributes = new() + { + FileName = "d2314e9b-4287-4e65-8045-b713d97c0b28.jpg" + } + } + ] + }; + IMangaDexClient mangaDexClient = Substitute.For(); mangaDexClient.GetMangaAsync(Arg.Any(), CancellationToken.None) - .Returns(entityResponse); + .Returns(mangaEntityResponse); mangaDexClient.GetFeedAsync(Arg.Any(), CancellationToken.None) - .Returns(collectionResponse); + .Returns(feedCollectionResponse); + + mangaDexClient.GetCoverArtAsync(Arg.Any(), CancellationToken.None) + .Returns(coverArtCollectionResponse); MangaDexMetadataProvider metadataProvider = new(mangaDexClient); SourceManga? sourceManga = await metadataProvider.GetMangaAsync("https://mangadex.org/title/ee96e2b7-9af2-4864-9656-649f4d3b6fec/gals-can-t-be-kind-to-otaku", CancellationToken.None); @@ -248,5 +278,9 @@ public class MangaDexMetadataTests sourceManga.Chapters[2].Number.ShouldBe(8.1f); sourceManga.Chapters[2].Title.ShouldBe("Otaku & Gyaru & Protegee"); sourceManga.Chapters[2].Url.ShouldBe("https://mangadex.org/chapter/b5206e9b-6e3e-4ef0-aa62-381fd0ff75a5"); + + sourceManga.CoverArt.Length.ShouldBe(2); + sourceManga.CoverArt[0].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/2569ffd8-4ba1-4030-8d08-b7a21333a7a6.jpg"); + sourceManga.CoverArt[1].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/d2314e9b-4287-4e65-8045-b713d97c0b28.jpg"); } } \ No newline at end of file