Finished MangaDexMetadataProvider logic.
This commit is contained in:
@@ -4,9 +4,8 @@ public class SourceManga
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public List<string> AlternateTitles { get; set; } = [];
|
||||
public List<string> Authors { get; set; } = [];
|
||||
public List<string> Artists { get; set; } = [];
|
||||
public List<SourceMangaTitle> AlternateTitles { get; set; } = [];
|
||||
public SourceMangaContributor[] Contributors { get; set; } = [];
|
||||
public MangaStatus Status { get; set; } = MangaStatus.Unknown;
|
||||
public List<string> Genres { get; set; } = [];
|
||||
public DateTime? UpdateDate { get; set; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
7
MangaReader.Core/Metadata/SourceMangaContributor.cs
Normal file
7
MangaReader.Core/Metadata/SourceMangaContributor.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public class SourceMangaContributor
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public SourceMangaContributorRole Role { get; set; }
|
||||
}
|
||||
8
MangaReader.Core/Metadata/SourceMangaContributorRole.cs
Normal file
8
MangaReader.Core/Metadata/SourceMangaContributorRole.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public enum SourceMangaContributorRole
|
||||
{
|
||||
Unknown,
|
||||
Author,
|
||||
Artist
|
||||
}
|
||||
9
MangaReader.Core/Metadata/SourceMangaLanguage.cs
Normal file
9
MangaReader.Core/Metadata/SourceMangaLanguage.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public enum SourceMangaLanguage
|
||||
{
|
||||
Unknown,
|
||||
Japanese,
|
||||
Romanji,
|
||||
English
|
||||
}
|
||||
7
MangaReader.Core/Metadata/SourceMangaTitle.cs
Normal file
7
MangaReader.Core/Metadata/SourceMangaTitle.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public class SourceMangaTitle
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public SourceMangaLanguage Language { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MangaDexEntity> 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<SourceMangaTitle> GetAlternateTitles(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.AltTitles == null || attributes.AltTitles.Count == 0)
|
||||
return [];
|
||||
|
||||
Dictionary<string, SourceMangaLanguage> languageIdMap = new()
|
||||
{
|
||||
{ "en", SourceMangaLanguage.English },
|
||||
{ "ja", SourceMangaLanguage.Japanese },
|
||||
{ "ja-ro", SourceMangaLanguage.Romanji },
|
||||
};
|
||||
|
||||
List<SourceMangaTitle> sourceMangaTitles = [];
|
||||
|
||||
foreach (Dictionary<string, string> 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<string> GetGenres(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.Tags == null || attributes.Tags.Count == 0)
|
||||
return [];
|
||||
|
||||
List<string> 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<MangaDexEntity> relationships)
|
||||
{
|
||||
List<SourceMangaContributor> contributors = [];
|
||||
|
||||
AuthorEntity[] authorEntities = [.. relationships.Where(entity => entity is AuthorEntity).Cast<AuthorEntity>()];
|
||||
ArtistEntity[] artistEntities = [.. relationships.Where(entity => entity is ArtistEntity).Cast<ArtistEntity>()];
|
||||
|
||||
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<SourceMangaChapter> GetChapters(MangaDexResponse? mangaDexFeedResponse)
|
||||
{
|
||||
if (mangaDexFeedResponse == null || mangaDexFeedResponse is not MangaDexCollectionResponse collectionResponse)
|
||||
return [];
|
||||
|
||||
//https://mangadex.org/chapter/46084762-855c-46dd-a7b6-66e5cd15604d
|
||||
|
||||
List<SourceMangaChapter> chapters = [];
|
||||
ChapterEntity[] chapterEntities = [.. collectionResponse.Data.Where(entity => entity is ChapterEntity).Cast<ChapterEntity>()];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<string> GetAlternateTitles(HtmlNode? node)
|
||||
private static List<SourceMangaTitle> GetAlternateTitles(HtmlNode? node)
|
||||
{
|
||||
if (node == null)
|
||||
return [];
|
||||
|
||||
return [.. node.InnerText.Split(';').Select(x => x.Trim())];
|
||||
List<SourceMangaTitle> 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);
|
||||
}
|
||||
|
||||
private static List<string> GetAuthors(HtmlNode? node)
|
||||
return [.. sourceMangaTitles];
|
||||
}
|
||||
|
||||
private static SourceMangaContributor[] GetContributors(HtmlNode? node)
|
||||
{
|
||||
if (node == null)
|
||||
return [];
|
||||
|
||||
return [.. node.InnerText.Split('-').Select(x => x.Trim())];
|
||||
List<SourceMangaContributor> 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
|
||||
|
||||
@@ -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<NatoMangaSearchResult[]> SearchAsync(string searchWord, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public partial class NatoMangaClient(IHttpService httpService) : INatoMangaClient
|
||||
{
|
||||
[GeneratedRegex(@"[^a-z0-9]+")]
|
||||
private static partial Regex NonAlphaNumericCharactersRegex();
|
||||
|
||||
[GeneratedRegex("_{2,}")]
|
||||
private static partial Regex ExtendedUnderscoresRegex();
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public async Task<NatoMangaSearchResult[]> SearchAsync(string searchWord, CancellationToken cancellationToken)
|
||||
{
|
||||
string formattedSearchWord = GetFormattedSearchWord(searchWord);
|
||||
string url = $"https://www.natomanga.com/home/search/json?searchword={formattedSearchWord}";
|
||||
|
||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<NatoMangaSearchResult[]>(response, _jsonSerializerOptions) ?? [];
|
||||
}
|
||||
|
||||
protected string GetSearchUrl(string keyword)
|
||||
{
|
||||
string formattedSeachWord = GetFormattedSearchWord(keyword);
|
||||
|
||||
return $"https://www.natomanga.com/home/search/json?searchword={formattedSeachWord}";
|
||||
}
|
||||
|
||||
private static string GetFormattedSearchWord(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return string.Empty;
|
||||
|
||||
// Convert to lowercase and normalize to decompose accents
|
||||
string normalized = input.ToLowerInvariant()
|
||||
.Normalize(NormalizationForm.FormD);
|
||||
|
||||
// Remove diacritics
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in normalized)
|
||||
{
|
||||
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
||||
sb.Append(c);
|
||||
}
|
||||
|
||||
// Replace non-alphanumeric characters with underscores
|
||||
string cleaned = NonAlphaNumericCharactersRegex().Replace(sb.ToString(), "_");
|
||||
|
||||
// Trim and collapse underscores
|
||||
cleaned = ExtendedUnderscoresRegex().Replace(cleaned, "_").Trim('_');
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
64
MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs
Normal file
64
MangaReader.Core/Sources/NatoManga/Api/NatoMangaClient.cs
Normal file
@@ -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<NatoMangaSearchResult[]> SearchAsync(string searchWord, CancellationToken cancellationToken)
|
||||
{
|
||||
string url = GetSearchUrl(searchWord);
|
||||
|
||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<NatoMangaSearchResult[]>(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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<IMangaDexClient>();
|
||||
|
||||
mangaDexClient.GetMangaAsync(Arg.Any<Guid>(), CancellationToken.None)
|
||||
.Returns(entityResponse);
|
||||
|
||||
mangaDexClient.GetFeedAsync(Arg.Any<Guid>(), 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");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user