Added abstraction layeer IHtmlLoader. Finished reorganizing test project folder structure.

This commit is contained in:
2025-06-09 00:09:59 -04:00
parent b5d22c3c7e
commit c26ed11bfc
30 changed files with 1966 additions and 132 deletions

View File

@@ -4,11 +4,10 @@ public class Manga
{ {
public int MangaId { get; set; } public int MangaId { get; set; }
public required string Slug { get; set; } public required string Slug { get; set; }
public required string Title { get; set; }
public string? Description { get; set; }
public virtual ICollection<MangaCover> Covers { get; set; } = []; public virtual ICollection<MangaCover> Covers { get; set; } = [];
public virtual ICollection<MangaTitle> Titles { get; set; } = []; public virtual ICollection<MangaTitle> Titles { get; set; } = [];
public virtual ICollection<MangaDescription> Descriptions { get; set; } = [];
public virtual ICollection<MangaSource> Sources { get; set; } = []; public virtual ICollection<MangaSource> Sources { get; set; } = [];
public virtual ICollection<MangaContributor> Contributors { get; set; } = []; public virtual ICollection<MangaContributor> Contributors { get; set; } = [];
public virtual ICollection<MangaGenre> Genres { get; set; } = []; public virtual ICollection<MangaGenre> Genres { get; set; } = [];

View File

@@ -7,6 +7,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
public DbSet<Manga> Mangas { get; set; } public DbSet<Manga> Mangas { get; set; }
public DbSet<MangaCover> MangaCovers { get; set; } public DbSet<MangaCover> MangaCovers { get; set; }
public DbSet<MangaTitle> MangaTitles { get; set; } public DbSet<MangaTitle> MangaTitles { get; set; }
public DbSet<MangaDescription> MangaDescriptions { get; set; }
public DbSet<Source> Sources { get; set; } public DbSet<Source> Sources { get; set; }
public DbSet<MangaSource> MangaSources { get; set; } public DbSet<MangaSource> MangaSources { get; set; }
public DbSet<Contributor> Contributors { get; set; } public DbSet<Contributor> Contributors { get; set; }
@@ -24,6 +25,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
ConfigureManga(modelBuilder); ConfigureManga(modelBuilder);
ConfigureMangaCover(modelBuilder); ConfigureMangaCover(modelBuilder);
ConfigureMangaTitle(modelBuilder); ConfigureMangaTitle(modelBuilder);
ConfigureMangaDescription(modelBuilder);
ConfigureSource(modelBuilder); ConfigureSource(modelBuilder);
ConfigureMangaSource(modelBuilder); ConfigureMangaSource(modelBuilder);
ConfigureContributor(modelBuilder); ConfigureContributor(modelBuilder);
@@ -41,9 +43,9 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
.Entity<Manga>() .Entity<Manga>()
.HasKey(x => x.MangaId); .HasKey(x => x.MangaId);
modelBuilder.Entity<Manga>() //modelBuilder.Entity<Manga>()
.HasIndex(x => x.Title) // .HasIndex(x => x.Title)
.IsUnique(); // .IsUnique();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.HasIndex(x => x.Slug) .HasIndex(x => x.Slug)
@@ -81,7 +83,15 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
.HasKey(mangaTitle => mangaTitle.MangaTitleId); .HasKey(mangaTitle => mangaTitle.MangaTitleId);
modelBuilder.Entity<MangaTitle>() modelBuilder.Entity<MangaTitle>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name }) .Property(mt => mt.Name)
.IsRequired();
modelBuilder.Entity<MangaTitle>()
.Property(mt => mt.Language)
.IsRequired();
modelBuilder.Entity<MangaTitle>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name, mangaTitle.Language })
.IsUnique(); .IsUnique();
modelBuilder modelBuilder
@@ -96,6 +106,36 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
} }
private static void ConfigureMangaDescription(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<MangaDescription>()
.HasKey(mangaTitle => mangaTitle.MangaTitleId);
modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Name)
.IsRequired();
modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Language)
.IsRequired();
modelBuilder.Entity<MangaDescription>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name, mangaTitle.Language })
.IsUnique();
modelBuilder
.Entity<MangaDescription>()
.HasIndex(mangaTitle => mangaTitle.Name);
modelBuilder
.Entity<MangaDescription>()
.HasOne(x => x.Manga)
.WithMany(x => x.Descriptions)
.HasForeignKey(x => x.MangaId)
.OnDelete(DeleteBehavior.Cascade);
}
private static void ConfigureSource(ModelBuilder modelBuilder) private static void ConfigureSource(ModelBuilder modelBuilder)
{ {
modelBuilder modelBuilder

View File

@@ -1,4 +1,6 @@
namespace MangaReader.Core.Data; using MangaReader.Core.Common;
namespace MangaReader.Core.Data;
public class MangaTitle public class MangaTitle
{ {
@@ -8,5 +10,6 @@ public class MangaTitle
public required Manga Manga { get; set; } public required Manga Manga { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public TitleType TitleType { get; set; } public required Language Language { get; set; }
public bool IsPrimary { get; set; }
} }

View File

@@ -1,12 +1,12 @@
namespace MangaReader.Core.Data; namespace MangaReader.Core.Data;
public enum TitleType //public enum TitleType
{ //{
Primary, // Primary,
OfficialTranslation, // OfficialTranslation,
FanTranslation, // FanTranslation,
Synonym, // Synonym,
Abbreviation, // Abbreviation,
Romaji, // Romaji,
Japanese // Japanese
} //}

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService; using MangaReader.Core.Http;
using MangaReader.Core.Metadata; using MangaReader.Core.Metadata;
using MangaReader.Core.Search; using MangaReader.Core.Search;
using MangaReader.Core.Sources.MangaDex.Api; using MangaReader.Core.Sources.MangaDex.Api;

View File

@@ -0,0 +1,16 @@
using HtmlAgilityPack;
namespace MangaReader.Core.Http;
public class HtmlLoader(IHttpService httpService) : IHtmlLoader
{
public async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
string html = await httpService.GetStringAsync(url, cancellationToken);
HtmlDocument doc = new();
doc.LoadHtml(html);
return doc;
}
}

View File

@@ -1,4 +1,4 @@
namespace MangaReader.Core.HttpService; namespace MangaReader.Core.Http;
public class HttpService(HttpClient httpClient) : IHttpService public class HttpService(HttpClient httpClient) : IHttpService
{ {

View File

@@ -0,0 +1,8 @@
using HtmlAgilityPack;
namespace MangaReader.Core.Http;
public interface IHtmlLoader
{
Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken);
}

View File

@@ -1,4 +1,4 @@
namespace MangaReader.Core.HttpService; namespace MangaReader.Core.Http;
public interface IHttpService public interface IHttpService
{ {

View File

@@ -1,21 +1,17 @@
using HtmlAgilityPack; namespace MangaReader.Core.Metadata;
namespace MangaReader.Core.Metadata;
public abstract class MangaWebCrawler : IMangaMetadataProvider public abstract class MangaWebCrawler : IMangaMetadataProvider
{ {
public abstract string SourceId { get; } public abstract string SourceId { get; }
public abstract Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken); public abstract Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken);
protected virtual async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken) //protected virtual async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{ //{
HtmlWeb web = new() // HtmlWeb web = new()
{ // {
UsingCacheIfExists = false // UsingCacheIfExists = false
}; // };
//return web.Load(url); // return await web.LoadFromWebAsync(url, cancellationToken);
//}
return await web.LoadFromWebAsync(url, cancellationToken);
}
} }

View File

@@ -2,8 +2,8 @@
public class SourceManga public class SourceManga
{ {
public required string Title { get; set; } public required SourceMangaTitle Title { get; set; }
public string? Description { get; set; } public SourceMangaDescription? Description { get; set; }
public List<SourceMangaTitle> AlternateTitles { get; set; } = []; public List<SourceMangaTitle> AlternateTitles { get; set; } = [];
public SourceMangaContributor[] Contributors { get; set; } = []; public SourceMangaContributor[] Contributors { get; set; } = [];
public MangaStatus Status { get; set; } = MangaStatus.Unknown; public MangaStatus Status { get; set; } = MangaStatus.Unknown;

View File

@@ -0,0 +1,9 @@
using MangaReader.Core.Common;
namespace MangaReader.Core.Metadata;
public class SourceMangaDescription
{
public required string Name { get; set; }
public Language Language { get; set; }
}

View File

@@ -4,6 +4,6 @@ namespace MangaReader.Core.Metadata;
public class SourceMangaTitle public class SourceMangaTitle
{ {
public required string Title { get; set; } public required string Name { get; set; }
public Language Language { get; set; } public Language Language { get; set; }
} }

View File

@@ -7,6 +7,12 @@ namespace MangaReader.Core.Pipeline;
public partial class MangaPipeline(MangaContext context) : IMangaPipeline public partial class MangaPipeline(MangaContext context) : IMangaPipeline
{ {
enum TitleType
{
Primary,
Secondary
}
public async Task RunAsync(MangaPipelineRequest request) public async Task RunAsync(MangaPipelineRequest request)
{ {
string sourceName = request.SourceName; string sourceName = request.SourceName;
@@ -17,10 +23,12 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
Manga manga = await GetOrAddMangaAsync(sourceManga); Manga manga = await GetOrAddMangaAsync(sourceManga);
await AddMangaSourceAsync(sourceUrl, manga, source); await AddMangaSourceAsync(sourceUrl, manga, source);
await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary);
await AddDescriptionAsync(manga, sourceManga.Description);
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles) foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
{ {
await AddTitleAsync(manga, alternateTitle); await AddTitleAsync(manga, alternateTitle, TitleType.Secondary);
} }
foreach (string genre in sourceManga.Genres) foreach (string genre in sourceManga.Genres)
@@ -55,15 +63,15 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
private async Task<Manga> GetOrAddMangaAsync(SourceManga sourceManga) private async Task<Manga> GetOrAddMangaAsync(SourceManga sourceManga)
{ {
Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga => manga.Title == sourceManga.Title); Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga =>
manga.Titles.Any(mangaTitle => mangaTitle.Name == sourceManga.Title.Name));
if (manga != null) if (manga != null)
return manga; return manga;
manga = new() manga = new()
{ {
Title = sourceManga.Title, Slug = GenerateSlug(sourceManga.Title.Name),
Slug = GenerateSlug(sourceManga.Title),
}; };
context.Add(manga); context.Add(manga);
@@ -104,10 +112,10 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
context.MangaSources.Add(mangaSource); context.MangaSources.Add(mangaSource);
} }
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle) private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle, TitleType titleType)
{ {
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt => MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt =>
mt.Manga == manga && mt.Name == sourceMangaTitle.Title); mt.Manga == manga && mt.Name == sourceMangaTitle.Name);
if (mangaTitle != null) if (mangaTitle != null)
return; return;
@@ -115,12 +123,35 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
mangaTitle = new() mangaTitle = new()
{ {
Manga = manga, Manga = manga,
Name = sourceMangaTitle.Title, Name = sourceMangaTitle.Name,
Language = sourceMangaTitle.Language,
IsPrimary = titleType == TitleType.Primary
}; };
context.MangaTitles.Add(mangaTitle); context.MangaTitles.Add(mangaTitle);
} }
private async Task AddDescriptionAsync(Manga manga, SourceMangaDescription? sourceMangaDescription)
{
if (sourceMangaDescription == null)
return;
MangaDescription? mangaDescription = await context.MangaDescriptions.FirstOrDefaultAsync(md =>
md.Manga == manga && md.Name == sourceMangaDescription.Name);
if (mangaDescription != null)
return;
mangaDescription = new()
{
Manga = manga,
Name = sourceMangaDescription.Name,
Language = sourceMangaDescription.Language
};
context.MangaDescriptions.Add(mangaDescription);
}
private async Task LinkGenreAsync(Manga manga, string genreName) private async Task LinkGenreAsync(Manga manga, string genreName)
{ {
Genre genre = await GetOrAddGenreAsync(genreName); Genre genre = await GetOrAddGenreAsync(genreName);
@@ -156,28 +187,28 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
return genre; return genre;
} }
private async Task AddChapterAsync(Manga manga, SourceMangaChapter sourceeMangaChapter) private async Task AddChapterAsync(Manga manga, SourceMangaChapter sourceMangaChapter)
{ {
MangaChapter mangaChapter = await context.MangaChapters.FirstOrDefaultAsync(x => x.ChapterNumber == sourceeMangaChapter.Number) MangaChapter mangaChapter = await context.MangaChapters.FirstOrDefaultAsync(x => x.ChapterNumber == sourceMangaChapter.Number)
?? AddMangaChapter(manga, sourceeMangaChapter); ?? AddMangaChapter(manga, sourceMangaChapter);
if (mangaChapter.VolumeNumber is null && sourceeMangaChapter.Volume is not null) if (mangaChapter.VolumeNumber is null && sourceMangaChapter.Volume is not null)
{ {
mangaChapter.VolumeNumber = sourceeMangaChapter.Volume; mangaChapter.VolumeNumber = sourceMangaChapter.Volume;
} }
if (mangaChapter.Title is null && sourceeMangaChapter.Title is not null) if (mangaChapter.Title is null && sourceMangaChapter.Title is not null)
{ {
mangaChapter.Title = sourceeMangaChapter.Title; mangaChapter.Title = sourceMangaChapter.Title;
} }
} }
private MangaChapter AddMangaChapter(Manga manga, SourceMangaChapter sourceeMangaChapter) private MangaChapter AddMangaChapter(Manga manga, SourceMangaChapter sourceMangaChapter)
{ {
MangaChapter mangaChapter = new() MangaChapter mangaChapter = new()
{ {
Manga = manga, Manga = manga,
ChapterNumber = sourceeMangaChapter.Number ChapterNumber = sourceMangaChapter.Number
}; };
context.MangaChapters.Add(mangaChapter); context.MangaChapters.Add(mangaChapter);

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService; using MangaReader.Core.Http;
using System.Text.Json; using System.Text.Json;
namespace MangaReader.Core.Search; namespace MangaReader.Core.Search;

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService; using MangaReader.Core.Http;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;

View File

@@ -50,7 +50,16 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
return mangaGuid; return mangaGuid;
} }
private static string GetTitle(MangaAttributes attributes) private static SourceMangaTitle GetTitle(MangaAttributes attributes)
{
return new()
{
Name = GetTileName(attributes),
Language = Language.English
};
}
private static string GetTileName(MangaAttributes attributes)
{ {
if (attributes.Title.TryGetValue("en", out string? title)) if (attributes.Title.TryGetValue("en", out string? title))
return title; return title;
@@ -81,7 +90,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
SourceMangaTitle sourceMangaTitle = new() SourceMangaTitle sourceMangaTitle = new()
{ {
Title = alternateTitle[alternateTitleKey], Name = alternateTitle[alternateTitleKey],
Language = language Language = language
}; };

View File

@@ -1,23 +1,28 @@
using HtmlAgilityPack; using HtmlAgilityPack;
using MangaReader.Core.Common; using MangaReader.Core.Common;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata; using MangaReader.Core.Metadata;
using System.Text; using System.Text;
using System.Web; using System.Web;
namespace MangaReader.Core.Sources.MangaNato.Metadata; namespace MangaReader.Core.Sources.MangaNato.Metadata;
public class MangaNatoWebCrawler : MangaWebCrawler public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
{ {
public override string SourceId => "MangaNato"; public override string SourceId => "MangaNato";
public override async Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken) public override async Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken)
{ {
HtmlDocument document = await GetHtmlDocumentAsync(url, cancellationToken); HtmlDocument document = await htmlLoader.GetHtmlDocumentAsync(url, cancellationToken);
MangaNatoMangaDocument node = new(document); MangaNatoMangaDocument node = new(document);
SourceManga manga = new() SourceManga manga = new()
{ {
Title = node.TitleNode?.InnerText ?? string.Empty, Title = new()
{
Name = node.TitleNode?.InnerText ?? string.Empty,
Language = Language.Unknown
},
AlternateTitles = GetAlternateTitles(node.AlternateTitlesNode), AlternateTitles = GetAlternateTitles(node.AlternateTitlesNode),
Contributors = GetContributors(node.AuthorsNode), Contributors = GetContributors(node.AuthorsNode),
Status = GetStatus(node.StatusNode), Status = GetStatus(node.StatusNode),
@@ -26,7 +31,11 @@ public class MangaNatoWebCrawler : MangaWebCrawler
RatingPercent = GetRatingPercent(node.AverageRatingNode, node.BestRatingNode), RatingPercent = GetRatingPercent(node.AverageRatingNode, node.BestRatingNode),
Votes = node.VotesNode != null ? int.Parse(node.VotesNode.InnerText) : 0, Votes = node.VotesNode != null ? int.Parse(node.VotesNode.InnerText) : 0,
Views = GetViews(node.ViewsNode), Views = GetViews(node.ViewsNode),
Description = GetTextFromNodes(node.StoryDescriptionTextNodes), Description = new()
{
Name = GetTextFromNodes(node.StoryDescriptionTextNodes),
Language = Language.Unknown
},
Chapters = GetChapters(node.ChapterNodes) Chapters = GetChapters(node.ChapterNodes)
}; };
@@ -46,7 +55,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
{ {
SourceMangaTitle sourceMangaTitle = new() SourceMangaTitle sourceMangaTitle = new()
{ {
Title = title, Name = title,
Language = Language.Unknown Language = Language.Unknown
}; };

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService; using MangaReader.Core.Http;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;

View File

@@ -1,20 +1,25 @@
using HtmlAgilityPack; using HtmlAgilityPack;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata; using MangaReader.Core.Metadata;
namespace MangaReader.Core.Sources.NatoManga.Metadata; namespace MangaReader.Core.Sources.NatoManga.Metadata;
public class NatoMangaWebCrawler : MangaWebCrawler public class NatoMangaWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
{ {
public override string SourceId => "NatoManga"; public override string SourceId => "NatoManga";
public override async Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken) public override async Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken)
{ {
HtmlDocument document = await GetHtmlDocumentAsync(url, cancellationToken); HtmlDocument document = await htmlLoader.GetHtmlDocumentAsync(url, cancellationToken);
NatoMangaHtmlDocument node = new(document); NatoMangaHtmlDocument node = new(document);
SourceManga manga = new() SourceManga manga = new()
{ {
Title = node.TitleNode?.InnerText ?? string.Empty, Title = new()
{
Name = node.TitleNode?.InnerText ?? string.Empty,
Language = Common.Language.Unknown
},
Genres = GetGenres(node.GenresNode), Genres = GetGenres(node.GenresNode),
Chapters = GetChapters(node.ChapterNodes) Chapters = GetChapters(node.ChapterNodes)
}; };

View File

@@ -13,16 +13,17 @@
<None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" /> <None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<None Remove="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" /> <None Remove="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" />
<None Remove="Sources\MangaDex\Api\Manga-Search-Response-2.json" /> <None Remove="Sources\MangaDex\Api\Manga-Search-Response-2.json" />
<None Remove="Sources\NatoManga\Api\Manga-Chapter-Response.html" />
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" /> <None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="WebCrawlers\NatoManga\SampleMangaPage.html"> <EmbeddedResource Include="Sources\NatoManga\Metadata\Manga-Response.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </EmbeddedResource>
<Content Include="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm"> <EmbeddedResource Include="Sources\MangaNato\Metadata\Manga-Response.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -30,6 +31,7 @@
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" /> <EmbeddedResource Include="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response-2.json" /> <EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response-2.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response.json" /> <EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Sources\NatoManga\Metadata\Manga-Chapter-Response.html" />
<EmbeddedResource Include="Sources\NatoManga\Api\Manga-Search-Response.json" /> <EmbeddedResource Include="Sources\NatoManga\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" /> <EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" /> <EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
@@ -59,4 +61,9 @@
<Using Include="Xunit" /> <Using Include="Xunit" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="WebCrawlers\NatoManga\" />
<Folder Include="WebCrawlers\Samples\" />
</ItemGroup>
</Project> </Project>

View File

@@ -17,12 +17,16 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
var sourceManga = new SourceManga var sourceManga = new SourceManga
{ {
Title = "Fullmetal Alchemist", Title = new()
{
Name = "Fullmetal Alchemist",
Language = Language.English
},
AlternateTitles = AlternateTitles =
[ [
new() new()
{ {
Title = "Hagane no Renkinjutsushi", Name = "Hagane no Renkinjutsushi",
Language = Language.Romaji Language = Language.Romaji
} }
], ],
@@ -49,7 +53,10 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
await pipeline.RunAsync(request); await pipeline.RunAsync(request);
context.Mangas.ShouldHaveSingleItem(); context.Mangas.ShouldHaveSingleItem();
context.MangaTitles.ShouldHaveSingleItem(); context.MangaTitles.Count().ShouldBe(2);
context.MangaTitles.Where(mt => mt.IsPrimary).ShouldHaveSingleItem();
context.MangaTitles.Where(mt => mt.IsPrimary).First().Name.ShouldBe("Fullmetal Alchemist");
context.MangaTitles.Where(mt => mt.IsPrimary).First().Language.ShouldBe(Language.English);
context.Genres.Count().ShouldBe(2); context.Genres.Count().ShouldBe(2);
context.MangaChapters.ShouldHaveSingleItem(); context.MangaChapters.ShouldHaveSingleItem();
} }

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService; using MangaReader.Core.Http;
using MangaReader.Core.Sources.MangaDex.Api; using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Tests.Utilities; using MangaReader.Tests.Utilities;
using NSubstitute; using NSubstitute;

View File

@@ -229,23 +229,23 @@ public class MangaDexMetadataTests
SourceManga? sourceManga = await metadataProvider.GetMangaAsync("https://mangadex.org/title/ee96e2b7-9af2-4864-9656-649f4d3b6fec/gals-can-t-be-kind-to-otaku", CancellationToken.None); 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.ShouldNotBeNull();
sourceManga.Title.ShouldBe("Gals Cant Be Kind to Otaku!?"); sourceManga.Title.Name.ShouldBe("Gals Cant Be Kind to Otaku!?");
sourceManga.AlternateTitles.Count.ShouldBe(5); sourceManga.AlternateTitles.Count.ShouldBe(5);
sourceManga.AlternateTitles[0].Title.ShouldBe("オタクに優しいギャルはいない!?"); sourceManga.AlternateTitles[0].Name.ShouldBe("オタクに優しいギャルはいない!?");
sourceManga.AlternateTitles[0].Language.ShouldBe(Language.Japanese); sourceManga.AlternateTitles[0].Language.ShouldBe(Language.Japanese);
sourceManga.AlternateTitles[1].Title.ShouldBe("Otaku ni Yasashii Gal wa Inai!?"); sourceManga.AlternateTitles[1].Name.ShouldBe("Otaku ni Yasashii Gal wa Inai!?");
sourceManga.AlternateTitles[1].Language.ShouldBe(Language.Romaji); sourceManga.AlternateTitles[1].Language.ShouldBe(Language.Romaji);
sourceManga.AlternateTitles[2].Title.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?"); sourceManga.AlternateTitles[2].Name.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?");
sourceManga.AlternateTitles[2].Language.ShouldBe(Language.Romaji); sourceManga.AlternateTitles[2].Language.ShouldBe(Language.Romaji);
sourceManga.AlternateTitles[3].Title.ShouldBe("Gal Can't Be Kind to Otaku!?"); sourceManga.AlternateTitles[3].Name.ShouldBe("Gal Can't Be Kind to Otaku!?");
sourceManga.AlternateTitles[3].Language.ShouldBe(Language.English); sourceManga.AlternateTitles[3].Language.ShouldBe(Language.English);
sourceManga.AlternateTitles[4].Title.ShouldBe("Gals Can't Be Kind To A Geek!?"); sourceManga.AlternateTitles[4].Name.ShouldBe("Gals Can't Be Kind To A Geek!?");
sourceManga.AlternateTitles[4].Language.ShouldBe(Language.English); sourceManga.AlternateTitles[4].Language.ShouldBe(Language.English);
sourceManga.Genres.Count.ShouldBe(5); sourceManga.Genres.Count.ShouldBe(5);

View File

@@ -1,47 +1,35 @@
using HtmlAgilityPack; using MangaReader.Core.Http;
using MangaReader.Core.Metadata; using MangaReader.Core.Metadata;
using MangaReader.Core.Sources.MangaNato.Metadata; using MangaReader.Core.Sources.MangaNato.Metadata;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly; using Shouldly;
using System.Data; using System.Data;
using System.Xml.Linq;
namespace MangaReader.Tests.WebCrawlers; namespace MangaReader.Tests.Sources.MangaNato.Metadata;
public class UnitTest1 public class MangaNatoMetadataTests
{ {
class TestMangaNatoWebCrawler : MangaNatoWebCrawler
{
protected override Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
HtmlWeb web = new()
{
UsingCacheIfExists = false
};
return Task.FromResult(web.Load(url));
}
}
private readonly string samplesPath;
private readonly string mangaNatoSampleFilePath;
public UnitTest1()
{
samplesPath = Path.Combine(AppContext.BaseDirectory, "WebCrawlers", "Samples");
mangaNatoSampleFilePath = Path.Combine(samplesPath, "MangaNato - Please Go Home, Akutsu-San!.htm");
}
[Fact] [Fact]
public async Task Get_Manga() public async Task Get_Manga()
{ {
var webCrawler = new TestMangaNatoWebCrawler(); string mangaHtml = await ReadJsonResourceAsync("Manga-Response.html");
var manga = await webCrawler.GetMangaAsync(mangaNatoSampleFilePath, CancellationToken.None);
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(mangaHtml));
HtmlLoader htmlLoader = new(httpService);
MangaNatoWebCrawler webCrawler = new(htmlLoader);
SourceManga? manga = await webCrawler.GetMangaAsync("/test-url", CancellationToken.None);
manga.ShouldNotBeNull(); manga.ShouldNotBeNull();
manga.Title.ShouldBe("Please Go Home, Akutsu-San!"); manga.Title.Name.ShouldBe("Please Go Home, Akutsu-San!");
manga.AlternateTitles.Select(x => x.Title).ShouldBe([ manga.AlternateTitles.Select(x => x.Name).ShouldBe([
"Kaette kudasai! Akutsu-san", "Kaette kudasai! Akutsu-san",
"Yankee Musume", "Yankee Musume",
"ヤンキー娘", "ヤンキー娘",
@@ -62,8 +50,8 @@ public class UnitTest1
manga.Votes.ShouldBe(15979); manga.Votes.ShouldBe(15979);
//manga.Description.ShouldStartWith("Ooyama-kun normally doesnt get involved with Akutsu-san, a delinquent girl in his class"); //manga.Description.ShouldStartWith("Ooyama-kun normally doesnt get involved with Akutsu-san, a delinquent girl in his class");
manga.Description.ShouldStartWith("Ooyama-kun normally doesnt get involved with Akutsu-san, a delinquent girl in his class"); manga.Description?.Name.ShouldStartWith("Ooyama-kun normally doesnt get involved with Akutsu-san, a delinquent girl in his class");
manga.Description.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935"); manga.Description?.Name.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935");
manga.Chapters.Count.ShouldBe(236); manga.Chapters.Count.ShouldBe(236);
@@ -79,4 +67,9 @@ public class UnitTest1
manga.Chapters[235].Views.ShouldBe(232_200); manga.Chapters[235].Views.ShouldBe(232_200);
manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0)); manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0));
} }
private static async Task<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.MangaNato.Metadata.{resourceName}");
}
} }

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService; using MangaReader.Core.Http;
using MangaReader.Core.Sources.NatoManga.Api; using MangaReader.Core.Sources.NatoManga.Api;
using MangaReader.Tests.Utilities; using MangaReader.Tests.Utilities;
using NSubstitute; using NSubstitute;
@@ -34,6 +34,9 @@ public class NatoMangaClientTests
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None) httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson)); .Returns(Task.FromResult(searchResultJson));
httpService.GetStringAsync(Arg.Any<string>(), Arg.Any<IDictionary<string,string>>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
NatoMangaClient natoMangaClient = new(httpService); NatoMangaClient natoMangaClient = new(httpService);
NatoMangaSearchResult[] searchResults = await natoMangaClient.SearchAsync("Gal Can't Be Kind", CancellationToken.None); NatoMangaSearchResult[] searchResults = await natoMangaClient.SearchAsync("Gal Can't Be Kind", CancellationToken.None);

File diff suppressed because one or more lines are too long

View File

@@ -1,35 +1,32 @@
using HtmlAgilityPack; using MangaReader.Core.Http;
using MangaReader.Core.Metadata;
using MangaReader.Core.Sources.NatoManga.Metadata; using MangaReader.Core.Sources.NatoManga.Metadata;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly; using Shouldly;
namespace MangaReader.Tests.WebCrawlers.NatoManga; namespace MangaReader.Tests.Sources.NatoManga.Metadata;
public class NatoMangaWebCrawlerTests public class NatoMangaWebCrawlerTests
{ {
class TestNatoMangaWebCrawler : NatoMangaWebCrawler
{
protected override Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
HtmlWeb web = new()
{
UsingCacheIfExists = false
};
return Task.FromResult(web.Load(url));
}
}
[Fact] [Fact]
public async Task Get_Manga() public async Task Get_Manga()
{ {
string sampleFilePath = Path.Combine(AppContext.BaseDirectory, "WebCrawlers", "NatoManga", "SampleMangaPage.html"); string mangaHtml = await ReadJsonResourceAsync("Manga-Response.html");
var webCrawler = new TestNatoMangaWebCrawler(); IHttpService httpService = Substitute.For<IHttpService>();
var manga = await webCrawler.GetMangaAsync(sampleFilePath, CancellationToken.None);
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(mangaHtml));
HtmlLoader htmlLoader = new(httpService);
NatoMangaWebCrawler webCrawler = new(htmlLoader);
SourceManga? manga = await webCrawler.GetMangaAsync("/test-url", CancellationToken.None);
manga.ShouldNotBeNull(); manga.ShouldNotBeNull();
manga.Title.ShouldBe("Gal Cant Be Kind to Otaku!?"); manga.Title.Name.ShouldBe("Gal Cant Be Kind to Otaku!?");
//manga.AlternateTitles.ShouldBe([ //manga.AlternateTitles.ShouldBe([
// "Kaette kudasai! Akutsu-san", // "Kaette kudasai! Akutsu-san",
@@ -63,4 +60,9 @@ public class NatoMangaWebCrawlerTests
//manga.Chapters[235].Views.ShouldBe(232_200); //manga.Chapters[235].Views.ShouldBe(232_200);
//manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0)); //manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0));
} }
private static async Task<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.NatoManga.Metadata.{resourceName}");
}
} }