diff --git a/MangaReader.Core/Metadata/SourceManga.cs b/MangaReader.Core/Metadata/SourceManga.cs index ebc5ef0..b0add23 100644 --- a/MangaReader.Core/Metadata/SourceManga.cs +++ b/MangaReader.Core/Metadata/SourceManga.cs @@ -4,9 +4,8 @@ public class SourceManga { public required string Title { get; set; } public string? Description { get; set; } - public List AlternateTitles { get; set; } = []; - public List Authors { get; set; } = []; - public List Artists { get; set; } = []; + public List AlternateTitles { get; set; } = []; + public SourceMangaContributor[] Contributors { get; set; } = []; public MangaStatus Status { get; set; } = MangaStatus.Unknown; public List Genres { get; set; } = []; public DateTime? UpdateDate { get; set; } diff --git a/MangaReader.Core/Metadata/SourceMangaChapter.cs b/MangaReader.Core/Metadata/SourceMangaChapter.cs index 7b8f8db..2bdc236 100644 --- a/MangaReader.Core/Metadata/SourceMangaChapter.cs +++ b/MangaReader.Core/Metadata/SourceMangaChapter.cs @@ -4,7 +4,7 @@ public class SourceMangaChapter { public int? Volume { get; set; } public required float Number { get; set; } - public string? Name { get; set; } + public string? Title { get; set; } public required string Url { get; set; } public long? Views { get; set; } public DateTime? UploadDate { get; set; } diff --git a/MangaReader.Core/Metadata/SourceMangaContributor.cs b/MangaReader.Core/Metadata/SourceMangaContributor.cs new file mode 100644 index 0000000..0773dd9 --- /dev/null +++ b/MangaReader.Core/Metadata/SourceMangaContributor.cs @@ -0,0 +1,7 @@ +namespace MangaReader.Core.Metadata; + +public class SourceMangaContributor +{ + public required string Name { get; set; } + public SourceMangaContributorRole Role { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Metadata/SourceMangaContributorRole.cs b/MangaReader.Core/Metadata/SourceMangaContributorRole.cs new file mode 100644 index 0000000..4f7fb61 --- /dev/null +++ b/MangaReader.Core/Metadata/SourceMangaContributorRole.cs @@ -0,0 +1,8 @@ +namespace MangaReader.Core.Metadata; + +public enum SourceMangaContributorRole +{ + Unknown, + Author, + Artist +} \ No newline at end of file diff --git a/MangaReader.Core/Metadata/SourceMangaLanguage.cs b/MangaReader.Core/Metadata/SourceMangaLanguage.cs new file mode 100644 index 0000000..f022220 --- /dev/null +++ b/MangaReader.Core/Metadata/SourceMangaLanguage.cs @@ -0,0 +1,9 @@ +namespace MangaReader.Core.Metadata; + +public enum SourceMangaLanguage +{ + Unknown, + Japanese, + Romanji, + English +} \ No newline at end of file diff --git a/MangaReader.Core/Metadata/SourceMangaTitle.cs b/MangaReader.Core/Metadata/SourceMangaTitle.cs new file mode 100644 index 0000000..c72bb30 --- /dev/null +++ b/MangaReader.Core/Metadata/SourceMangaTitle.cs @@ -0,0 +1,7 @@ +namespace MangaReader.Core.Metadata; + +public class SourceMangaTitle +{ + public required string Title { get; set; } + public SourceMangaLanguage Language { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipeline.cs b/MangaReader.Core/Pipeline/MangaPipeline.cs index 16cac66..24f9e09 100644 --- a/MangaReader.Core/Pipeline/MangaPipeline.cs +++ b/MangaReader.Core/Pipeline/MangaPipeline.cs @@ -11,7 +11,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline { Manga manga = await GetOrAddMangaAsync(sourceManga); - foreach (string alternateTitle in sourceManga.AlternateTitles) + foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) { await AddTitleAsync(manga, alternateTitle); } @@ -62,9 +62,10 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline [GeneratedRegex(@"\s+")] private static partial Regex RemoveSpacesWithDashRegex(); - private async Task AddTitleAsync(Manga manga, string title) + private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle) { - MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => mt.Manga == manga && mt.TitleEntry == title); + MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => + mt.Manga == manga && mt.TitleEntry == sourceMangaTitle.Title); if (mangaTitle != null) return; @@ -72,7 +73,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline mangaTitle = new() { Manga = manga, - TitleEntry = title, + TitleEntry = sourceMangaTitle.Title, }; context.MangaTitles.Add(mangaTitle); @@ -123,9 +124,9 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline mangaChapter.VolumeNumber = sourceeMangaChapter.Volume; } - if (mangaChapter.Title is null && sourceeMangaChapter.Name is not null) + if (mangaChapter.Title is null && sourceeMangaChapter.Title is not null) { - mangaChapter.Title = sourceeMangaChapter.Name; + mangaChapter.Title = sourceeMangaChapter.Title; } } diff --git a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs index 5b5db15..2dd573c 100644 --- a/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Metadata/MangaDexMetadataProvider.cs @@ -12,10 +12,27 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe Guid mangaGuid = GetSourceMangaGuid(url); MangaDexResponse? mangaDexResponse = await mangaDexClient.GetMangaAsync(mangaGuid, cancellationToken); - if (mangaDexResponse == null) + if (mangaDexResponse == null || mangaDexResponse is not MangaDexEntityResponse mangaDexEntityResponse) return null; - throw new NotImplementedException(); + if (mangaDexEntityResponse.Data == null || mangaDexEntityResponse.Data is not MangaEntity mangaEntity) + return null; + + if (mangaEntity.Attributes == null) + return null; + + MangaAttributes mangaAttributes = mangaEntity.Attributes; + List mangaRelationships = mangaEntity.Relationships; + MangaDexResponse? mangaDexFeedResponse = await mangaDexClient.GetFeedAsync(mangaGuid, cancellationToken); + + return new SourceManga() + { + Title = GetTitle(mangaAttributes), + AlternateTitles = GetAlternateTitles(mangaAttributes), + Genres = GetGenres(mangaAttributes), + Contributors = GetContributors(mangaRelationships), + Chapters = GetChapters(mangaDexFeedResponse) + }; } private static Guid GetSourceMangaGuid(string url) @@ -29,4 +46,139 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe return mangaGuid; } + + private static string GetTitle(MangaAttributes attributes) + { + if (attributes.Title.TryGetValue("en", out string? title)) + return title; + + return string.Empty; + } + + private static List GetAlternateTitles(MangaAttributes attributes) + { + if (attributes.AltTitles == null || attributes.AltTitles.Count == 0) + return []; + + Dictionary languageIdMap = new() + { + { "en", SourceMangaLanguage.English }, + { "ja", SourceMangaLanguage.Japanese }, + { "ja-ro", SourceMangaLanguage.Romanji }, + }; + + List sourceMangaTitles = []; + + foreach (Dictionary alternateTitle in attributes.AltTitles) + { + foreach (string alternateTitleKey in alternateTitle.Keys) + { + if (languageIdMap.TryGetValue(alternateTitleKey, out SourceMangaLanguage language) == false) + continue; + + SourceMangaTitle sourceMangaTitle = new() + { + Title = alternateTitle[alternateTitleKey], + Language = language + }; + + sourceMangaTitles.Add(sourceMangaTitle); + } + } + + return sourceMangaTitles; + } + + private static List GetGenres(MangaAttributes attributes) + { + if (attributes.Tags == null || attributes.Tags.Count == 0) + return []; + + List tags = []; + + foreach (TagEntity tagEntity in attributes.Tags) + { + if (tagEntity.Attributes == null) + continue; + + if (tagEntity.Attributes.Name == null || tagEntity.Attributes.Name.Count == 0) + continue; + + tags.Add(tagEntity.Attributes.Name.FirstOrDefault().Value); + } + + return tags; + } + + private static SourceMangaContributor[] GetContributors(List relationships) + { + List contributors = []; + + AuthorEntity[] authorEntities = [.. relationships.Where(entity => entity is AuthorEntity).Cast()]; + ArtistEntity[] artistEntities = [.. relationships.Where(entity => entity is ArtistEntity).Cast()]; + + foreach (AuthorEntity authorEntity in authorEntities) + { + if (authorEntity.Attributes == null) + continue; + + SourceMangaContributor contributor = new() + { + Name = authorEntity.Attributes.Name, + Role = SourceMangaContributorRole.Author + }; + + contributors.Add(contributor); + } + + foreach (ArtistEntity artistEntity in artistEntities) + { + if (artistEntity.Attributes == null) + continue; + + SourceMangaContributor contributor = new() + { + Name = artistEntity.Attributes.Name, + Role = SourceMangaContributorRole.Artist + }; + + contributors.Add(contributor); + } + + return [.. contributors]; + } + + private static List GetChapters(MangaDexResponse? mangaDexFeedResponse) + { + if (mangaDexFeedResponse == null || mangaDexFeedResponse is not MangaDexCollectionResponse collectionResponse) + return []; + + //https://mangadex.org/chapter/46084762-855c-46dd-a7b6-66e5cd15604d + + List chapters = []; + ChapterEntity[] chapterEntities = [.. collectionResponse.Data.Where(entity => entity is ChapterEntity).Cast()]; + + foreach (ChapterEntity chapterEntity in chapterEntities) + { + if (chapterEntity.Attributes == null || chapterEntity.Attributes.TranslatedLanguage != "en") + continue; + + int? volume = int.TryParse(chapterEntity.Attributes.Volume, out var temp) ? temp : null; + + if (float.TryParse(chapterEntity.Attributes.Chapter, out float chapterNumber) == false) + continue; + + SourceMangaChapter chapter = new() + { + Volume = volume, + Number = chapterNumber, + Title = chapterEntity.Attributes.Title, + Url = $"https://mangadex.org/chapter/{chapterEntity.Id}" + }; + + chapters.Add(chapter); + } + + return chapters; + } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs index 7c0bff3..b3b18b0 100644 --- a/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs +++ b/MangaReader.Core/Sources/MangaNato/Metadata/MangaNatoWebCrawler.cs @@ -18,7 +18,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler { Title = node.TitleNode?.InnerText ?? string.Empty, AlternateTitles = GetAlternateTitles(node.AlternateTitlesNode), - Authors = GetAuthors(node.AuthorsNode), + Contributors = GetContributors(node.AuthorsNode), Status = GetStatus(node.StatusNode), Genres = GetGenres(node.GenresNode), UpdateDate = GetUpdateDate(node.UpdateDateNode), @@ -32,20 +32,50 @@ public class MangaNatoWebCrawler : MangaWebCrawler return manga; } - private static List GetAlternateTitles(HtmlNode? node) + private static List GetAlternateTitles(HtmlNode? node) { if (node == null) return []; - return [.. node.InnerText.Split(';').Select(x => x.Trim())]; + List sourceMangaTitles = []; + + string[] titles = [.. node.InnerText.Split(';').Select(x => x.Trim())]; + + foreach (string title in titles) + { + SourceMangaTitle sourceMangaTitle = new() + { + Title = title, + Language = SourceMangaLanguage.Unknown + }; + + sourceMangaTitles.Add(sourceMangaTitle); + } + + return [.. sourceMangaTitles]; } - private static List GetAuthors(HtmlNode? node) + private static SourceMangaContributor[] GetContributors(HtmlNode? node) { if (node == null) return []; - return [.. node.InnerText.Split('-').Select(x => x.Trim())]; + List contributors = []; + + string[] names = [.. node.InnerText.Split('-').Select(x => x.Trim())]; + + foreach (string name in names) + { + SourceMangaContributor contributor = new() + { + Name = name, + Role = SourceMangaContributorRole.Author + }; + + contributors.Add(contributor); + } + + return [.. contributors]; } private static MangaStatus GetStatus(HtmlNode? node) @@ -138,7 +168,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler SourceMangaChapter chapter = new() { Number = GetChapterNumber(chapterNameNode), - Name = chapterNameNode?.InnerText ?? string.Empty, + Title = chapterNameNode?.InnerText ?? string.Empty, Url = chapterNameNode?.Attributes["href"].Value ?? string.Empty, Views = GetViews(chapterViewNode), UploadDate = chapterTimeNode != null ? DateTime.Parse(chapterTimeNode.Attributes["title"].Value) : null diff --git a/MangaReader.Core/Sources/NatoManga/Api/INatoMangaClient.cs b/MangaReader.Core/Sources/NatoManga/Api/INatoMangaClient.cs index 3b257f3..8e48085 100644 --- a/MangaReader.Core/Sources/NatoManga/Api/INatoMangaClient.cs +++ b/MangaReader.Core/Sources/NatoManga/Api/INatoMangaClient.cs @@ -1,73 +1,6 @@ -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; +namespace MangaReader.Core.Sources.NatoManga.Api; public interface INatoMangaClient { Task 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 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(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; - } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs b/MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs new file mode 100644 index 0000000..a0a3d38 --- /dev/null +++ b/MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs @@ -0,0 +1,64 @@ +using MangaReader.Core.HttpService; +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace MangaReader.Core.Sources.NatoManga.Api; + +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 SearchAsync(string searchWord, CancellationToken cancellationToken) + { + string url = GetSearchUrl(searchWord); + + string response = await httpService.GetStringAsync(url, cancellationToken); + + return JsonSerializer.Deserialize(response, _jsonSerializerOptions) ?? []; + } + + protected string GetSearchUrl(string searchWord) + { + string formattedSeachWord = GetFormattedSearchWord(searchWord); + + return $"https://www.natomanga.com/home/search/json?searchword={formattedSeachWord}"; + } + + private static string GetFormattedSearchWord(string searchWord) + { + if (string.IsNullOrWhiteSpace(searchWord)) + return string.Empty; + + // Convert to lowercase and normalize to decompose accents + string normalized = searchWord.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; + } +} \ No newline at end of file diff --git a/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs b/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs index e7abf40..b30f8e6 100644 --- a/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs +++ b/MangaReader.Core/Sources/NatoManga/Metadata/NatoMangaWebCrawler.cs @@ -96,7 +96,7 @@ public class NatoMangaWebCrawler : MangaWebCrawler SourceMangaChapter chapter = new() { Number = GetChapterNumber(chapterNameNode), - Name = chapterNameNode.InnerText, + Title = chapterNameNode.InnerText, Url = chapterNameNode.Attributes["href"].Value, Views = GetViews(chapterViewNode), UploadDate = DateTime.Parse(chapterTimeNode.Attributes["title"].Value) diff --git a/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs new file mode 100644 index 0000000..0b8e642 --- /dev/null +++ b/MangaReader.Tests/Sources/MangaDex/Metadata/MangaDexMetadataTests.cs @@ -0,0 +1,252 @@ +using MangaReader.Core.Metadata; +using MangaReader.Core.Sources.MangaDex.Api; +using MangaReader.Core.Sources.MangaDex.Metadata; +using NSubstitute; +using Shouldly; + +namespace MangaReader.Tests.Sources.MangaDex.Metadata; + +public class MangaDexMetadataTests +{ + [Fact] + public async Task Get_Manga() + { + MangaDexEntityResponse entityResponse = new() + { + Result = "ok", + Response = "entity", + Data = new MangaEntity() + { + Id = new Guid("ee96e2b7-9af2-4864-9656-649f4d3b6fec"), + Type = "manga", + Attributes = new() + { + Title = + { + { "en", "Gals Can’t Be Kind to Otaku!?" } + }, + AltTitles = + [ + new() + { + { "ja", "オタクに優しいギャルはいない!?" } + }, + new() + { + { "ja-ro", "Otaku ni Yasashii Gal wa Inai!?" } + }, + new() + { + { "ja-ro", "Otaku ni Yasashii Gyaru ha Inai!?" } + }, + new() + { + { "en", "Gal Can't Be Kind to Otaku!?" } + }, + new() + { + { "en", "Gals Can't Be Kind To A Geek!?" } + } + ], + Tags = + [ + new() + { + Id = new Guid("423e2eae-a7a2-4a8b-ac03-a8351462d71d"), + Type = "tag", + Attributes = new() + { + Group = "genre", + Name = + { + { "en", "Romance" } + } + } + }, + new() + { + Id = new Guid("4d32cc48-9f00-4cca-9b5a-a839f0764984"), + Type = "tag", + Attributes = new() + { + Group = "genre", + Name = + { + { "en", "Comedy" } + } + } + }, + new() + { + Id = new Guid("caaa44eb-cd40-4177-b930-79d3ef2afe87"), + Type = "tag", + Attributes = new() + { + Group = "theme", + Name = + { + { "en", "School Life" } + } + } + }, + new() + { + Id = new Guid("e5301a23-ebd9-49dd-a0cb-2add944c7fe9"), + Type = "tag", + Attributes = new() + { + Group = "genre", + Name = + { + { "en", "Slice of Life" } + } + } + }, + new() + { + Id = new Guid("fad12b5e-68ba-460e-b933-9ae8318f5b65"), + Type = "tag", + Attributes = new() + { + Group = "theme", + Name = + { + { "en", "Gyaru" } + } + } + } + ], + }, + Relationships = + [ + new AuthorEntity() + { + Id = new Guid(), + Type = "author", + Attributes = new() + { + Name = "Norishiro-chan" + } + }, + new ArtistEntity() + { + Id = new Guid(), + Type = "artist", + Attributes = new() + { + Name = "Sakana Uozimi" + } + } + ] + } + }; + + MangaDexCollectionResponse collectionResponse = new() + { + Result = "ok", + Response = "collection", + Data = + [ + new ChapterEntity() + { + Id = new Guid("46084762-855c-46dd-a7b6-66e5cd15604d"), + Type = "chapter", + Attributes = new() + { + TranslatedLanguage = "en", + Volume = null, + Chapter = "69", + Title = "Otaku & Gyaru & Playing Couple" + } + }, + new ChapterEntity() + { + Id = new Guid("7521d1eb-0caf-4c4d-b96a-adc816ada3ec"), + Type = "chapter", + Attributes = new() + { + TranslatedLanguage = "en", + Volume = "9", + Chapter = "68", + Title = "Otaku & Gyaru & A Couple Date" + } + }, + new ChapterEntity() + { + Id = new Guid("b5206e9b-6e3e-4ef0-aa62-381fd0ff75a5"), + Type = "chapter", + Attributes = new() + { + TranslatedLanguage = "en", + Volume = "2", + Chapter = "8.1", + Title = "Otaku & Gyaru & Protegee" + } + } + ] + }; + + IMangaDexClient mangaDexClient = Substitute.For(); + + mangaDexClient.GetMangaAsync(Arg.Any(), CancellationToken.None) + .Returns(entityResponse); + + mangaDexClient.GetFeedAsync(Arg.Any(), CancellationToken.None) + .Returns(collectionResponse); + + 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); + + sourceManga.ShouldNotBeNull(); + sourceManga.Title.ShouldBe("Gals Can’t Be Kind to Otaku!?"); + + sourceManga.AlternateTitles.Count.ShouldBe(5); + + sourceManga.AlternateTitles[0].Title.ShouldBe("オタクに優しいギャルはいない!?"); + sourceManga.AlternateTitles[0].Language.ShouldBe(SourceMangaLanguage.Japanese); + + sourceManga.AlternateTitles[1].Title.ShouldBe("Otaku ni Yasashii Gal wa Inai!?"); + sourceManga.AlternateTitles[1].Language.ShouldBe(SourceMangaLanguage.Romanji); + + sourceManga.AlternateTitles[2].Title.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?"); + sourceManga.AlternateTitles[2].Language.ShouldBe(SourceMangaLanguage.Romanji); + + sourceManga.AlternateTitles[3].Title.ShouldBe("Gal Can't Be Kind to Otaku!?"); + sourceManga.AlternateTitles[3].Language.ShouldBe(SourceMangaLanguage.English); + + sourceManga.AlternateTitles[4].Title.ShouldBe("Gals Can't Be Kind To A Geek!?"); + sourceManga.AlternateTitles[4].Language.ShouldBe(SourceMangaLanguage.English); + + sourceManga.Genres.Count.ShouldBe(5); + sourceManga.Genres[0].ShouldBe("Romance"); + sourceManga.Genres[1].ShouldBe("Comedy"); + sourceManga.Genres[2].ShouldBe("School Life"); + sourceManga.Genres[3].ShouldBe("Slice of Life"); + sourceManga.Genres[4].ShouldBe("Gyaru"); + + sourceManga.Contributors.Length.ShouldBe(2); + + sourceManga.Contributors[0].Name.ShouldBe("Norishiro-chan"); + sourceManga.Contributors[0].Role.ShouldBe(SourceMangaContributorRole.Author); + + sourceManga.Contributors[1].Name.ShouldBe("Sakana Uozimi"); + sourceManga.Contributors[1].Role.ShouldBe(SourceMangaContributorRole.Artist); + + sourceManga.Chapters.Count.ShouldBe(3); + + sourceManga.Chapters[0].Volume.ShouldBeNull(); + sourceManga.Chapters[0].Number.ShouldBe(69); + sourceManga.Chapters[0].Title.ShouldBe("Otaku & Gyaru & Playing Couple"); + sourceManga.Chapters[0].Url.ShouldBe("https://mangadex.org/chapter/46084762-855c-46dd-a7b6-66e5cd15604d"); + + sourceManga.Chapters[1].Volume.ShouldBe(9); + sourceManga.Chapters[1].Number.ShouldBe(68); + sourceManga.Chapters[1].Title.ShouldBe("Otaku & Gyaru & A Couple Date"); + sourceManga.Chapters[1].Url.ShouldBe("https://mangadex.org/chapter/7521d1eb-0caf-4c4d-b96a-adc816ada3ec"); + + sourceManga.Chapters[2].Volume.ShouldBe(2); + 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"); + } +} \ No newline at end of file diff --git a/MangaReader.Tests/WebCrawlers/NatoManga/NatoMangaWebCrawlerTests.cs b/MangaReader.Tests/WebCrawlers/NatoManga/NatoMangaWebCrawlerTests.cs index 7342062..56eee6b 100644 --- a/MangaReader.Tests/WebCrawlers/NatoManga/NatoMangaWebCrawlerTests.cs +++ b/MangaReader.Tests/WebCrawlers/NatoManga/NatoMangaWebCrawlerTests.cs @@ -53,13 +53,13 @@ public class NatoMangaWebCrawlerTests manga.Chapters[0].Url.ShouldBe("https://www.natomanga.com/manga/gal-cant-be-kind-to-otaku/chapter-69"); manga.Chapters[0].Number.ShouldBe(69); - manga.Chapters[0].Name.ShouldBe("Chapter 69"); + manga.Chapters[0].Title.ShouldBe("Chapter 69"); manga.Chapters[0].Views.ShouldBe(8146); //manga.Chapters[0].UploadDate.ShouldBe(new DateTime(2025, 4, 23, 17, 17, 0)); //manga.Chapters[235].URL.ShouldBe("https://chapmanganato.to/manga-hf984788/chapter-0.1"); //manga.Chapters[235].Number.ShouldBe(0.1f); - //manga.Chapters[235].Name.ShouldBe("Vol.0 Chapter : Oneshot"); + //manga.Chapters[235].Title.ShouldBe("Vol.0 Chapter : Oneshot"); //manga.Chapters[235].Views.ShouldBe(232_200); //manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0)); } diff --git a/MangaReader.Tests/WebCrawlers/UnitTest1.cs b/MangaReader.Tests/WebCrawlers/UnitTest1.cs index 5bc8a64..8724b9c 100644 --- a/MangaReader.Tests/WebCrawlers/UnitTest1.cs +++ b/MangaReader.Tests/WebCrawlers/UnitTest1.cs @@ -2,6 +2,8 @@ using MangaReader.Core.Metadata; using MangaReader.Core.Sources.MangaNato.Metadata; using Shouldly; +using System.Data; +using System.Xml.Linq; namespace MangaReader.Tests.WebCrawlers; @@ -39,13 +41,19 @@ public class UnitTest1 manga.Title.ShouldBe("Please Go Home, Akutsu-San!"); - manga.AlternateTitles.ShouldBe([ + manga.AlternateTitles.Select(x => x.Title).ShouldBe([ "Kaette kudasai! Akutsu-san", "Yankee Musume", "ヤンキー娘", "帰ってください! 阿久津さん"]); - manga.Authors.ShouldBe(["Nagaoka Taichi"]); + SourceMangaContributor[] expectedContributors = + [ + new() { Name = "Nagaoka Taichi", Role = SourceMangaContributorRole.Author } + ]; + + manga.Contributors.ShouldBeEquivalentTo(expectedContributors); + manga.Status.ShouldBe(MangaStatus.Ongoing); manga.Genres.ShouldBe(["Comedy", "Romance", "School life"]); manga.UpdateDate.ShouldBe(new DateTime(2024, 9, 26, 0, 12, 0)); @@ -61,13 +69,13 @@ public class UnitTest1 manga.Chapters[0].Url.ShouldBe("https://chapmanganato.to/manga-hf984788/chapter-186"); manga.Chapters[0].Number.ShouldBe(186); - manga.Chapters[0].Name.ShouldBe("Chapter 186"); + manga.Chapters[0].Title.ShouldBe("Chapter 186"); manga.Chapters[0].Views.ShouldBe(37_900); manga.Chapters[0].UploadDate.ShouldBe(new DateTime(2024, 9, 26, 0, 9, 0)); manga.Chapters[235].Url.ShouldBe("https://chapmanganato.to/manga-hf984788/chapter-0.1"); manga.Chapters[235].Number.ShouldBe(0.1f); - manga.Chapters[235].Name.ShouldBe("Vol.0 Chapter : Oneshot"); + manga.Chapters[235].Title.ShouldBe("Vol.0 Chapter : Oneshot"); manga.Chapters[235].Views.ShouldBe(232_200); manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0)); }