Compare commits
10 Commits
4e5be6c910
...
d747f68df8
| Author | SHA1 | Date | |
|---|---|---|---|
| d747f68df8 | |||
| 4797d3c559 | |||
| 33e521e8bb | |||
| a82eab0ecb | |||
| 000a20bb0f | |||
| c26ed11bfc | |||
| b5d22c3c7e | |||
| 70513559cb | |||
| 7dbcdc6169 | |||
| 1348684144 |
8
MangaReader.Core/Common/ContributorRole.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace MangaReader.Core.Common;
|
||||
|
||||
public enum ContributorRole
|
||||
{
|
||||
Unknown,
|
||||
Author,
|
||||
Artist
|
||||
}
|
||||
9
MangaReader.Core/Common/Language.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MangaReader.Core.Common;
|
||||
|
||||
public enum Language
|
||||
{
|
||||
Unknown,
|
||||
Japanese,
|
||||
Romaji,
|
||||
English
|
||||
}
|
||||
@@ -4,13 +4,13 @@ public class Manga
|
||||
{
|
||||
public int MangaId { get; set; }
|
||||
public required string Slug { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int? Year { get; set; }
|
||||
|
||||
public virtual ICollection<MangaCover> Covers { 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<MangaContributor> Contributors { get; set; } = [];
|
||||
public virtual ICollection<MangaGenre> Genres { get; set; } = [];
|
||||
public virtual ICollection<MangaChapter> Chapters { get; set; } = [];
|
||||
//public virtual ICollection<MangaChapter> Chapters { get; set; } = [];
|
||||
}
|
||||
@@ -7,15 +7,22 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
|
||||
public DbSet<Manga> Mangas { get; set; }
|
||||
public DbSet<MangaCover> MangaCovers { get; set; }
|
||||
public DbSet<MangaTitle> MangaTitles { get; set; }
|
||||
public DbSet<MangaDescription> MangaDescriptions { get; set; }
|
||||
public DbSet<Source> Sources { get; set; }
|
||||
public DbSet<MangaSource> MangaSources { get; set; }
|
||||
public DbSet<Contributor> Contributors { get; set; }
|
||||
public DbSet<MangaContributor> MangaContributors { get; set; }
|
||||
public DbSet<Genre> Genres { get; set; }
|
||||
public DbSet<MangaGenre> MangaGenres { get; set; }
|
||||
public DbSet<MangaChapter> MangaChapters { get; set; }
|
||||
public DbSet<ChapterSource> ChapterSources { get; set; }
|
||||
public DbSet<ChapterPage> ChapterPages { get; set; }
|
||||
//public DbSet<MangaChapter> MangaChapters { get; set; }
|
||||
//public DbSet<ChapterSource> ChapterSources { get; set; }
|
||||
//public DbSet<ChapterPage> ChapterPages { get; set; }
|
||||
|
||||
public DbSet<SourceTitle> SourceTitles { get; set; }
|
||||
public DbSet<SourceDescription> SourceDescriptions { get; set; }
|
||||
public DbSet<SourceCover> SourceCovers { get; set; }
|
||||
public DbSet<SourceChapter> SourceChapters { get; set; }
|
||||
public DbSet<SourcePage> SourcePages { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -24,15 +31,21 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
|
||||
ConfigureManga(modelBuilder);
|
||||
ConfigureMangaCover(modelBuilder);
|
||||
ConfigureMangaTitle(modelBuilder);
|
||||
ConfigureMangaDescription(modelBuilder);
|
||||
ConfigureSource(modelBuilder);
|
||||
ConfigureMangaSource(modelBuilder);
|
||||
ConfigureContributor(modelBuilder);
|
||||
ConfigureMangaContributor(modelBuilder);
|
||||
ConfigureGenre(modelBuilder);
|
||||
ConfigureMangaGenre(modelBuilder);
|
||||
ConfigureMangaChapter(modelBuilder);
|
||||
ConfigureChapterSource(modelBuilder);
|
||||
ConfigureChapterPage(modelBuilder);
|
||||
//ConfigureMangaChapter(modelBuilder);
|
||||
//ConfigureChapterSource(modelBuilder);
|
||||
//ConfigureChapterPage(modelBuilder);
|
||||
ConfigureSourceTitle(modelBuilder);
|
||||
ConfigureSourceDescription(modelBuilder);
|
||||
ConfigureMangaSourceCover(modelBuilder);
|
||||
ConfigureMangaSourceChapter(modelBuilder);
|
||||
ConfigureSourcePage(modelBuilder);
|
||||
}
|
||||
|
||||
private static void ConfigureManga(ModelBuilder modelBuilder)
|
||||
@@ -41,9 +54,9 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
|
||||
.Entity<Manga>()
|
||||
.HasKey(x => x.MangaId);
|
||||
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasIndex(x => x.Title)
|
||||
.IsUnique();
|
||||
//modelBuilder.Entity<Manga>()
|
||||
// .HasIndex(x => x.Title)
|
||||
// .IsUnique();
|
||||
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasIndex(x => x.Slug)
|
||||
@@ -81,12 +94,20 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
|
||||
.HasKey(mangaTitle => mangaTitle.MangaTitleId);
|
||||
|
||||
modelBuilder.Entity<MangaTitle>()
|
||||
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.TitleEntry })
|
||||
.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();
|
||||
|
||||
modelBuilder
|
||||
.Entity<MangaTitle>()
|
||||
.HasIndex(mangaTitle => mangaTitle.TitleEntry);
|
||||
.HasIndex(mangaTitle => mangaTitle.Name);
|
||||
|
||||
modelBuilder
|
||||
.Entity<MangaTitle>()
|
||||
@@ -96,6 +117,36 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
private static void ConfigureMangaDescription(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<MangaDescription>()
|
||||
.HasKey(mangaDescription => mangaDescription.MangaDescriptionId);
|
||||
|
||||
modelBuilder.Entity<MangaDescription>()
|
||||
.Property(mangaDescription => mangaDescription.Text)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<MangaDescription>()
|
||||
.Property(mangaDescription => mangaDescription.Language)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<MangaDescription>()
|
||||
.HasIndex(mangaDescription => new { mangaDescription.MangaId, mangaDescription.Text, mangaDescription.Language })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder
|
||||
.Entity<MangaDescription>()
|
||||
.HasIndex(mangaDescription => mangaDescription.Text);
|
||||
|
||||
modelBuilder
|
||||
.Entity<MangaDescription>()
|
||||
.HasOne(x => x.Manga)
|
||||
.WithMany(x => x.Descriptions)
|
||||
.HasForeignKey(x => x.MangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
private static void ConfigureSource(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
@@ -112,7 +163,11 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<MangaSource>()
|
||||
.HasKey(mangaSource => new { mangaSource.MangaId, mangaSource.SourceId });
|
||||
.HasKey(mangaSource => mangaSource.MangaSourceId);
|
||||
|
||||
//modelBuilder
|
||||
// .Entity<MangaSource>()
|
||||
// .HasKey(mangaSource => new { mangaSource.MangaId, mangaSource.SourceId });
|
||||
|
||||
modelBuilder.Entity<MangaSource>()
|
||||
.HasIndex(x => x.Url)
|
||||
@@ -178,50 +233,177 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
private static void ConfigureMangaChapter(ModelBuilder modelBuilder)
|
||||
//private static void ConfigureMangaChapter(ModelBuilder modelBuilder)
|
||||
//{
|
||||
// modelBuilder
|
||||
// .Entity<MangaChapter>()
|
||||
// .HasKey(x => x.MangaChapterId);
|
||||
|
||||
// modelBuilder
|
||||
// .Entity<MangaChapter>()
|
||||
// .HasOne(x => x.Manga)
|
||||
// .WithMany(x => x.Chapters)
|
||||
// .HasForeignKey(x => x.MangaId)
|
||||
// .OnDelete(DeleteBehavior.Cascade);
|
||||
//}
|
||||
|
||||
//private static void ConfigureChapterSource(ModelBuilder modelBuilder)
|
||||
//{
|
||||
// modelBuilder
|
||||
// .Entity<ChapterSource>()
|
||||
// .HasKey(chapterSource => new { chapterSource.MangaChapterId, chapterSource.SourceId });
|
||||
|
||||
// modelBuilder
|
||||
// .Entity<ChapterSource>()
|
||||
// .HasOne(x => x.Chapter)
|
||||
// .WithMany(x => x.Sources)
|
||||
// .HasForeignKey(x => x.MangaChapterId)
|
||||
// .OnDelete(DeleteBehavior.Cascade);
|
||||
//}
|
||||
|
||||
//private static void ConfigureChapterPage(ModelBuilder modelBuilder)
|
||||
//{
|
||||
// modelBuilder
|
||||
// .Entity<ChapterPage>()
|
||||
// .HasKey(chapterPage => chapterPage.ChapterPageId);
|
||||
|
||||
// modelBuilder
|
||||
// .Entity<ChapterPage>()
|
||||
// .HasIndex(chapterPage => new { chapterPage.MangaChapterId, chapterPage.PageNumber })
|
||||
// .IsUnique(true);
|
||||
|
||||
// modelBuilder
|
||||
// .Entity<ChapterPage>()
|
||||
// .HasOne(x => x.MangaChapter)
|
||||
// .WithMany(x => x.Pages)
|
||||
// .HasForeignKey(x => x.MangaChapterId)
|
||||
// .OnDelete(DeleteBehavior.Cascade);
|
||||
//}
|
||||
|
||||
private static void ConfigureSourceTitle(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<MangaChapter>()
|
||||
.HasKey(x => x.MangaChapterId);
|
||||
.Entity<SourceTitle>()
|
||||
.HasKey(x => x.SourceTitleId);
|
||||
|
||||
modelBuilder
|
||||
.Entity<MangaChapter>()
|
||||
.HasOne(x => x.Manga)
|
||||
.WithMany(x => x.Chapters)
|
||||
.HasForeignKey(x => x.MangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.Entity<SourceTitle>()
|
||||
.Property(x => x.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(512);
|
||||
|
||||
// Avoid duplicate rows coming from the same source record
|
||||
modelBuilder
|
||||
.Entity<SourceTitle>()
|
||||
.HasIndex(x => new { x.MangaSourceId, x.Language, x.Name })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourceTitle>()
|
||||
.HasOne(x => x.MangaSource)
|
||||
.WithMany(x => x.Titles)
|
||||
.HasForeignKey(x => x.MangaSourceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
private static void ConfigureChapterSource(ModelBuilder modelBuilder)
|
||||
private static void ConfigureSourceDescription(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<ChapterSource>()
|
||||
.HasKey(chapterSource => new { chapterSource.MangaChapterId, chapterSource.SourceId });
|
||||
.Entity<SourceDescription>()
|
||||
.HasKey(x => x.SourceDescriptionId);
|
||||
|
||||
modelBuilder
|
||||
.Entity<ChapterSource>()
|
||||
.HasOne(x => x.Chapter)
|
||||
.WithMany(x => x.Sources)
|
||||
.HasForeignKey(x => x.MangaChapterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.Entity<SourceDescription>()
|
||||
.Property(x => x.Text)
|
||||
.IsRequired();
|
||||
|
||||
// If sources can emit multiple descriptions per language, keep it non-unique.
|
||||
// If not, uncomment:
|
||||
//modelBuilder
|
||||
// .Entity<SourceDescription>()
|
||||
// .HasIndex(x => new { x.MangaSourceId, x.Language })
|
||||
// .IsUnique();
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourceDescription>()
|
||||
.HasOne(x => x.MangaSource)
|
||||
.WithMany(x => x.Descriptions)
|
||||
.HasForeignKey(x => x.MangaSourceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
private static void ConfigureChapterPage(ModelBuilder modelBuilder)
|
||||
private static void ConfigureMangaSourceCover(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<ChapterPage>()
|
||||
.HasKey(chapterPage => chapterPage.ChapterPageId);
|
||||
.Entity<SourceCover>()
|
||||
.HasKey(sourceCover => sourceCover.SourceCoverId);
|
||||
|
||||
modelBuilder
|
||||
.Entity<ChapterPage>()
|
||||
.HasIndex(chapterPage => new { chapterPage.MangaChapterId, chapterPage.PageNumber })
|
||||
.Entity<SourceCover>()
|
||||
.Property(x => x.Url)
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048);
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourceCover>()
|
||||
.HasIndex(sourceCover => new { sourceCover.MangaSourceId, sourceCover.Url })
|
||||
.IsUnique(true);
|
||||
|
||||
modelBuilder
|
||||
.Entity<ChapterPage>()
|
||||
.HasOne(x => x.MangaChapter)
|
||||
.Entity<SourceCover>()
|
||||
.HasOne(x => x.MangaSource)
|
||||
.WithMany(x => x.Covers)
|
||||
.HasForeignKey(x => x.MangaSourceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourceCover>()
|
||||
.HasOne(x => x.MangaCover)
|
||||
.WithMany() // or .WithMany(c => c.SourceCovers) if you add a collection nav on MangaCover
|
||||
.HasForeignKey(x => x.MangaCoverId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Helpful for lookups when you dedupe/file-link after download
|
||||
modelBuilder
|
||||
.Entity<SourceCover>()
|
||||
.HasIndex(x => x.MangaCoverId);
|
||||
}
|
||||
|
||||
private static void ConfigureMangaSourceChapter(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<SourceChapter>()
|
||||
.HasKey(sourceChapter => sourceChapter.SourceChapterId);
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourceChapter>()
|
||||
.HasIndex(sourceChapter => new { sourceChapter.MangaSourceId, sourceChapter.ChapterNumber, sourceChapter.Url })
|
||||
.IsUnique(true);
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourceChapter>()
|
||||
.HasOne(x => x.MangaSource)
|
||||
.WithMany(x => x.Chapters)
|
||||
.HasForeignKey(x => x.MangaSourceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
private static void ConfigureSourcePage(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder
|
||||
.Entity<SourcePage>()
|
||||
.HasKey(sourcePage => sourcePage.SourcePageId);
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourcePage>()
|
||||
.HasIndex(sourcePage => new { sourcePage.SourceChapterId, sourcePage.PageNumber })
|
||||
.IsUnique(true);
|
||||
|
||||
modelBuilder
|
||||
.Entity<SourcePage>()
|
||||
.HasOne(x => x.Chapter)
|
||||
.WithMany(x => x.Pages)
|
||||
.HasForeignKey(x => x.MangaChapterId)
|
||||
.HasForeignKey(x => x.SourceChapterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
using MangaReader.Core.Common;
|
||||
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class MangaContributor
|
||||
{
|
||||
@@ -8,5 +10,5 @@ public class MangaContributor
|
||||
public int ContributorId { get; set; }
|
||||
public required Contributor Contributor { get; set; }
|
||||
|
||||
public MangaContributorRole Role { get; set; }
|
||||
public ContributorRole Role { get; set; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public enum MangaContributorRole
|
||||
{
|
||||
Author,
|
||||
Artist
|
||||
}
|
||||
14
MangaReader.Core/Data/MangaDescription.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using MangaReader.Core.Common;
|
||||
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class MangaDescription
|
||||
{
|
||||
public int MangaDescriptionId { get; set; }
|
||||
|
||||
public int MangaId { get; set; }
|
||||
public required Manga Manga { get; set; }
|
||||
|
||||
public required string Text { get; set; }
|
||||
public required Language Language { get; set; }
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
public class MangaSource
|
||||
{
|
||||
public int MangaSourceId { get; set; }
|
||||
|
||||
public int MangaId { get; set; }
|
||||
public required Manga Manga { get; set; }
|
||||
|
||||
@@ -9,4 +11,9 @@ public class MangaSource
|
||||
public required Source Source { get; set; }
|
||||
|
||||
public required string Url { get; set; }
|
||||
|
||||
public virtual ICollection<SourceTitle> Titles { get; set; } = [];
|
||||
public virtual ICollection<SourceDescription> Descriptions { get; set; } = [];
|
||||
public virtual ICollection<SourceCover> Covers { get; set; } = [];
|
||||
public virtual ICollection<SourceChapter> Chapters { get; set; } = [];
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
using MangaReader.Core.Common;
|
||||
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class MangaTitle
|
||||
{
|
||||
@@ -7,6 +9,7 @@ public class MangaTitle
|
||||
public int MangaId { get; set; }
|
||||
public required Manga Manga { get; set; }
|
||||
|
||||
public required string TitleEntry { get; set; }
|
||||
public TitleType TitleType { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required Language Language { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
7
MangaReader.Core/Data/ScanlationGroup.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class ScanlationGroup
|
||||
{
|
||||
public int ScanlationGroupId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
20
MangaReader.Core/Data/SourceChapter.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class SourceChapter
|
||||
{
|
||||
public int SourceChapterId { get; set; }
|
||||
|
||||
public int MangaSourceId { get; set; }
|
||||
public required MangaSource MangaSource { get; set; }
|
||||
|
||||
public int? ScanlationGroupId { get; set; }
|
||||
public ScanlationGroup? ScanlationGroup { get; set; }
|
||||
|
||||
public required float ChapterNumber { get; set; }
|
||||
public int? VolumeNumber { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
public required string Url { get; set; }
|
||||
|
||||
public ICollection<SourcePage> Pages { get; set; } = [];
|
||||
}
|
||||
14
MangaReader.Core/Data/SourceCover.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class SourceCover
|
||||
{
|
||||
public int SourceCoverId { get; set; }
|
||||
|
||||
public int MangaSourceId { get; set; }
|
||||
public required MangaSource MangaSource { get; set; }
|
||||
|
||||
public required string Url { get; set; }
|
||||
|
||||
public int? MangaCoverId { get; set; }
|
||||
public MangaCover? MangaCover { get; set; }
|
||||
}
|
||||
14
MangaReader.Core/Data/SourceDescription.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using MangaReader.Core.Common;
|
||||
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class SourceDescription
|
||||
{
|
||||
public int SourceDescriptionId { get; set; }
|
||||
|
||||
public int MangaSourceId { get; set; }
|
||||
public required MangaSource MangaSource { get; set; }
|
||||
|
||||
public required string Text { get; set; }
|
||||
public required Language Language { get; set; }
|
||||
}
|
||||
12
MangaReader.Core/Data/SourcePage.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class SourcePage
|
||||
{
|
||||
public int SourcePageId { get; set; }
|
||||
|
||||
public int SourceChapterId { get; set; }
|
||||
public required SourceChapter Chapter { get; set; }
|
||||
|
||||
public int PageNumber { get; set; }
|
||||
public required string Url { get; set; }
|
||||
}
|
||||
15
MangaReader.Core/Data/SourceTitle.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using MangaReader.Core.Common;
|
||||
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public class SourceTitle
|
||||
{
|
||||
public int SourceTitleId { get; set; }
|
||||
|
||||
public int MangaSourceId { get; set; }
|
||||
public required MangaSource MangaSource { get; set; }
|
||||
|
||||
public required string Name { get; set; }
|
||||
public required Language Language { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace MangaReader.Core.Data;
|
||||
|
||||
public enum TitleType
|
||||
{
|
||||
Primary,
|
||||
OfficialTranslation,
|
||||
FanTranslation,
|
||||
Synonym,
|
||||
Abbreviation,
|
||||
Romaji,
|
||||
Japanese
|
||||
}
|
||||
//public enum TitleType
|
||||
//{
|
||||
// Primary,
|
||||
// OfficialTranslation,
|
||||
// FanTranslation,
|
||||
// Synonym,
|
||||
// Abbreviation,
|
||||
// Romaji,
|
||||
// Japanese
|
||||
//}
|
||||
@@ -1,6 +1,15 @@
|
||||
using MangaReader.Core.Search;
|
||||
using MangaReader.Core.Data;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Pipeline;
|
||||
using MangaReader.Core.Search;
|
||||
using MangaReader.Core.Sources.MangaDex.Api;
|
||||
using MangaReader.Core.Sources.MangaDex.Metadata;
|
||||
using MangaReader.Core.Sources.MangaDex.Search;
|
||||
using MangaReader.Core.Sources.NatoManga.Api;
|
||||
using MangaReader.Core.Sources.NatoManga.Metadata;
|
||||
using MangaReader.Core.Sources.NatoManga.Search;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
#pragma warning disable IDE0130 // Namespace does not match folder structure
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
@@ -8,11 +17,52 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddMangaReader(this IServiceCollection services)
|
||||
public static IServiceCollection AddMangaReader(this IServiceCollection services, Action<DbContextOptionsBuilder>? optionsAction = null)
|
||||
{
|
||||
services.AddScoped<IMangaSearchProvider, NatoMangaSearchProvider>();
|
||||
// Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
|
||||
services.AddHttpClient<IHttpService, HttpService>(client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
|
||||
});
|
||||
|
||||
// Http
|
||||
services.AddScoped<IHttpService, HttpService>();
|
||||
services.AddScoped<IHtmlLoader, HtmlLoader>();
|
||||
|
||||
// NatoManga
|
||||
//services.AddScoped<INatoMangaClient, NatoMangaClient>();
|
||||
//services.AddScoped<IMangaSearchProvider, NatoMangaSearchProvider>();
|
||||
//services.AddScoped<IMangaMetadataProvider, NatoMangaWebCrawler>();
|
||||
|
||||
// MangaDex
|
||||
services.AddScoped<IMangaDexClient, MangaDexClient>();
|
||||
services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>();
|
||||
//services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>();
|
||||
services.AddKeyedScoped<IMangaMetadataProvider, MangaDexMetadataProvider>("MangaDex");
|
||||
|
||||
services.AddScoped<IMangaSearchCoordinator, MangaSearchCoordinator>();
|
||||
services.AddScoped<IMangaMetadataCoordinator, MangaMetadataCoordinator>();
|
||||
|
||||
services.AddScoped<IMangaPipeline, MangaPipeline>();
|
||||
|
||||
// Database
|
||||
services.AddDbContext<MangaContext>(options =>
|
||||
{
|
||||
if (optionsAction is not null)
|
||||
{
|
||||
optionsAction(options);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"MangaReader",
|
||||
"manga.db");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
||||
options.UseSqlite($"Data Source={dbPath}");
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
16
MangaReader.Core/Http/HtmlLoader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
25
MangaReader.Core/Http/HttpService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace MangaReader.Core.Http;
|
||||
|
||||
public class HttpService(HttpClient httpClient) : IHttpService
|
||||
{
|
||||
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken)
|
||||
=> GetStringAsync(url, new Dictionary<string, string>(), cancellationToken);
|
||||
|
||||
public async Task<string> GetStringAsync(string url, IDictionary<string, string> headers, CancellationToken cancellationToken)
|
||||
{
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, url);
|
||||
|
||||
foreach (KeyValuePair<string, string> header in headers)
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
//httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("MangaReader/1.0");
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
|
||||
|
||||
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
8
MangaReader.Core/Http/IHtmlLoader.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace MangaReader.Core.Http;
|
||||
|
||||
public interface IHtmlLoader
|
||||
{
|
||||
Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken);
|
||||
}
|
||||
7
MangaReader.Core/Http/IHttpService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MangaReader.Core.Http;
|
||||
|
||||
public interface IHttpService
|
||||
{
|
||||
Task<string> GetStringAsync(string url, CancellationToken cancellationToken);
|
||||
Task<string> GetStringAsync(string url, IDictionary<string, string> headers, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace MangaReader.Core.HttpService;
|
||||
|
||||
public class HttpService(HttpClient httpClient) : IHttpService
|
||||
{
|
||||
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken)
|
||||
=> httpClient.GetStringAsync(url, cancellationToken);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace MangaReader.Core.HttpService;
|
||||
|
||||
public interface IHttpService
|
||||
{
|
||||
Task<string> GetStringAsync(string url, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -7,8 +7,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
6
MangaReader.Core/Metadata/IMangaMetadataCoordinator.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public interface IMangaMetadataCoordinator
|
||||
{
|
||||
IMangaMetadataProvider GetProvider(string sourceName);
|
||||
}
|
||||
11
MangaReader.Core/Metadata/MangaMetadataCoordinator.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public class MangaMetadataCoordinator(IServiceProvider serviceProvider) : IMangaMetadataCoordinator
|
||||
{
|
||||
public IMangaMetadataProvider GetProvider(string sourceName)
|
||||
{
|
||||
return serviceProvider.GetRequiredKeyedService<IMangaMetadataProvider>(sourceName);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,17 @@
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace MangaReader.Core.Metadata;
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public abstract class MangaWebCrawler : IMangaMetadataProvider
|
||||
{
|
||||
public abstract string SourceId { get; }
|
||||
public abstract Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken);
|
||||
|
||||
protected virtual async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
HtmlWeb web = new()
|
||||
{
|
||||
UsingCacheIfExists = false
|
||||
};
|
||||
//protected virtual async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
|
||||
//{
|
||||
// HtmlWeb web = new()
|
||||
// {
|
||||
// UsingCacheIfExists = false
|
||||
// };
|
||||
|
||||
//return web.Load(url);
|
||||
|
||||
return await web.LoadFromWebAsync(url, cancellationToken);
|
||||
}
|
||||
// return await web.LoadFromWebAsync(url, cancellationToken);
|
||||
//}
|
||||
}
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
public class SourceManga
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public List<SourceMangaTitle> AlternateTitles { get; set; } = [];
|
||||
public required SourceMangaTitle Title { get; set; }
|
||||
public SourceMangaDescription[] Descriptions { get; set; } = [];
|
||||
public SourceMangaTitle[] AlternateTitles { get; set; } = [];
|
||||
public SourceMangaContributor[] Contributors { get; set; } = [];
|
||||
public MangaStatus Status { get; set; } = MangaStatus.Unknown;
|
||||
public List<string> Genres { get; set; } = [];
|
||||
public string[] Genres { get; set; } = [];
|
||||
public DateTime? UpdateDate { get; set; }
|
||||
public long? Views { get; set; }
|
||||
public float? RatingPercent { get; set; }
|
||||
public int? Votes { get; set; }
|
||||
public List<SourceMangaChapter> Chapters { get; set; } = [];
|
||||
public SourceMangaChapter[] Chapters { get; set; } = [];
|
||||
public string[] CoverArtUrls { get; set; } = [];
|
||||
public int? Year { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Common;
|
||||
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public class SourceMangaContributor
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public SourceMangaContributorRole Role { get; set; }
|
||||
public ContributorRole Role { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public enum SourceMangaContributorRole
|
||||
{
|
||||
Unknown,
|
||||
Author,
|
||||
Artist
|
||||
}
|
||||
9
MangaReader.Core/Metadata/SourceMangaDescription.cs
Normal 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; }
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public enum SourceMangaLanguage
|
||||
{
|
||||
Unknown,
|
||||
Japanese,
|
||||
Romanji,
|
||||
English
|
||||
}
|
||||
//public enum SourceMangaLanguage
|
||||
//{
|
||||
// Unknown,
|
||||
// Japanese,
|
||||
// Romanji,
|
||||
// English
|
||||
//}
|
||||
@@ -1,7 +1,9 @@
|
||||
namespace MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Common;
|
||||
|
||||
namespace MangaReader.Core.Metadata;
|
||||
|
||||
public class SourceMangaTitle
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public SourceMangaLanguage Language { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public Language Language { get; set; }
|
||||
}
|
||||
8
MangaReader.Core/Pages/IMangaPageProvider.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using MangaReader.Core.Sources;
|
||||
|
||||
namespace MangaReader.Core.Pages;
|
||||
|
||||
public interface IMangaPageProvider : IMangaSourceComponent
|
||||
{
|
||||
Task<IReadOnlyList<string>> GetPageImageUrlsAsync(string chapterUrl, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using MangaReader.Core.Metadata;
|
||||
|
||||
namespace MangaReader.Core.Pipeline;
|
||||
namespace MangaReader.Core.Pipeline;
|
||||
|
||||
public interface IMangaPipeline
|
||||
{
|
||||
Task RunAsync(SourceManga mangaDto);
|
||||
Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken);
|
||||
Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
11
MangaReader.Core/Pipeline/MangaMetadataPipelineRequest.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using MangaReader.Core.Metadata;
|
||||
|
||||
namespace MangaReader.Core.Pipeline;
|
||||
|
||||
public class MangaMetadataPipelineRequest
|
||||
{
|
||||
public int? MangaId { get; init; }
|
||||
public required string SourceName { get; init; }
|
||||
public required string SourceUrl { get; init; }
|
||||
public required SourceManga SourceManga { get; init; }
|
||||
}
|
||||
7
MangaReader.Core/Pipeline/MangaPagePipelineRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MangaReader.Core.Pipeline;
|
||||
|
||||
public class MangaPagePipelineRequest
|
||||
{
|
||||
public required int SourceChapterId { get; init; }
|
||||
public required IReadOnlyList<string> PageImageUrls { get; init; }
|
||||
}
|
||||
@@ -1,19 +1,45 @@
|
||||
using MangaReader.Core.Data;
|
||||
using MangaReader.Core.Metadata;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MangaReader.Core.Pipeline;
|
||||
|
||||
public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
{
|
||||
public async Task RunAsync(SourceManga sourceManga)
|
||||
enum TitleType
|
||||
{
|
||||
Manga manga = await GetOrAddMangaAsync(sourceManga);
|
||||
Primary,
|
||||
Secondary
|
||||
}
|
||||
|
||||
public async Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
string sourceName = request.SourceName;
|
||||
string sourceUrl = request.SourceUrl;
|
||||
SourceManga sourceManga = request.SourceManga;
|
||||
|
||||
Source source = await GetOrAddSourceAsync(sourceName);
|
||||
Manga manga = await GetOrAddMangaAsync(sourceManga, sourceUrl);
|
||||
MangaSource mangaSource = await AddMangaSourceAsync(sourceUrl, manga, source);
|
||||
|
||||
await AddSourceTitleAsync(mangaSource, sourceManga.Title, TitleType.Primary);
|
||||
await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary);
|
||||
|
||||
foreach (SourceMangaDescription description in sourceManga.Descriptions)
|
||||
{
|
||||
await AddSourceDescriptionAsync(mangaSource, description);
|
||||
await AddDescriptionAsync(manga, description);
|
||||
}
|
||||
|
||||
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
|
||||
{
|
||||
await AddTitleAsync(manga, alternateTitle);
|
||||
await AddSourceTitleAsync(mangaSource, alternateTitle, TitleType.Secondary);
|
||||
await AddTitleAsync(manga, alternateTitle, TitleType.Secondary);
|
||||
}
|
||||
|
||||
foreach (string genre in sourceManga.Genres)
|
||||
@@ -21,25 +47,58 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
await LinkGenreAsync(manga, genre);
|
||||
}
|
||||
|
||||
foreach (SourceMangaContributor contributor in sourceManga.Contributors)
|
||||
{
|
||||
await LinkMangaContributorAsync(manga, contributor);
|
||||
}
|
||||
|
||||
foreach (SourceMangaChapter chapter in sourceManga.Chapters)
|
||||
{
|
||||
await AddChapterAsync(manga, chapter);
|
||||
await AddChapterAsync(mangaSource, chapter);
|
||||
}
|
||||
|
||||
foreach (string coverArtUrl in sourceManga.CoverArtUrls)
|
||||
{
|
||||
await AddCoverAsync(mangaSource, coverArtUrl);
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
private async Task<Manga> GetOrAddMangaAsync(SourceManga sourceManga)
|
||||
private async Task<Source> GetOrAddSourceAsync(string sourceName)
|
||||
{
|
||||
Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga => manga.Title == sourceManga.Title);
|
||||
Source? source = await context.Sources.FirstOrDefaultAsync(s => s.Name == sourceName);
|
||||
|
||||
if (source != null)
|
||||
return source;
|
||||
|
||||
source = new()
|
||||
{
|
||||
Name = sourceName
|
||||
};
|
||||
|
||||
context.Sources.Add(source);
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private async Task<Manga> GetOrAddMangaAsync(SourceManga sourceManga, string sourceUrl)
|
||||
{
|
||||
//Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga =>
|
||||
// manga.Titles.Any(mangaTitle => mangaTitle.Name == sourceManga.Title.Name));
|
||||
|
||||
Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga =>
|
||||
manga.Sources.Any(mangaSource => mangaSource.Url == sourceUrl));
|
||||
|
||||
if (manga != null)
|
||||
{
|
||||
manga.Year = sourceManga.Year ?? manga.Year;
|
||||
return manga;
|
||||
}
|
||||
|
||||
manga = new()
|
||||
{
|
||||
Title = sourceManga.Title,
|
||||
Slug = GenerateSlug(sourceManga.Title),
|
||||
Slug = GenerateSlug(sourceManga.Title.Name),
|
||||
};
|
||||
|
||||
context.Add(manga);
|
||||
@@ -62,10 +121,73 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
[GeneratedRegex(@"\s+")]
|
||||
private static partial Regex RemoveSpacesWithDashRegex();
|
||||
|
||||
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle)
|
||||
private async Task<MangaSource> AddMangaSourceAsync(string sourceUrl, Manga manga, Source source)
|
||||
{
|
||||
MangaSource? mangaSource = await context.MangaSources.FirstOrDefaultAsync(ms =>
|
||||
ms.Manga == manga && ms.Source == source && ms.Url == sourceUrl);
|
||||
|
||||
if (mangaSource != null)
|
||||
return mangaSource;
|
||||
|
||||
mangaSource = new()
|
||||
{
|
||||
Manga = manga,
|
||||
Source = source,
|
||||
Url = sourceUrl
|
||||
};
|
||||
|
||||
context.MangaSources.Add(mangaSource);
|
||||
|
||||
return mangaSource;
|
||||
}
|
||||
|
||||
private async Task AddSourceTitleAsync(MangaSource mangaSource, SourceMangaTitle sourceMangaTitle, TitleType titleType)
|
||||
{
|
||||
SourceTitle? sourceTitle = await context.SourceTitles.FirstOrDefaultAsync(mt =>
|
||||
mt.MangaSource == mangaSource && mt.Name == sourceMangaTitle.Name);
|
||||
|
||||
if (sourceTitle != null)
|
||||
return;
|
||||
|
||||
sourceTitle = new()
|
||||
{
|
||||
MangaSource = mangaSource,
|
||||
Name = sourceMangaTitle.Name,
|
||||
Language = sourceMangaTitle.Language,
|
||||
IsPrimary = titleType == TitleType.Primary
|
||||
};
|
||||
|
||||
context.SourceTitles.Add(sourceTitle);
|
||||
}
|
||||
|
||||
private async Task AddSourceDescriptionAsync(MangaSource mangaSource, SourceMangaDescription? sourceMangaDescription)
|
||||
{
|
||||
if (sourceMangaDescription == null)
|
||||
return;
|
||||
|
||||
SourceDescription? sourceDescription = await context.SourceDescriptions.FirstOrDefaultAsync(md =>
|
||||
md.MangaSource == mangaSource && md.Language == sourceMangaDescription.Language);
|
||||
|
||||
if (sourceDescription != null)
|
||||
{
|
||||
sourceDescription.Text = sourceMangaDescription.Name;
|
||||
return;
|
||||
}
|
||||
|
||||
sourceDescription = new()
|
||||
{
|
||||
MangaSource = mangaSource,
|
||||
Text = sourceMangaDescription.Name,
|
||||
Language = sourceMangaDescription.Language
|
||||
};
|
||||
|
||||
context.SourceDescriptions.Add(sourceDescription);
|
||||
}
|
||||
|
||||
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle, TitleType titleType)
|
||||
{
|
||||
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt =>
|
||||
mt.Manga == manga && mt.TitleEntry == sourceMangaTitle.Title);
|
||||
mt.Manga == manga && mt.Name == sourceMangaTitle.Name);
|
||||
|
||||
if (mangaTitle != null)
|
||||
return;
|
||||
@@ -73,12 +195,38 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
mangaTitle = new()
|
||||
{
|
||||
Manga = manga,
|
||||
TitleEntry = sourceMangaTitle.Title,
|
||||
Name = sourceMangaTitle.Name,
|
||||
Language = sourceMangaTitle.Language,
|
||||
IsPrimary = titleType == TitleType.Primary
|
||||
};
|
||||
|
||||
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.Language == sourceMangaDescription.Language);
|
||||
|
||||
if (mangaDescription != null)
|
||||
{
|
||||
mangaDescription.Text = sourceMangaDescription.Name;
|
||||
return;
|
||||
}
|
||||
|
||||
mangaDescription = new()
|
||||
{
|
||||
Manga = manga,
|
||||
Text = sourceMangaDescription.Name,
|
||||
Language = sourceMangaDescription.Language
|
||||
};
|
||||
|
||||
context.MangaDescriptions.Add(mangaDescription);
|
||||
}
|
||||
|
||||
private async Task LinkGenreAsync(Manga manga, string genreName)
|
||||
{
|
||||
Genre genre = await GetOrAddGenreAsync(genreName);
|
||||
@@ -114,32 +262,233 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
|
||||
return genre;
|
||||
}
|
||||
|
||||
private async Task AddChapterAsync(Manga manga, SourceMangaChapter sourceeMangaChapter)
|
||||
private async Task LinkMangaContributorAsync(Manga manga, SourceMangaContributor sourceMangaContributor)
|
||||
{
|
||||
MangaChapter mangaChapter = await context.MangaChapters.FirstOrDefaultAsync(x => x.ChapterNumber == sourceeMangaChapter.Number)
|
||||
?? AddMangaChapter(manga, sourceeMangaChapter);
|
||||
Contributor contributor = await GetOrAddContributorAsync(sourceMangaContributor.Name);
|
||||
|
||||
if (mangaChapter.VolumeNumber is null && sourceeMangaChapter.Volume is not null)
|
||||
{
|
||||
mangaChapter.VolumeNumber = sourceeMangaChapter.Volume;
|
||||
}
|
||||
MangaContributor? mangaContributor = await context.MangaContributors.FirstOrDefaultAsync(x =>
|
||||
x.Manga == manga && x.Contributor == contributor && x.Role == sourceMangaContributor.Role);
|
||||
|
||||
if (mangaChapter.Title is null && sourceeMangaChapter.Title is not null)
|
||||
{
|
||||
mangaChapter.Title = sourceeMangaChapter.Title;
|
||||
}
|
||||
}
|
||||
if (mangaContributor != null)
|
||||
return;
|
||||
|
||||
private MangaChapter AddMangaChapter(Manga manga, SourceMangaChapter sourceeMangaChapter)
|
||||
{
|
||||
MangaChapter mangaChapter = new()
|
||||
mangaContributor = new()
|
||||
{
|
||||
Manga = manga,
|
||||
ChapterNumber = sourceeMangaChapter.Number
|
||||
Contributor = contributor,
|
||||
Role = sourceMangaContributor.Role
|
||||
};
|
||||
|
||||
context.MangaChapters.Add(mangaChapter);
|
||||
|
||||
return mangaChapter;
|
||||
context.MangaContributors.Add(mangaContributor);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Contributor> GetOrAddContributorAsync(string contributorName)
|
||||
{
|
||||
Contributor? trackedContributor = context.ChangeTracker
|
||||
.Entries<Contributor>()
|
||||
.Select(e => e.Entity)
|
||||
.FirstOrDefault(c => c.Name == contributorName);
|
||||
|
||||
if (trackedContributor is not null)
|
||||
return trackedContributor;
|
||||
|
||||
Contributor? contributor = await context.Contributors.FirstOrDefaultAsync(x => x.Name == contributorName);
|
||||
|
||||
if (contributor == null)
|
||||
{
|
||||
contributor = new()
|
||||
{
|
||||
Name = contributorName,
|
||||
};
|
||||
|
||||
await context.Contributors.AddAsync(contributor);
|
||||
}
|
||||
|
||||
return contributor;
|
||||
}
|
||||
|
||||
private async Task AddChapterAsync(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter)
|
||||
{
|
||||
SourceChapter sourceChapter = await GetSourceChapter(mangaSource, sourceMangaChapter)
|
||||
?? AddSourceChapter(mangaSource, sourceMangaChapter);
|
||||
|
||||
if (sourceChapter.VolumeNumber is null && sourceMangaChapter.Volume is not null)
|
||||
{
|
||||
sourceChapter.VolumeNumber = sourceMangaChapter.Volume;
|
||||
}
|
||||
|
||||
if (sourceChapter.Title is null && sourceMangaChapter.Title is not null)
|
||||
{
|
||||
sourceChapter.Title = sourceMangaChapter.Title;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SourceChapter?> GetSourceChapter(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter)
|
||||
{
|
||||
return await context.SourceChapters.FirstOrDefaultAsync(x =>
|
||||
x.MangaSource == mangaSource && x.ChapterNumber == sourceMangaChapter.Number);
|
||||
}
|
||||
|
||||
private SourceChapter AddSourceChapter(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter)
|
||||
{
|
||||
SourceChapter sourceChapter = new()
|
||||
{
|
||||
MangaSource = mangaSource,
|
||||
ChapterNumber = sourceMangaChapter.Number,
|
||||
Url = sourceMangaChapter.Url
|
||||
};
|
||||
|
||||
context.SourceChapters.Add(sourceChapter);
|
||||
|
||||
return sourceChapter;
|
||||
}
|
||||
|
||||
private async Task AddCoverAsync(MangaSource mangaSource, string coverArtUrl)
|
||||
{
|
||||
SourceCover? sourceCover = await context.SourceCovers.FirstOrDefaultAsync(x =>
|
||||
x.MangaSource == mangaSource && x.Url == coverArtUrl);
|
||||
|
||||
if (sourceCover == null)
|
||||
{
|
||||
sourceCover = new()
|
||||
{
|
||||
MangaSource = mangaSource,
|
||||
Url = coverArtUrl
|
||||
};
|
||||
|
||||
context.SourceCovers.Add(sourceCover);
|
||||
}
|
||||
}
|
||||
|
||||
//public async Task RunCoversAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken)
|
||||
//{
|
||||
// SourceCover[] sourceCovers = await context.SourceCovers
|
||||
// .Where(x => x.MangaCoverId == null)
|
||||
// .ToArrayAsync(cancellationToken);
|
||||
|
||||
// foreach (SourceCover sourceCover in sourceCovers)
|
||||
// {
|
||||
// await AddOrUpdateCoverAsync(sourceCover, cancellationToken);
|
||||
// }
|
||||
//}
|
||||
|
||||
//private async Task AddOrUpdateCoverAsync(SourceCover sourceCover, CancellationToken cancellationToken)
|
||||
//{
|
||||
// HttpClient client = httpClientFactory.CreateClient();
|
||||
// client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0");
|
||||
|
||||
// using HttpResponseMessage responseMessage = await client.GetAsync(sourceCover.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
// responseMessage.EnsureSuccessStatusCode();
|
||||
|
||||
// await using Stream networkStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken);
|
||||
|
||||
// Image image = await Image.LoadAsync(networkStream, cancellationToken);
|
||||
// networkStream.Position = 0;
|
||||
|
||||
// IImageFormat imageFormat = await Image.DetectFormatAsync(networkStream, cancellationToken);
|
||||
|
||||
// // Normalize output extension (you can keep WEBP if your viewer supports it; WinUI often prefers JPEG/PNG)
|
||||
// string extension = (imageFormat.Name?.ToLowerInvariant()) switch
|
||||
// {
|
||||
// "png" => "png",
|
||||
// _ => "jpg"
|
||||
// };
|
||||
|
||||
// // Save to memory to hash & maybe convert formats
|
||||
// await using MemoryStream memoryStream = new();
|
||||
|
||||
// if (extension == "png")
|
||||
// await image.SaveAsPngAsync(memoryStream, cancellationToken);
|
||||
// else
|
||||
// await image.SaveAsJpegAsync(memoryStream, cancellationToken);
|
||||
|
||||
// memoryStream.Position = 0;
|
||||
|
||||
// // Compute hash for dedupe
|
||||
// string sha256;
|
||||
// using (var sha = SHA256.Create())
|
||||
// sha256 = Convert.ToHexString(sha.ComputeHash(memoryStream)).ToLowerInvariant();
|
||||
// memoryStream.Position = 0;
|
||||
|
||||
// // Check if we already have this exact file
|
||||
// var existing = await context.MangaCovers.FirstOrDefaultAsync(c => c.Sha256 == sha256 && c.MangaId == sc.MangaSource.MangaId, ct);
|
||||
// var existing2 = await context.MangaCovers.FirstOrDefaultAsync(c => c. == sha256 && c.MangaId == sc.MangaSource.MangaId, ct);
|
||||
|
||||
// MangaCover cover;
|
||||
|
||||
// if (existing is not null)
|
||||
// {
|
||||
// cover = existing;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // Choose file path
|
||||
// var folder = _paths.GetMangaCoverFolder(sc.MangaSource.MangaId);
|
||||
// var guid = Guid.NewGuid();
|
||||
// var fileName = $"{guid}.{ext}";
|
||||
// var fullPath = Path.Combine(folder, fileName);
|
||||
|
||||
// // Write to disk
|
||||
// await using (var fs = File.Create(fullPath))
|
||||
// await ms.CopyToAsync(fs, ct);
|
||||
|
||||
// // Create DB record
|
||||
// cover = new MangaCover
|
||||
// {
|
||||
// Manga = sc.MangaSource.Manga,
|
||||
// Guid = guid,
|
||||
// FileExtension = ext,
|
||||
// Sha256 = sha256,
|
||||
// Width = image.Width,
|
||||
// Height = image.Height,
|
||||
// IsPrimary = false
|
||||
// };
|
||||
|
||||
// context.MangaCovers.Add(cover);
|
||||
// }
|
||||
|
||||
// // Link and mark done
|
||||
// sourceCover.MangaCover = cover;
|
||||
// //sc.Status = CoverDownloadStatus.Done;
|
||||
// //sc.Error = null;
|
||||
|
||||
// await context.SaveChangesAsync(cancellationToken);
|
||||
//}
|
||||
|
||||
public async Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId, cancellationToken);
|
||||
|
||||
if (sourceChapter == null)
|
||||
return;
|
||||
|
||||
int currentPageNumber = 1;
|
||||
|
||||
foreach (string pageImageUrl in request.PageImageUrls)
|
||||
{
|
||||
await AddOrUpdateSourcePageAsync(sourceChapter, currentPageNumber++, pageImageUrl, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddOrUpdateSourcePageAsync(SourceChapter sourceChapter, int pageNumber, string pageImageUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
SourcePage? sourcePage = await context.SourcePages.FirstOrDefaultAsync(x =>
|
||||
x.Chapter == sourceChapter && x.PageNumber == pageNumber, cancellationToken);
|
||||
|
||||
if (sourcePage == null)
|
||||
{
|
||||
sourcePage = new()
|
||||
{
|
||||
Chapter = sourceChapter,
|
||||
PageNumber = pageNumber,
|
||||
Url = pageImageUrl
|
||||
};
|
||||
|
||||
await context.SourcePages.AddAsync(sourcePage, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
sourcePage.Url = pageImageUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using MangaReader.Core.HttpService;
|
||||
using MangaReader.Core.Http;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MangaReader.Core.Search;
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
public record MangaSearchResult
|
||||
{
|
||||
public required string Source { get; init; }
|
||||
public required string Url { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string? Thumbnail { get; init; }
|
||||
public string? Author { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string[] Genres { get; init; } = [];
|
||||
}
|
||||
@@ -8,4 +8,6 @@ public interface IMangaDexClient
|
||||
Task<MangaDexResponse?> GetMangaAsync(Guid mangaGuid, CancellationToken cancellationToken);
|
||||
Task<MangaDexResponse?> GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken);
|
||||
Task<MangaDexChapterResponse?> GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken);
|
||||
Task<MangaDexResponse?> GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken);
|
||||
Task<MangaDexResponse?> GetCoverArtAsync(Guid[] mangaGuid, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -6,4 +6,5 @@ public class MangaAttributes
|
||||
public List<Dictionary<string, string>> AltTitles { get; set; } = [];
|
||||
public Dictionary<string, string> Description { get; set; } = [];
|
||||
public List<TagEntity> Tags { get; set; } = [];
|
||||
public int? Year { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using MangaReader.Core.HttpService;
|
||||
using MangaReader.Core.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -20,17 +20,28 @@ namespace MangaReader.Core.Sources.MangaDex.Api
|
||||
}
|
||||
|
||||
private async Task<MangaDexResponse?> GetAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
//string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
//return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions);
|
||||
|
||||
return await GetAsync<MangaDexResponse>(url, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<T?> GetAsync<T>(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions);
|
||||
return JsonSerializer.Deserialize<T?>(response, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> SearchMangaByTitleAsync(string title, CancellationToken cancellationToken)
|
||||
{
|
||||
string normalizedKeyword = GetNormalizedKeyword(title);
|
||||
|
||||
return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5", cancellationToken);
|
||||
//return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5", cancellationToken);
|
||||
return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=10&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&includes[]=cover_art&order[followedCount]=desc&order[relevance]=desc", cancellationToken);
|
||||
//
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> SearchMangaByAuthorAsync(string author, CancellationToken cancellationToken)
|
||||
@@ -64,10 +75,24 @@ namespace MangaReader.Core.Sources.MangaDex.Api
|
||||
|
||||
public async Task<MangaDexChapterResponse?> GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken)
|
||||
{
|
||||
string url = $"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false";
|
||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
//string url = $"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false";
|
||||
//string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<MangaDexChapterResponse>(response, _jsonSerializerOptions);
|
||||
//return JsonSerializer.Deserialize<MangaDexChapterResponse>(response, _jsonSerializerOptions);
|
||||
|
||||
return await GetAsync<MangaDexChapterResponse>($"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken)
|
||||
{
|
||||
return await GetCoverArtAsync([mangaGuid], cancellationToken);
|
||||
//return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuid}&limit=100&offset=0", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<MangaDexResponse?> GetCoverArtAsync(Guid[] mangaGuids, CancellationToken cancellationToken)
|
||||
{
|
||||
string mangaGuidQuery = string.Join("&manga[]=", mangaGuids);
|
||||
return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuidQuery}&limit=100&offset=0", cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Common;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Sources.MangaDex.Api;
|
||||
|
||||
namespace MangaReader.Core.Sources.MangaDex.Metadata;
|
||||
@@ -24,14 +25,17 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
MangaAttributes mangaAttributes = mangaEntity.Attributes;
|
||||
List<MangaDexEntity> mangaRelationships = mangaEntity.Relationships;
|
||||
MangaDexResponse? mangaDexFeedResponse = await mangaDexClient.GetFeedAsync(mangaGuid, cancellationToken);
|
||||
MangaDexResponse? coverArtResponse = await mangaDexClient.GetCoverArtAsync(mangaGuid, cancellationToken);
|
||||
|
||||
return new SourceManga()
|
||||
{
|
||||
Title = GetTitle(mangaAttributes),
|
||||
AlternateTitles = GetAlternateTitles(mangaAttributes),
|
||||
Descriptions = GetDescriptions(mangaAttributes),
|
||||
Genres = GetGenres(mangaAttributes),
|
||||
Contributors = GetContributors(mangaRelationships),
|
||||
Chapters = GetChapters(mangaDexFeedResponse)
|
||||
Chapters = GetChapters(mangaDexFeedResponse),
|
||||
CoverArtUrls = GetCoverArtUrls(mangaGuid, coverArtResponse)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,24 +51,38 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
return mangaGuid;
|
||||
}
|
||||
|
||||
private static string GetTitle(MangaAttributes attributes)
|
||||
private static SourceMangaTitle GetTitle(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.Title.TryGetValue("en", out string? title))
|
||||
return title;
|
||||
(string title, Language langauge) = GetTileAndLanguage(attributes);
|
||||
|
||||
return string.Empty;
|
||||
return new()
|
||||
{
|
||||
Name = title,
|
||||
Language = langauge
|
||||
};
|
||||
}
|
||||
|
||||
private static List<SourceMangaTitle> GetAlternateTitles(MangaAttributes attributes)
|
||||
private static (string title, Language langauge) GetTileAndLanguage(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.Title.TryGetValue("en", out string? englishTitle))
|
||||
return (englishTitle, Language.English);
|
||||
|
||||
if (attributes.Title.TryGetValue("ja-ro", out string? japaneseTitle))
|
||||
return (japaneseTitle, Language.Japanese);
|
||||
|
||||
return (string.Empty, Language.Unknown);
|
||||
}
|
||||
|
||||
private static SourceMangaTitle[] GetAlternateTitles(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.AltTitles == null || attributes.AltTitles.Count == 0)
|
||||
return [];
|
||||
|
||||
Dictionary<string, SourceMangaLanguage> languageIdMap = new()
|
||||
Dictionary<string, Language> languageIdMap = new()
|
||||
{
|
||||
{ "en", SourceMangaLanguage.English },
|
||||
{ "ja", SourceMangaLanguage.Japanese },
|
||||
{ "ja-ro", SourceMangaLanguage.Romanji },
|
||||
{ "en", Language.English },
|
||||
{ "ja", Language.Japanese },
|
||||
{ "ja-ro", Language.Romaji },
|
||||
};
|
||||
|
||||
List<SourceMangaTitle> sourceMangaTitles = [];
|
||||
@@ -73,12 +91,12 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
{
|
||||
foreach (string alternateTitleKey in alternateTitle.Keys)
|
||||
{
|
||||
if (languageIdMap.TryGetValue(alternateTitleKey, out SourceMangaLanguage language) == false)
|
||||
if (languageIdMap.TryGetValue(alternateTitleKey, out Language language) == false)
|
||||
continue;
|
||||
|
||||
SourceMangaTitle sourceMangaTitle = new()
|
||||
{
|
||||
Title = alternateTitle[alternateTitleKey],
|
||||
Name = alternateTitle[alternateTitleKey],
|
||||
Language = language
|
||||
};
|
||||
|
||||
@@ -86,10 +104,41 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
}
|
||||
}
|
||||
|
||||
return sourceMangaTitles;
|
||||
return [.. sourceMangaTitles];
|
||||
}
|
||||
|
||||
private static List<string> GetGenres(MangaAttributes attributes)
|
||||
private static SourceMangaDescription[] GetDescriptions(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.AltTitles == null || attributes.AltTitles.Count == 0)
|
||||
return [];
|
||||
|
||||
Dictionary<string, Language> languageIdMap = new()
|
||||
{
|
||||
{ "en", Language.English },
|
||||
{ "ja", Language.Japanese },
|
||||
{ "ja-ro", Language.Romaji },
|
||||
};
|
||||
|
||||
List<SourceMangaDescription> sourceMangaDescriptions = [];
|
||||
|
||||
foreach (string key in attributes.Description.Keys)
|
||||
{
|
||||
if (languageIdMap.TryGetValue(key, out Language language) == false)
|
||||
continue;
|
||||
|
||||
SourceMangaDescription sourceMangaDescription = new()
|
||||
{
|
||||
Name = attributes.Description[key],
|
||||
Language = language
|
||||
};
|
||||
|
||||
sourceMangaDescriptions.Add(sourceMangaDescription);
|
||||
}
|
||||
|
||||
return [.. sourceMangaDescriptions];
|
||||
}
|
||||
|
||||
private static string[] GetGenres(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.Tags == null || attributes.Tags.Count == 0)
|
||||
return [];
|
||||
@@ -107,7 +156,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
tags.Add(tagEntity.Attributes.Name.FirstOrDefault().Value);
|
||||
}
|
||||
|
||||
return tags;
|
||||
return [.. tags];
|
||||
}
|
||||
|
||||
private static SourceMangaContributor[] GetContributors(List<MangaDexEntity> relationships)
|
||||
@@ -125,7 +174,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
SourceMangaContributor contributor = new()
|
||||
{
|
||||
Name = authorEntity.Attributes.Name,
|
||||
Role = SourceMangaContributorRole.Author
|
||||
Role = ContributorRole.Author
|
||||
};
|
||||
|
||||
contributors.Add(contributor);
|
||||
@@ -139,7 +188,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
SourceMangaContributor contributor = new()
|
||||
{
|
||||
Name = artistEntity.Attributes.Name,
|
||||
Role = SourceMangaContributorRole.Artist
|
||||
Role = ContributorRole.Artist
|
||||
};
|
||||
|
||||
contributors.Add(contributor);
|
||||
@@ -148,7 +197,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
return [.. contributors];
|
||||
}
|
||||
|
||||
private static List<SourceMangaChapter> GetChapters(MangaDexResponse? mangaDexFeedResponse)
|
||||
private static SourceMangaChapter[] GetChapters(MangaDexResponse? mangaDexFeedResponse)
|
||||
{
|
||||
if (mangaDexFeedResponse == null || mangaDexFeedResponse is not MangaDexCollectionResponse collectionResponse)
|
||||
return [];
|
||||
@@ -179,6 +228,27 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
|
||||
chapters.Add(chapter);
|
||||
}
|
||||
|
||||
return chapters;
|
||||
return [.. chapters];
|
||||
}
|
||||
|
||||
private static string[] GetCoverArtUrls(Guid mangaGuid, MangaDexResponse? coverArtResponse)
|
||||
{
|
||||
if (coverArtResponse == null || coverArtResponse is not MangaDexCollectionResponse collectionResponse)
|
||||
return [];
|
||||
|
||||
List<string> coverArtUrls = [];
|
||||
CoverArtEntity[] coverArtEntities = [.. collectionResponse.Data.Where(entity => entity is CoverArtEntity).Cast<CoverArtEntity>()];
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -21,43 +21,109 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
|
||||
if (response == null || (response is not MangaDexCollectionResponse collectionResponse))
|
||||
return [];
|
||||
|
||||
List<MangaSearchResult> mangaSearchResults = [];
|
||||
MangaEntity[] mangaEntities = [.. collectionResponse.Data.Where(entity => entity is MangaEntity).Cast<MangaEntity>()];
|
||||
|
||||
foreach (MangaDexEntity entity in collectionResponse.Data)
|
||||
if (mangaEntities.Length == 0)
|
||||
return [];
|
||||
|
||||
Dictionary<Guid, List<CoverArtEntity>> mangaCoverArtMap = await GetCoverArtFileNamesAsync(mangaEntities, cancellationToken);
|
||||
|
||||
List<MangaSearchResult> mangaSearchResults = [];
|
||||
Dictionary<Guid, MangaSearchResult> thing = [];
|
||||
|
||||
foreach (MangaEntity mangaEntity in mangaEntities)
|
||||
{
|
||||
MangaSearchResult? mangaSearchResult = GetMangaSearchResult(entity);
|
||||
CoverArtEntity[] coverArtEntites = [.. mangaCoverArtMap[mangaEntity.Id]];
|
||||
|
||||
MangaSearchResult? mangaSearchResult = GetMangaSearchResult(mangaEntity, coverArtEntites);
|
||||
|
||||
if (mangaSearchResult == null)
|
||||
continue;
|
||||
|
||||
mangaSearchResults.Add(mangaSearchResult);
|
||||
}
|
||||
|
||||
if (thing.Count > 0)
|
||||
{
|
||||
Guid[] mangaGuids = [.. thing.Select(x => x.Key)];
|
||||
var reults = await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken);
|
||||
//var reults = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken);
|
||||
}
|
||||
|
||||
return [.. mangaSearchResults];
|
||||
}
|
||||
|
||||
private static MangaSearchResult? GetMangaSearchResult(MangaDexEntity entity)
|
||||
private MangaSearchResult? GetMangaSearchResult(MangaEntity mangaEntity, CoverArtEntity[] coverArtEntites)
|
||||
{
|
||||
if (entity is not MangaEntity mangaEntity)
|
||||
MangaAttributes? mangaAttributes = mangaEntity.Attributes;
|
||||
|
||||
if (mangaAttributes == null)
|
||||
return null;
|
||||
|
||||
if (mangaEntity.Attributes == null)
|
||||
return null;
|
||||
|
||||
string title = mangaEntity.Attributes.Title.FirstOrDefault().Value;
|
||||
string title = GetTitle(mangaAttributes);
|
||||
string slug = GenerateSlug(title);
|
||||
|
||||
MangaSearchResult mangaSearchResult = new()
|
||||
{
|
||||
Source = SourceId,
|
||||
Title = title,
|
||||
Description = GetDescription(mangaAttributes),
|
||||
Genres = GetGenres(mangaAttributes),
|
||||
Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}",
|
||||
Thumbnail = GetThumbnail(mangaEntity)
|
||||
Thumbnail = GetThumbnail(mangaEntity, coverArtEntites)
|
||||
};
|
||||
|
||||
return mangaSearchResult;
|
||||
}
|
||||
|
||||
private static string GetTitle(MangaAttributes attributes)
|
||||
{
|
||||
var alternateTitle = attributes.AltTitles.Where(x => x.ContainsKey("en")).FirstOrDefault();
|
||||
|
||||
if (alternateTitle?.Count > 0)
|
||||
return alternateTitle["en"];
|
||||
|
||||
if (attributes.Title.TryGetValue("en", out string? title))
|
||||
return title;
|
||||
|
||||
if (attributes.Title.Count > 0)
|
||||
return attributes.Title.ElementAt(0).Value;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetDescription(MangaAttributes attributes)
|
||||
{
|
||||
if (attributes.Description.TryGetValue("en", out string? description))
|
||||
return description;
|
||||
|
||||
if (attributes.Description.Count > 0)
|
||||
return attributes.Description.ElementAt(0).Value;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static 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];
|
||||
}
|
||||
|
||||
public static string GenerateSlug(string title)
|
||||
{
|
||||
// title.ToLowerInvariant().Normalize(NormalizationForm.FormD);
|
||||
@@ -70,7 +136,18 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
|
||||
return title.Trim('-');
|
||||
}
|
||||
|
||||
private static string? GetThumbnail(MangaDexEntity mangaDexEntity)
|
||||
private static string? GetThumbnail(MangaDexEntity mangaDexEntity, CoverArtEntity[] coverArtEntites)
|
||||
{
|
||||
string? fileName = GetCoverArtFileNameFromMangaEntity(mangaDexEntity)
|
||||
?? GetCoverArtFileNameFromCoverArtEntities(coverArtEntites);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
return null;
|
||||
|
||||
return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}";
|
||||
}
|
||||
|
||||
private static string? GetCoverArtFileNameFromMangaEntity(MangaDexEntity mangaDexEntity)
|
||||
{
|
||||
CoverArtEntity? coverArtEntity = (CoverArtEntity?)mangaDexEntity.Relationships.FirstOrDefault(entity =>
|
||||
entity is CoverArtEntity);
|
||||
@@ -78,11 +155,59 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
|
||||
if (coverArtEntity == null || string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName))
|
||||
return null;
|
||||
|
||||
string? fileName = coverArtEntity.Attributes?.FileName;
|
||||
return coverArtEntity.Attributes?.FileName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName))
|
||||
return null;
|
||||
private static string? GetCoverArtFileNameFromCoverArtEntities(CoverArtEntity[] coverArtEntites)
|
||||
{
|
||||
return coverArtEntites.Where(coverArtEntity =>
|
||||
string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName) == false).FirstOrDefault()?.Attributes!.FileName;
|
||||
}
|
||||
|
||||
return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}";
|
||||
private async Task<Dictionary<Guid, List<CoverArtEntity>>> GetCoverArtFileNamesAsync(MangaEntity[] mangaEntities, CancellationToken cancellationToken)
|
||||
{
|
||||
Guid[] mangaGuids = [.. mangaEntities.Select(entity => entity.Id)];
|
||||
|
||||
return await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<Guid, List<CoverArtEntity>>> GetCoverArtFileNamesAsync(Guid[] mangaGuids, CancellationToken cancellationToken)
|
||||
{
|
||||
Dictionary<Guid, List<CoverArtEntity>> result = [];
|
||||
|
||||
foreach (Guid mangaGuid in mangaGuids)
|
||||
{
|
||||
result.Add(mangaGuid, []);
|
||||
}
|
||||
|
||||
MangaDexResponse? response = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken);
|
||||
|
||||
if (response == null || (response is not MangaDexCollectionResponse collectionResponse))
|
||||
return result;
|
||||
|
||||
CoverArtEntity[] coverArtEntities = [.. collectionResponse.Data.Where(entity => entity is CoverArtEntity).Cast<CoverArtEntity>()];
|
||||
|
||||
if (coverArtEntities.Length == 0)
|
||||
return result;
|
||||
|
||||
CoverArtEntity[] orderedCoverArtEntities = [.. coverArtEntities.OrderBy(x => x.Attributes?.Volume)];
|
||||
|
||||
foreach (var coverArtEntity in orderedCoverArtEntities)
|
||||
{
|
||||
if (coverArtEntity.Attributes == null)
|
||||
continue;
|
||||
|
||||
MangaEntity? mangaEntity = (MangaEntity?)coverArtEntity.Relationships.FirstOrDefault(relationship => relationship is MangaEntity);
|
||||
|
||||
if (mangaEntity == null)
|
||||
continue;
|
||||
|
||||
if (result.ContainsKey(mangaEntity.Id) == false)
|
||||
continue;
|
||||
|
||||
result[mangaEntity.Id].Add(coverArtEntity);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
using HtmlAgilityPack;
|
||||
using MangaReader.Core.Common;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Metadata;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
|
||||
namespace MangaReader.Core.Sources.MangaNato.Metadata;
|
||||
|
||||
public class MangaNatoWebCrawler : MangaWebCrawler
|
||||
public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
|
||||
{
|
||||
public override string SourceId => "MangaNato";
|
||||
|
||||
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);
|
||||
|
||||
SourceManga manga = new()
|
||||
{
|
||||
Title = node.TitleNode?.InnerText ?? string.Empty,
|
||||
Title = new()
|
||||
{
|
||||
Name = node.TitleNode?.InnerText ?? string.Empty,
|
||||
Language = Language.Unknown
|
||||
},
|
||||
AlternateTitles = GetAlternateTitles(node.AlternateTitlesNode),
|
||||
Contributors = GetContributors(node.AuthorsNode),
|
||||
Status = GetStatus(node.StatusNode),
|
||||
@@ -25,14 +31,21 @@ public class MangaNatoWebCrawler : MangaWebCrawler
|
||||
RatingPercent = GetRatingPercent(node.AverageRatingNode, node.BestRatingNode),
|
||||
Votes = node.VotesNode != null ? int.Parse(node.VotesNode.InnerText) : 0,
|
||||
Views = GetViews(node.ViewsNode),
|
||||
Description = GetTextFromNodes(node.StoryDescriptionTextNodes),
|
||||
Descriptions =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = GetTextFromNodes(node.StoryDescriptionTextNodes),
|
||||
Language = Language.Unknown
|
||||
}
|
||||
],
|
||||
Chapters = GetChapters(node.ChapterNodes)
|
||||
};
|
||||
|
||||
return manga;
|
||||
}
|
||||
|
||||
private static List<SourceMangaTitle> GetAlternateTitles(HtmlNode? node)
|
||||
private static SourceMangaTitle[] GetAlternateTitles(HtmlNode? node)
|
||||
{
|
||||
if (node == null)
|
||||
return [];
|
||||
@@ -45,8 +58,8 @@ public class MangaNatoWebCrawler : MangaWebCrawler
|
||||
{
|
||||
SourceMangaTitle sourceMangaTitle = new()
|
||||
{
|
||||
Title = title,
|
||||
Language = SourceMangaLanguage.Unknown
|
||||
Name = title,
|
||||
Language = Language.Unknown
|
||||
};
|
||||
|
||||
sourceMangaTitles.Add(sourceMangaTitle);
|
||||
@@ -69,7 +82,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
|
||||
SourceMangaContributor contributor = new()
|
||||
{
|
||||
Name = name,
|
||||
Role = SourceMangaContributorRole.Author
|
||||
Role = ContributorRole.Author
|
||||
};
|
||||
|
||||
contributors.Add(contributor);
|
||||
@@ -88,7 +101,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string> GetGenres(HtmlNode? node)
|
||||
private static string[] GetGenres(HtmlNode? node)
|
||||
{
|
||||
if (node == null)
|
||||
return [];
|
||||
@@ -152,12 +165,12 @@ public class MangaNatoWebCrawler : MangaWebCrawler
|
||||
return (int)Math.Round(average / best * 100);
|
||||
}
|
||||
|
||||
private static List<SourceMangaChapter> GetChapters(HtmlNodeCollection? chapterNodes)
|
||||
private static SourceMangaChapter[] GetChapters(HtmlNodeCollection? chapterNodes)
|
||||
{
|
||||
List<SourceMangaChapter> chapters = [];
|
||||
|
||||
if (chapterNodes == null)
|
||||
return chapters;
|
||||
return [];
|
||||
|
||||
List<SourceMangaChapter> chapters = [];
|
||||
|
||||
foreach (var node in chapterNodes)
|
||||
{
|
||||
@@ -177,7 +190,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
|
||||
chapters.Add(chapter);
|
||||
}
|
||||
|
||||
return chapters;
|
||||
return [.. chapters];
|
||||
}
|
||||
|
||||
private static float GetChapterNumber(HtmlNode? chapterNameNode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using MangaReader.Core.HttpService;
|
||||
using MangaReader.Core.Http;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -23,12 +23,15 @@ public partial class NatoMangaClient(IHttpService httpService) : INatoMangaClien
|
||||
{
|
||||
string url = GetSearchUrl(searchWord);
|
||||
|
||||
string response = await httpService.GetStringAsync(url, cancellationToken);
|
||||
Dictionary<string,string> requestHeader = [];
|
||||
requestHeader.Add("Referer", "https://www.natomanga.com/");
|
||||
|
||||
string response = await httpService.GetStringAsync(url, requestHeader, cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<NatoMangaSearchResult[]>(response, _jsonSerializerOptions) ?? [];
|
||||
}
|
||||
|
||||
protected string GetSearchUrl(string searchWord)
|
||||
protected static string GetSearchUrl(string searchWord)
|
||||
{
|
||||
string formattedSeachWord = GetFormattedSearchWord(searchWord);
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
using HtmlAgilityPack;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Metadata;
|
||||
|
||||
namespace MangaReader.Core.Sources.NatoManga.Metadata;
|
||||
|
||||
public class NatoMangaWebCrawler : MangaWebCrawler
|
||||
public class NatoMangaWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
|
||||
{
|
||||
public override string SourceId => "NatoManga";
|
||||
|
||||
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);
|
||||
|
||||
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),
|
||||
Chapters = GetChapters(node.ChapterNodes)
|
||||
};
|
||||
@@ -22,7 +27,7 @@ public class NatoMangaWebCrawler : MangaWebCrawler
|
||||
return manga;
|
||||
}
|
||||
|
||||
private static List<string> GetGenres(HtmlNode? node)
|
||||
private static string[] GetGenres(HtmlNode? node)
|
||||
{
|
||||
if (node == null)
|
||||
return [];
|
||||
@@ -72,12 +77,12 @@ public class NatoMangaWebCrawler : MangaWebCrawler
|
||||
};
|
||||
}
|
||||
|
||||
private static List<SourceMangaChapter> GetChapters(HtmlNodeCollection? chapterNodes)
|
||||
private static SourceMangaChapter[] GetChapters(HtmlNodeCollection? chapterNodes)
|
||||
{
|
||||
List<SourceMangaChapter> chapters = [];
|
||||
|
||||
if (chapterNodes == null)
|
||||
return chapters;
|
||||
return [];
|
||||
|
||||
List<SourceMangaChapter> chapters = [];
|
||||
|
||||
foreach (var node in chapterNodes)
|
||||
{
|
||||
@@ -105,7 +110,7 @@ public class NatoMangaWebCrawler : MangaWebCrawler
|
||||
chapters.Add(chapter);
|
||||
}
|
||||
|
||||
return chapters;
|
||||
return [.. chapters];
|
||||
}
|
||||
|
||||
private static float GetChapterNumber(HtmlNode chapterNameNode)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using HtmlAgilityPack;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Pages;
|
||||
|
||||
namespace MangaReader.Core.Sources.NatoManga.Pages;
|
||||
|
||||
public class NatoMangaPageProvider(IHtmlLoader htmlLoader) : IMangaPageProvider
|
||||
{
|
||||
public string SourceId => "NatoManga";
|
||||
|
||||
public async Task<IReadOnlyList<string>> GetPageImageUrlsAsync(string chapterUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
List<string> imageUrlCollection = [];
|
||||
|
||||
HtmlDocument document = await htmlLoader.GetHtmlDocumentAsync(chapterUrl, cancellationToken);
|
||||
HtmlNodeCollection? htmlNodeCollection = GetImageNodeCollection(document);
|
||||
|
||||
if (htmlNodeCollection == null)
|
||||
return imageUrlCollection;
|
||||
|
||||
foreach (var htmlNode in htmlNodeCollection)
|
||||
{
|
||||
string imageSourceUrl = htmlNode.GetAttributeValue<string>("src", string.Empty);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imageSourceUrl))
|
||||
continue;
|
||||
|
||||
imageUrlCollection.Add(imageSourceUrl);
|
||||
}
|
||||
|
||||
return imageUrlCollection;
|
||||
}
|
||||
|
||||
private static HtmlNodeCollection? GetImageNodeCollection(HtmlDocument document)
|
||||
{
|
||||
HtmlNode? chapterReaderNode = document.DocumentNode.SelectSingleNode(".//div[@class='container-chapter-reader']");
|
||||
|
||||
if (chapterReaderNode == null)
|
||||
return null;
|
||||
|
||||
return chapterReaderNode.SelectNodes(".//img");
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public partial class NatoMangaSearchProvider(INatoMangaClient natoMangaClient) :
|
||||
{
|
||||
MangaSearchResult mangaSearchResult = new()
|
||||
{
|
||||
Source = SourceId,
|
||||
Title = searchResult.Name,
|
||||
Thumbnail = searchResult.Thumb,
|
||||
Url = searchResult.Url
|
||||
|
||||
@@ -11,21 +11,27 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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-Search-Response-2.json" />
|
||||
<None Remove="Sources\NatoManga\Api\Manga-Chapter-Response.html" />
|
||||
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="WebCrawlers\NatoManga\SampleMangaPage.html">
|
||||
<EmbeddedResource Include="Sources\NatoManga\Metadata\Manga-Response.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm">
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Sources\MangaNato\Metadata\Manga-Response.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Chapter-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.json" />
|
||||
<EmbeddedResource Include="Sources\NatoManga\Pages\Manga-Chapter-Response.html" />
|
||||
<EmbeddedResource Include="Sources\NatoManga\Api\Manga-Search-Response.json" />
|
||||
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" />
|
||||
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
|
||||
@@ -36,11 +42,12 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -54,4 +61,9 @@
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="WebCrawlers\NatoManga\" />
|
||||
<Folder Include="WebCrawlers\Samples\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
90
MangaReader.Tests/Pipeline/MangaPipelineTests.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using MangaReader.Core.Common;
|
||||
using MangaReader.Core.Data;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Pipeline;
|
||||
using MangaReader.Tests.Utilities;
|
||||
using Shouldly;
|
||||
|
||||
namespace MangaReader.Tests.Pipeline;
|
||||
|
||||
public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<TestDbContextFactory>
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunAsync_SavesMangaTitlesChaptersGenres()
|
||||
{
|
||||
using MangaContext context = factory.CreateContext();
|
||||
var pipeline = new MangaPipeline(context);
|
||||
|
||||
var sourceManga = new SourceManga
|
||||
{
|
||||
Title = new()
|
||||
{
|
||||
Name = "Fullmetal Alchemist",
|
||||
Language = Language.English
|
||||
},
|
||||
AlternateTitles =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = "Hagane no Renkinjutsushi",
|
||||
Language = Language.Romaji
|
||||
}
|
||||
],
|
||||
Genres = ["Action", "Adventure"],
|
||||
Contributors =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = "Manga Author",
|
||||
Role = ContributorRole.Author
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Manga Author",
|
||||
Role = ContributorRole.Artist
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Helper Artist",
|
||||
Role = ContributorRole.Artist
|
||||
}
|
||||
],
|
||||
Chapters =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Number = 1,
|
||||
Title = "The Two Alchemists",
|
||||
Volume = 1,
|
||||
Url = string.Empty
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
MangaMetadataPipelineRequest request = new()
|
||||
{
|
||||
SourceName = "MySource",
|
||||
SourceUrl = "https://wwww.mymangasource.org/my-manga",
|
||||
SourceManga = sourceManga
|
||||
};
|
||||
|
||||
await pipeline.RunMetadataAsync(request, CancellationToken.None);
|
||||
|
||||
context.Mangas.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.MangaContributors.Count().ShouldBe(3);
|
||||
context.MangaContributors.ElementAt(0).Contributor.Name.ShouldBe("Manga Author");
|
||||
context.MangaContributors.ElementAt(0).Role.ShouldBe(ContributorRole.Author);
|
||||
context.MangaContributors.ElementAt(1).Contributor.Name.ShouldBe("Manga Author");
|
||||
context.MangaContributors.ElementAt(1).Role.ShouldBe(ContributorRole.Artist);
|
||||
context.MangaContributors.ElementAt(2).Contributor.Name.ShouldBe("Helper Artist");
|
||||
context.MangaContributors.ElementAt(2).Role.ShouldBe(ContributorRole.Artist);
|
||||
|
||||
context.SourceChapters.ShouldHaveSingleItem();
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,16 @@ public class MangaSearchCoordinatorTests
|
||||
[
|
||||
new()
|
||||
{
|
||||
Title = "Test Manga 1",
|
||||
Source = "Manga Source 1",
|
||||
Url = "https://mangasource1.com/manga/1",
|
||||
Title = "Test Manga 1",
|
||||
Thumbnail = "https://mangasource1.com/manga/cover/1.png"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Source = "Manga Source 1",
|
||||
Url = "https://mangasource1.com/manga/2",
|
||||
Title = "Test Manga 2",
|
||||
Url = "https://mangasource2.com/manga/2",
|
||||
Thumbnail = "https://mangasource2.com/manga/cover/2.png"
|
||||
}
|
||||
]);
|
||||
@@ -35,8 +37,9 @@ public class MangaSearchCoordinatorTests
|
||||
[
|
||||
new()
|
||||
{
|
||||
Source = "Manga Source 2",
|
||||
Url = "https://mangasource2.com/manga/3",
|
||||
Title = "Test Manga 3",
|
||||
Url = "https://mangasource3.com/manga/3",
|
||||
Thumbnail = "https://mangasource3.com/manga/cover/3.png"
|
||||
},
|
||||
]);
|
||||
@@ -57,14 +60,16 @@ public class MangaSearchCoordinatorTests
|
||||
[
|
||||
new()
|
||||
{
|
||||
Title = "Test Manga 1",
|
||||
Source = "Manga Source 1",
|
||||
Url = "https://mangasource1.com/manga/1",
|
||||
Title = "Test Manga 1",
|
||||
Thumbnail = "https://mangasource1.com/manga/cover/1.png"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Source = "Manga Source 1",
|
||||
Url = "https://mangasource1.com/manga/2",
|
||||
Title = "Test Manga 2",
|
||||
Url = "https://mangasource2.com/manga/2",
|
||||
Thumbnail = "https://mangasource2.com/manga/cover/2.png"
|
||||
}
|
||||
]);
|
||||
@@ -73,8 +78,9 @@ public class MangaSearchCoordinatorTests
|
||||
[
|
||||
new()
|
||||
{
|
||||
Source = "Manga Source 2",
|
||||
Url = "https://mangasource2.com/manga/3",
|
||||
Title = "Test Manga 3",
|
||||
Url = "https://mangasource3.com/manga/3",
|
||||
Thumbnail = "https://mangasource3.com/manga/cover/3.png"
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,890 @@
|
||||
{
|
||||
"result": "ok",
|
||||
"response": "collection",
|
||||
"data": [
|
||||
{
|
||||
"id": "e78a489b-6632-4d61-b00b-5206f5b8b22b",
|
||||
"type": "manga",
|
||||
"attributes": {
|
||||
"title": { "en": "Tensei Shitara Slime Datta Ken" },
|
||||
"altTitles": [
|
||||
{ "en": "That Time I Got Reincarnated as a Slime" },
|
||||
{ "fr": "Moi, quand je me r\u00e9incarne en Slime" },
|
||||
{ "pl": "Odrodzony jako galareta" },
|
||||
{ "en": "Regarding Reincarnated to Slime" },
|
||||
{ "ja-ro": "Tensei Slime" },
|
||||
{ "ja-ro": "TenSli" },
|
||||
{ "ja-ro": "TenSura" },
|
||||
{ "en": "In Regards to My Reincarnation as a Slime" },
|
||||
{ "it": "Vita da Slime" },
|
||||
{ "ru": "\u041e \u043c\u043e\u0451\u043c \u043f\u0435\u0440\u0435\u0440\u043e\u0436\u0434\u0435\u043d\u0438\u0438 \u0432 \u0441\u043b\u0438\u0437\u044c" },
|
||||
{ "th": "\u0e40\u0e01\u0e34\u0e14\u0e43\u0e2b\u0e21\u0e48\u0e17\u0e31\u0e49\u0e07\u0e17\u0e35\u0e01\u0e47\u0e40\u0e1b\u0e47\u0e19\u0e2a\u0e44\u0e25\u0e21\u0e4c\u0e44\u0e1b\u0e0b\u0e30\u0e41\u0e25\u0e49\u0e27" },
|
||||
{ "zh-hk": "\u5173\u4e8e\u6211\u8f6c\u751f\u540e\u6210\u4e3a\u53f2\u83b1\u59c6\u7684\u90a3\u4ef6\u4e8b" },
|
||||
{ "ja": "\u8ee2\u751f\u3057\u305f\u3089\u30b9\u30e9\u30a4\u30e0\u3060\u3063\u305f\u4ef6" },
|
||||
{ "ko": "\uc804\uc0dd\ud588\ub354\ub2c8 \uc2ac\ub77c\uc784\uc774\uc5c8\ub358 \uac74\uc5d0 \ub300\ud558\uc5ec" },
|
||||
{ "es-la": "Aquella vez que me convert\u00ed en Slime" },
|
||||
{ "ar": "\u0630\u0644\u0643 \u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0630\u064a \u062a\u062c\u0633\u062f\u062a \u0641\u064a\u0647 \u0643\u0633\u0644\u0627\u064a\u0645" },
|
||||
{ "fi": "Kun j\u00e4lleensynnyin hirvi\u00f6n\u00e4" },
|
||||
{ "tr": "O zaman bir bal\u00e7\u0131k olarak reenkarne oldum" },
|
||||
{ "tr": "O zaman bir slime olarak reenkarne oldum" },
|
||||
{ "de": "Meine Wiedergeburt als Schleim in einer anderen Welt" }
|
||||
],
|
||||
"description": {
|
||||
"en": "The ordinary Mikami Satoru found himself dying after being stabbed by a slasher. It should have been the end of his meager 37 years, but he found himself deaf and blind after hearing a mysterious voice. \nHe had been reincarnated into a slime! \n \nWhile complaining about becoming the weak but famous slime and enjoying the life of a slime at the same time, Mikami Satoru met with the Catastrophe-level monster \u201cStorm Dragon Veldora\u201d, and his fate began to move.\n\n---\n**Links:**\n- Alternative Official English - [K MANGA](https:\/\/kmanga.kodansha.com\/title\/10044\/episode\/317350) (U.S. Only), [INKR](https:\/\/comics.inkr.com\/title\/233-that-time-i-got-reincarnated-as-a-slime), [Azuki](https:\/\/www.azuki.co\/series\/that-time-i-got-reincarnated-as-a-slime), [Coolmic](https:\/\/coolmic.me\/titles\/587), [Manga Planet](https:\/\/mangaplanet.com\/comic\/618e32db10673)",
|
||||
"ru": "37-\u043b\u0435\u0442\u043d\u0438\u0439 \u044f\u043f\u043e\u043d\u0435\u0446-\u0445\u043e\u043b\u043e\u0441\u0442\u044f\u043a \u0431\u044b\u043b \u0437\u0430\u0440\u0435\u0437\u0430\u043d \u043d\u0430 \u0443\u043b\u0438\u0446\u0435 \u043a\u0430\u043a\u0438\u043c-\u0442\u043e \u043c\u0435\u0440\u0437\u0430\u0432\u0446\u0435\u043c-\u0433\u0440\u0430\u0431\u0438\u0442\u0435\u043b\u0435\u043c. \u0422\u0443\u0442 \u0431\u044b \u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u0438 \u043a\u043e\u043d\u0435\u0446, \u0434\u0430 \u0432\u0441\u0451 \u043e\u0431\u0435\u0440\u043d\u0443\u043b\u043e\u0441\u044c \u0438\u043d\u0430\u0447\u0435, \u043d\u0435\u043e\u0436\u0438\u0434\u0430\u043d\u043d\u043e \u043e\u043d \u043f\u0435\u0440\u0435\u0440\u043e\u0434\u0438\u043b\u0441\u044f \u0441\u043b\u0438\u0437\u044c\u044e \u0432 \u0444\u044d\u043d\u0442\u0435\u0437\u0438\u0439\u043d\u043e\u043c \u043c\u0438\u0440\u0435. \u041d\u043e \u0447\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u043f\u0443\u0441\u043a\u0430\u0439 \u0438 \u0440\u0430\u0437\u0443\u043c\u043d\u0430\u044f, \u043d\u043e \u0441\u043b\u0438\u0437\u044c? \r\n\r\n\r\n---\r\n\r\n**Links:** \r\n- [Anime Season 1 on ANN](https:\/\/www.animenewsnetwork.com\/encyclopedia\/anime.php?id=20736)",
|
||||
"es-la": "Un hombre, que al tratar de salvar a su compa\u00f1ero de trabajo y su novia, fue apu\u00f1alado por un ladr\u00f3n que escapaba. Mientras mor\u00eda desangrado escuch\u00f3 una voz extra\u00f1a. Esta voz escuch\u00f3 su lamento de haber muerto virgen y a causa de eso le dio la Habilidad \u00danica \"Gran Sabio\" \u00bfFue esto una burla? Ahora \u00e9l ha reencarnado como un Slime en otro mundo, \u00bfSer\u00e1 este el inicio de una emocionante aventura?",
|
||||
"pt-br": "Depois de ser morto por um ladr\u00e3o que fugia, um rapaz normal de 37 anos de idade se encontra reencarnado em um outro mundo como um slime cego com habilidades \u00fanicas. Com um novo nome \"Rimuru Tempest\" ele chegou depois de conhecer seu novo amigo, o \"n\u00edvel cat\u00e1strofe\", Drag\u00e3o da Tempestade Verudora, ele come\u00e7a sua vida de slime em outro mundo com seu crescente n\u00famero de seguidores."
|
||||
},
|
||||
"isLocked": true,
|
||||
"links": {
|
||||
"al": "86399",
|
||||
"ap": "that-time-i-got-reincarnated-as-a-slime",
|
||||
"bw": "series\/56105",
|
||||
"kt": "35483",
|
||||
"mu": "119910",
|
||||
"nu": "tensei-shitara-slime-datta-ken",
|
||||
"amz": "https:\/\/www.amazon.co.jp\/gp\/product\/B074CFC3N4",
|
||||
"cdj": "http:\/\/www.cdjapan.co.jp\/product\/NEOBK-1858955",
|
||||
"ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/334900",
|
||||
"mal": "87609",
|
||||
"raw": "https:\/\/pocket.shonenmagazine.com\/episode\/10834108156631339284",
|
||||
"engtl": "https:\/\/kodansha.us\/series\/that-time-i-got-reincarnated-as-a-slime"
|
||||
},
|
||||
"originalLanguage": "ja",
|
||||
"lastVolume": "",
|
||||
"lastChapter": "",
|
||||
"publicationDemographic": "shounen",
|
||||
"status": "ongoing",
|
||||
"year": 2015,
|
||||
"contentRating": "safe",
|
||||
"tags": [
|
||||
{
|
||||
"id": "0bc90acb-ccc1-44ca-a34a-b9f3a73259d0",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Reincarnation" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "36fd93ea-e8b8-445e-b836-358f02b3d33d",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Monsters" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "391b0423-d847-456f-aff0-8b0cfc03066b",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Action" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "39730448-9a5f-48a2-85b0-a70db87b1233",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Demons" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "4d32cc48-9f00-4cca-9b5a-a839f0764984",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Comedy" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "81183756-1453-4c81-aa9e-f6e1b63be016",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Samurai" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "ace04997-f6bd-436e-b261-779182193d3d",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Isekai" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "cdc58593-87dd-415e-bbc0-2ec27bf404cc",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Fantasy" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "f4122d1c-3b44-44d0-9936-ff7502c39ad3",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Adaptation" },
|
||||
"description": {},
|
||||
"group": "format",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"state": "published",
|
||||
"chapterNumbersResetOnNewVolume": false,
|
||||
"createdAt": "2018-11-05T01:59:35+00:00",
|
||||
"updatedAt": "2025-01-23T19:15:00+00:00",
|
||||
"version": 82,
|
||||
"availableTranslatedLanguages": [ "fr", "ar", "es-la", "id", "en", "pt-br" ],
|
||||
"latestUploadedChapter": "3872f6f3-f327-410c-b61b-0b955fc42609"
|
||||
},
|
||||
"relationships": [
|
||||
{
|
||||
"id": "dbf8af05-7173-49f3-bf60-f4ea3f586486",
|
||||
"type": "author"
|
||||
},
|
||||
{
|
||||
"id": "560748c6-fbe7-49f5-8258-7b3292942101",
|
||||
"type": "artist"
|
||||
},
|
||||
{
|
||||
"id": "1575a7ba-6f3e-477e-9491-74506a21b268",
|
||||
"type": "cover_art",
|
||||
"attributes": {
|
||||
"description": "Volume 28 Cover from Booklive",
|
||||
"volume": "28",
|
||||
"fileName": "67de8b2f-c080-4006-91dd-a3b87abdb7fd.jpg",
|
||||
"locale": "ja",
|
||||
"createdAt": "2025-01-23T19:13:27+00:00",
|
||||
"updatedAt": "2025-01-23T19:13:27+00:00",
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0d20230b-60de-4f02-b898-b477748ee667",
|
||||
"type": "manga",
|
||||
"related": "colored"
|
||||
},
|
||||
{
|
||||
"id": "0e620699-0033-4b54-beb6-1bd82e6ee02e",
|
||||
"type": "manga",
|
||||
"related": "side_story"
|
||||
},
|
||||
{
|
||||
"id": "1180743d-8e38-4c00-b767-c53169fadc6a",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "1f284c6f-73f2-48db-a43b-2c35c40b1021",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "40633ae0-794a-4dc7-b318-30774ef9908d",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "4c0f5ac2-37e9-421e-934f-b1351f9ee6b3",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "4fd9e91c-696f-468a-bf0c-a4d26468851c",
|
||||
"type": "manga",
|
||||
"related": "side_story"
|
||||
},
|
||||
{
|
||||
"id": "58703998-d847-42a2-9ff4-9c671d36772f",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "5ede3032-6278-439f-a06b-c3f6d1493554",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "615a8f24-4289-437d-b0b7-c32e5b9d09b0",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "61d81be6-2759-4cc4-9815-7952a3449149",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "7afb9330-261e-4717-8042-8d41b2b3deba",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "7b650718-55d6-4094-afe6-95f59e8d0c4c",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "7d580248-cf9c-4fb6-925e-343ffb3dcc7e",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "a1343483-8779-4b6f-b919-9025a89d98c3",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "b956fd7d-f50a-4e2b-94d7-84bd9aa125e1",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "bd76862b-640c-4448-b721-5a22b6691774",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "c2972668-1107-4c2f-a06b-aaa2252906fb",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "c8e83aab-43e8-425c-bd55-e7fa7fd666f7",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "cab847c6-2748-4259-b9f4-c62bffd51311",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "e2d738e5-340f-4b24-aef3-e624623154a0",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "f0e05005-4ac8-4f5b-aca9-8762ede16daa",
|
||||
"type": "manga",
|
||||
"related": "doujinshi"
|
||||
},
|
||||
{
|
||||
"id": "f1c79d23-d306-40e5-8b47-17cab7408f1a",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5e3a710f-0b0d-482b-9e84-d9c91960c625",
|
||||
"type": "manga",
|
||||
"attributes": {
|
||||
"title": { "en": "Yancha Gal no Anjou-san" },
|
||||
"altTitles": [
|
||||
{ "ja": "\u3084\u3093\u3061\u3083\u30ae\u30e3\u30eb\u306e\u5b89\u57ce\u3055\u3093" },
|
||||
{ "ja-ro": "Yancha Gyaru no Anjou-san" },
|
||||
{ "en": "Anjo the Mischievous Gal" },
|
||||
{ "en": "The Mischievous Gal Anjou-san" },
|
||||
{ "ru": "\u041e\u0437\u043e\u0440\u043d\u0430\u044f \u0413\u044f\u0440\u0443 \u0410\u043d\u0434\u0437\u0451-\u0441\u0430\u043d" },
|
||||
{ "th": "\u0e04\u0e38\u0e13\u0e2d\u0e31\u0e19\u0e42\u0e08 \u0e2b\u0e22\u0e2d\u0e01\u0e19\u0e31\u0e01\u0e40\u0e1e\u0e23\u0e32\u0e30\u0e23\u0e31\u0e01\u0e19\u0e30" },
|
||||
{ "zh": "\u64c5\u957f\u6311\u9017\u7684\u5b89\u57ce\u540c\u5b66" },
|
||||
{ "zh-ro": "Sh\u00e0nch\u00e1ng ti\u01ceod\u00f2u de \u0101nch\u00e9ng t\u00f3ngxu\u00e9" },
|
||||
{ "zh": "\u6dd8\u6c14\u8fa3\u59b9\u5b89\u57ce" },
|
||||
{ "zh": "\u987d\u76ae\u8fa3\u59b9\u5b89\u57ce\u540c\u5b66" },
|
||||
{ "ko": "\uc7a5\ub09c\uce58\ub294\uac38\ub8e8 \uc548\uc8e0 \uc591" },
|
||||
{ "ko-ro": "Jangnanchineungyalu Anjyo Yang" }
|
||||
],
|
||||
"description": {
|
||||
"de": "Seto ist ein total normaler und irgendwie langweiliger Sch\u00fcler. Aus einem unerkl\u00e4rlichen Grund jedoch, l\u00e4sst seine Mitsch\u00fclerin Anjou ihn nicht in Ruhe! Der ernste Seto und die energische Anjou bilden ein kontrastreiches Duo, doch Anjou macht das nichts aus, da sie sehr viel Spa\u00df hat, Seto zu necken. Andererseits hat Seto eine schwere Zeit mit Anjous Eskapaden. Wenn er doch nur w\u00fcsste, dass seine Reaktionen der Grund sind, dass Anjou weitermacht. \n \nDoch \u2026 ist ihr flirten wirklich nur gespielt \u2026?",
|
||||
"en": "Seto is a completely ordinary and somewhat boring high school student. Yet, for whatever reason, his errant gyaru classmate Anjou just won't leave him alone! The serious Seto and energetic Anjou make a contrasting duo, but Anjou doesn't seem to mind, as she has too much fun teasing him. On the other hand, Seto has a hard time dealing with all of her endless antics; little does he realize that his humorous reactions are precisely the reason Anjou enjoys his company.\n\nBut\u2026 just how much of her flirting is merely an act?\n\n**Official English:** [emaqi - USA & Canada only](https:\/\/emaqi.com\/manga\/anjo-the-mischievous-gal)",
|
||||
"fr": "Seto est un lyc\u00e9en ne souhaitant qu'une chose : passer sa scolarit\u00e9 sans \u00eatre remarqu\u00e9 et \u00eatre en paix. Malheureusement pour lui, il attire l'attention de la pire personne possible dans sa classe, Anjou la gal. A partir de ce jour, le quotidien de Seto va drastiquement changer gr\u00e2ce (ou \u00e0 cause) de sa camarade, cherchant \u00e0 s'amuser au d\u00e9pend de notre h\u00e9ros.",
|
||||
"ja": "\u771f\u9762\u76ee\u3067\u30af\u30e9\u30b9\u306e\u4e2d\u3067\u3082\u76ee\u7acb\u305f\u306a\u3044\u702c\u6238\u304f\u3093\u306b\u306f\u3001\u306a\u305c\u304b\u3044\u3064\u3082\u30a4\u30b1\u3066\u308b\u30ae\u30e3\u30eb\u306e\u5b89\u57ce\u3055\u3093\u304c\u3044\u3061\u3044\u3061\u30a8\u30ed\u304f\u7d61\u3093\u3067\u304f\u308b\u3002\u3044\u3064\u3082\u30ae\u30ea\u30ae\u30ea\u3067\u30c9\u30ad\u30c9\u30ad\u3059\u308b\u601d\u6625\u671f\u3074\u3061\u3074\u3061\u30e9\u30d6\u30b3\u30e1\u30c7\u30a3\uff01",
|
||||
"ru": "\u0421\u0435\u0442\u043e - \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u044b\u0439 \u0441\u0442\u0430\u0440\u0448\u0435\u043a\u043b\u0430\u0441\u0441\u043d\u0438\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u0435\u043b \u0442\u0438\u0445\u0443\u044e \u0448\u043a\u043e\u043b\u044c\u043d\u0443\u044e \u0436\u0438\u0437\u043d\u044c, \u043f\u043e\u043a\u0430 \u043d\u0435 \u0441\u0442\u0430\u043b \u0436\u0435\u0440\u0442\u0432\u043e\u0439 \u0434\u0440\u0430\u0437\u043d\u0438\u043b\u043e\u043a \u0433\u044f\u0440\u0443 \u0410\u043d\u0434\u0437\u0451-\u0441\u0430\u043d.",
|
||||
"pt-br": "Seto \u00e9 um estudante completamente comum e um tanto chato. Mesmo assim, por alguma raz\u00e3o, sua nada correta colega de classe Gal Anjou simplesmente n\u00e3o o deixa em paz!\n\nO Seto s\u00e9rio e a en\u00e9rgica Anjou formam uma dupla um tanto interessante, mas Anjou n\u00e3o parece se importar, pois se diverte muito provocando-o. Por outro lado, Seto tem dificuldade em lidar com todas as suas travessuras intermin\u00e1veis; mal ele percebe que suas rea\u00e7\u00f5es humor\u00edsticas s\u00e3o precisamente o motivo pelo qual Anjou gosta de sua companhia.\n\nMas\u2026 quanto de seu flerte \u00e9 meramente uma atua\u00e7\u00e3o?"
|
||||
},
|
||||
"isLocked": true,
|
||||
"links": {
|
||||
"al": "101315",
|
||||
"ap": "yancha-gal-no-anjou-san",
|
||||
"bw": "series\/154016",
|
||||
"kt": "40927",
|
||||
"mu": "145904",
|
||||
"amz": "https:\/\/www.amazon.co.jp\/dp\/B07J2W5N37",
|
||||
"cdj": "https:\/\/www.cdjapan.co.jp\/product\/NEOBK-2193867",
|
||||
"ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/447131\/",
|
||||
"mal": "111357",
|
||||
"raw": "https:\/\/piccoma.com\/web\/product\/30277?etype=episode",
|
||||
"engtl": "https:\/\/x.com\/emaqi_official\/status\/1838216760945770879"
|
||||
},
|
||||
"originalLanguage": "ja",
|
||||
"lastVolume": "",
|
||||
"lastChapter": "",
|
||||
"publicationDemographic": "seinen",
|
||||
"status": "ongoing",
|
||||
"year": 2017,
|
||||
"contentRating": "suggestive",
|
||||
"tags": [
|
||||
{
|
||||
"id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Romance" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "4d32cc48-9f00-4cca-9b5a-a839f0764984",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Comedy" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "caaa44eb-cd40-4177-b930-79d3ef2afe87",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "School Life" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Slice of Life" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "fad12b5e-68ba-460e-b933-9ae8318f5b65",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Gyaru" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"state": "published",
|
||||
"chapterNumbersResetOnNewVolume": false,
|
||||
"createdAt": "2019-05-21T04:40:11+00:00",
|
||||
"updatedAt": "2025-01-02T18:55:29+00:00",
|
||||
"version": 64,
|
||||
"availableTranslatedLanguages": [ "en", "fr", "pt-br", "ru", "es-la", "pl", "id", "vi", "it", "hu" ],
|
||||
"latestUploadedChapter": "8bfd2743-73f1-42b5-8a69-c41ac42f4117"
|
||||
},
|
||||
"relationships": [
|
||||
{
|
||||
"id": "02812ab1-327c-443c-ac52-0e602e0cafe4",
|
||||
"type": "author"
|
||||
},
|
||||
{
|
||||
"id": "02812ab1-327c-443c-ac52-0e602e0cafe4",
|
||||
"type": "artist"
|
||||
},
|
||||
{
|
||||
"id": "205787ba-a981-4966-b02e-e9cd82baacce",
|
||||
"type": "cover_art",
|
||||
"attributes": {
|
||||
"description": "Volume 15 Cover from BookLive",
|
||||
"volume": "15",
|
||||
"fileName": "c5111530-7823-4451-bd42-c439a2aaeece.jpg",
|
||||
"locale": "ja",
|
||||
"createdAt": "2025-02-18T21:22:27+00:00",
|
||||
"updatedAt": "2025-02-18T21:22:27+00:00",
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1d2e06de-cc7a-480b-a3b9-0d06971bd165",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "2fa18b85-7ec3-4c15-a87d-32cc6aed6ca8",
|
||||
"type": "manga",
|
||||
"related": "spin_off"
|
||||
},
|
||||
{
|
||||
"id": "7a2ecc5c-b215-47ab-a7f0-fcdeff941e9f",
|
||||
"type": "manga",
|
||||
"related": "side_story"
|
||||
},
|
||||
{
|
||||
"id": "c400ee54-26cd-48bf-ac88-5cf7cc3ab77c",
|
||||
"type": "manga",
|
||||
"related": "colored"
|
||||
},
|
||||
{
|
||||
"id": "ce068526-df38-45b9-899f-a2a672b4442a",
|
||||
"type": "manga",
|
||||
"related": "preserialization"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d8323b7b-9a7a-462b-90f0-2759fed52511",
|
||||
"type": "manga",
|
||||
"attributes": {
|
||||
"title": { "en": "Dosanko Gal wa Namaramenkoi" },
|
||||
"altTitles": [
|
||||
{ "ja": "\u9053\u7523\u5b50\u30ae\u30e3\u30eb\u306f\u306a\u307e\u3089\u3081\u3093\u3053\u3044" },
|
||||
{ "ja-ro": "Dosanko Gyura wa Namaramenkoi" },
|
||||
{ "en": "Hokkaido Gals are Super Adorable!" },
|
||||
{ "ko": "\ub3c4\uc0b0\ucf54 \uac38\ub8e8\ub294 \ucc38\ub9d0\ub85c \uadc0\uc5ec\uc6cc" },
|
||||
{ "pt-br": "Gyarus de Hokkaido s\u00e3o ador\u00e1veis!" },
|
||||
{ "ru": "\u0414\u043e\u0441\u0430\u043d\u043a\u043e-\u0433\u044f\u0440\u0443 \u0447\u0443\u0434\u043e \u043a\u0430\u043a \u043c\u0438\u043b\u044b" },
|
||||
{ "es": "Esa gal de Hokkaido es demasiado linda" },
|
||||
{ "tr": "Hokkaido'nun Gyaru K\u0131zlar\u0131 Acayip G\u00fczel!" },
|
||||
{ "uk": "\u0414\u043e\u0441\u0430\u043d\u043a\u043e-\u0433\u044f\u0440\u0443 \u0441\u0442\u0440\u0430\u0445 \u044f\u043a\u0456 \u0433\u0430\u0440\u043d\u0435\u043d\u044c\u043a\u0456" }
|
||||
],
|
||||
"description": {
|
||||
"en": "Shiki Tsubasa has just moved from Tokyo to Hokkaido in the middle of winter. Not quite appreciating how far apart towns are in the country, he gets off the taxi at the next town over from his destination so he can see the sights around his home, but he is shocked when he learns the \"next town\" is a 3-hour walk away. However, he also meets a cute Dosanko (born and raised in Hokkaido) gyaru named Fuyuki Minami who is braving 8 degrees celcius below 0 weather in the standard gyaru outfit of short skirts and bare legs!",
|
||||
"es": "Tsubasa es un chico que se va vivir desde Tokyo a Hokkaido en pleno invierno porque han trasladado a su padre. En su antiguo instituto no era nada popular y se met\u00edan con \u00e9l siempre que pod\u00edan. Dispuesto a empezar una nueva vida, llega en taxi entre un paisaje nevado y se baja a las primeras de cambio pensando que ya ha llegado.\n\nPero pronto se da cuenta de que en Hokkaido las cosas son diferentes que en la gran ciudad y aqu\u00ed las distancias son mucho mayores, as\u00ed que el sitio al que iba est\u00e1 a tres horas andando: eso significa que se ha quedado tirado en medio de la nieve. El impacto es mayor cuando se encuentra con Minami Fuyuki, una chica vestida como la t\u00edpica gal pese al fr\u00edo que hace.\n\nMinami no s\u00f3lo es guapa, sino que es una chica supermaja y con las maneras de una gal, un crush instant\u00e1neo para Tsubasa, aunque el combo de gal m\u00e1s chica de pueblo lo deja bastante perplejo y m\u00e1s cuando se entera de que van al mismo instituto. Gracias a Minami podr\u00e1 integrarse bastante r\u00e1pido y conocer a otras gals, aunque el choque cultural a varios niveles est\u00e1 servido. \u00a1Sus d\u00edas de gals, fr\u00edo y diversi\u00f3n acaban de empezar!",
|
||||
"fr": "Natsukawa Tsubasa vient de d\u00e9m\u00e9nager de Tokyo \u00e0 Hokkaido, en plein hiver. Ne se rendant pas compte de la r\u00e9alit\u00e9 des distances \u00e0 la campagne, il se retrouve perdu \u00e0 3 heures de marche de sa destination. Mais il fait \u00e9galement la rencontre d\u2019une Dosanko (\u00ab n\u00e9e et \u00e9lev\u00e9e \u00e0 Hokkaido \u00bb) gyaru, en minijupe par \u2013 8\u00b0C ! \n\n\n---\n\n**Links:** \n- [Author's Twitter](https:\/\/twitter.com\/ikada_kai) | [Author's YouTube channel](https:\/\/www.youtube.com\/channel\/UC-U4OJu-cEF2VlnM61B77bQ) | [Author's Pixiv](https:\/\/www.pixiv.net\/en\/users\/21326958)",
|
||||
"ja": "\u5317\u6d77\u9053\u5317\u898b\u5e02\u306b\u8ee2\u6821\u3057\u3066\u304d\u305f\u56db\u5b63 \u7ffc\u306f\u3001\u771f\u3063\u767d\u306a\u9280\u4e16\u754c\u30671\u4eba\u306e\u201c\u30ae\u30e3\u30eb\u201d\u3068\u51fa\u4f1a\u3046\u2015\u2015\u3002\u6c37\u70b9\u4e0b\u3067\u3082\u751f\u8db3\u3067\u3001\u8ddd\u96e2\u304c\u8fd1\u304f\u3066\u3001\u65b9\u8a00\u30d0\u30ea\u30d0\u30ea\uff01",
|
||||
"ru": "\u041d\u0430\u0446\u0443\u043a\u0430\u0432\u0430 \u0426\u0443\u0431\u0430\u0441\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u043f\u0435\u0440\u0435\u0435\u0445\u0430\u043b \u0438\u0437 \u0422\u043e\u043a\u0438\u043e \u043d\u0430 \u0425\u043e\u043a\u043a\u0430\u0439\u0434\u043e. \u00a0\u041d\u0435 \u0438\u043c\u0435\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f, \u043e \u0442\u043e\u043c \u043a\u0430\u043a\u043e\u0435 \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u0440\u0430\u0441\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u043d\u0430\u0441\u0435\u043b\u0451\u043d\u043d\u044b\u043c\u0438 \u043f\u0443\u043d\u043a\u0442\u0430\u043c\u0438 \u0432 \u0437\u0430\u0445\u043e\u043b\u0443\u0441\u0442\u044c\u0435 \u043e\u043d \u0440\u0435\u0448\u0438\u043b \u0432\u044b\u0439\u0442\u0438 \u0438\u0437 \u0442\u0430\u043a\u0441\u0438 \u0432 \u0441\u043e\u0441\u0435\u0434\u043d\u0435\u043c \u0433\u043e\u0440\u043e\u0434\u0435, \u043f\u043e\u0441\u0440\u0435\u0434\u0438 \u0437\u0438\u043c\u044b. \u041e\u0434\u043d\u0430\u043a\u043e \u043e\u043d \u0431\u044b\u043b \u0448\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d \u0443\u0437\u043d\u0430\u0432, \u0447\u0442\u043e \u0434\u043e \u0441\u043e\u0441\u0435\u0434\u043d\u0435\u0433\u043e \u0433\u043e\u0440\u043e\u0434\u0430 \u0438\u0434\u0442\u0438 \u0446\u0435\u043b\u044b\u0445 3 \u0447\u0430\u0441\u0430. \u0422\u0443\u0442 \u0436\u0435 \u043e\u043d \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0435\u0442 \u0414\u043e\u0441\u0430\u043d\u043a\u043e-\u0433\u044f\u0440\u0443 (\u0440\u043e\u0434\u043e\u043c \u0438\u0437 \u0425\u043e\u043a\u043a\u0430\u0439\u0434\u043e) \u043f\u043e \u0438\u043c\u0435\u043d\u0438 \u0424\u0443\u044e\u043a\u0438 \u041c\u0438\u043d\u0430\u043c\u0438, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0438 \u043c\u0438\u043d\u0443\u0441 8 \u0433\u0440\u0430\u0434\u0443\u0441\u0430\u0445 \u0446\u0435\u043b\u044c\u0441\u0438\u044f \u0445\u043e\u0434\u0438\u0442 \u0432 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0439 \u043e\u0434\u0435\u0436\u0434\u0435 \u0433\u044f\u0440\u0443: \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0439 \u044e\u0431\u043a\u0435 \u0438 \u0441 \u0433\u043e\u043b\u044b\u043c\u0438 \u043d\u043e\u0433\u0430\u043c\u0438. \n\n\n---",
|
||||
"tr": "Liseli Tsubasa, Hokkaido\u2019daki Kitami \u015fehrine ta\u015f\u0131n\u0131r ve oradaki bir otob\u00fcs dura\u011f\u0131nda bir \"gyaru\" ile tan\u0131\u015f\u0131r. Dondurucu so\u011fu\u011fa ra\u011fmen \u00e7\u0131plak bacaklar\u0131yla beyaz kar manzaras\u0131n\u0131n kar\u015f\u0131s\u0131nda tek ba\u015f\u0131na durdu\u011funu g\u00f6rmek kalbini cezbeder.",
|
||||
"pt-br": "Shiki Tsubasa acaba de se mudar de T\u00f3quio para Hokkaido no meio do inverno. Sem perceber a dist\u00e2ncia entre as cidades do pa\u00eds, ele desce do t\u00e1xi na cidade mais pr\u00f3xima de seu destino para poder ver os pontos tur\u00edsticos ao redor de sua casa, mas fica chocado quando descobre que a \"pr\u00f3xima cidade\" \u00e9 uma a tr\u00eas horas a p\u00e9. No entanto, ele tamb\u00e9m conhece uma linda Dosanko (nascido e criado em Hokkaido) gyaru chamado Fuyuki Minami, que est\u00e1 enfrentando um clima de 8 graus Celsius abaixo de 0 com a roupa gyaru padr\u00e3o de saias curtas e pernas nuas!"
|
||||
},
|
||||
"isLocked": false,
|
||||
"links": {
|
||||
"al": "111403",
|
||||
"ap": "dosanko-gyaru-wa-namaramenkoi",
|
||||
"bw": "series\/225993\/list",
|
||||
"kt": "55322",
|
||||
"mu": "152497",
|
||||
"amz": "https:\/\/www.amazon.co.jp\/dp\/B084NT3Q8X",
|
||||
"cdj": "https:\/\/www.cdjapan.co.jp\/product\/NEOBK-2635186",
|
||||
"ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/568854",
|
||||
"mal": "121597",
|
||||
"raw": "https:\/\/shonenjumpplus.com\/episode\/10834108156684177150",
|
||||
"engtl": "https:\/\/mangaplus.shueisha.co.jp\/titles\/100116"
|
||||
},
|
||||
"originalLanguage": "ja",
|
||||
"lastVolume": "14",
|
||||
"lastChapter": "119",
|
||||
"publicationDemographic": "shounen",
|
||||
"status": "completed",
|
||||
"year": 2019,
|
||||
"contentRating": "suggestive",
|
||||
"tags": [
|
||||
{
|
||||
"id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Romance" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "4d32cc48-9f00-4cca-9b5a-a839f0764984",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Comedy" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "aafb99c1-7f60-43fa-b75f-fc9502ce29c7",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Harem" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "b9af3a63-f058-46de-a9a0-e0c13906197a",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Drama" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "caaa44eb-cd40-4177-b930-79d3ef2afe87",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "School Life" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Slice of Life" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "fad12b5e-68ba-460e-b933-9ae8318f5b65",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Gyaru" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"state": "published",
|
||||
"chapterNumbersResetOnNewVolume": false,
|
||||
"createdAt": "2019-10-12T20:49:23+00:00",
|
||||
"updatedAt": "2025-02-01T22:40:38+00:00",
|
||||
"version": 50,
|
||||
"availableTranslatedLanguages": [ "es", "ru", "en" ],
|
||||
"latestUploadedChapter": "b0950d10-7e2d-4278-98bd-52f3d92cc494"
|
||||
},
|
||||
"relationships": [
|
||||
{
|
||||
"id": "b77fe548-6f64-4380-8cca-faee8891a7d3",
|
||||
"type": "author"
|
||||
},
|
||||
{
|
||||
"id": "b77fe548-6f64-4380-8cca-faee8891a7d3",
|
||||
"type": "artist"
|
||||
},
|
||||
{
|
||||
"id": "a78a2332-99cf-42b3-8285-0eed22c41251",
|
||||
"type": "cover_art",
|
||||
"attributes": {
|
||||
"description": "",
|
||||
"volume": "14",
|
||||
"fileName": "2f11791d-e3ff-4347-b4a8-b39aafc3b121.jpg",
|
||||
"locale": "ja",
|
||||
"createdAt": "2024-10-31T17:30:22+00:00",
|
||||
"updatedAt": "2024-10-31T17:30:22+00:00",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "a920060c-7e39-4ac1-980c-f0e605a40ae4",
|
||||
"type": "manga",
|
||||
"attributes": {
|
||||
"title": { "en": "Gal Yome no Himitsu" },
|
||||
"altTitles": [
|
||||
{ "ja": "\u30ae\u30e3\u30eb\u5ac1\u306e\u79d8\u5bc6" },
|
||||
{ "ja-ro": "Gyaru Yome no Himitsu" },
|
||||
{ "en": "Secrets of the Gal Wife" },
|
||||
{ "id": "Rahasia Istri Gal" },
|
||||
{ "en": "My Gyaru Wife's Secret" }
|
||||
],
|
||||
"description": {
|
||||
"en": "Fuyuki is a beautiful and cool gal! But there's a secret side of her that she only shows in front of her husband...?",
|
||||
"ja": "\u51ac\u96ea\uff08\u3075\u3086\u304d\uff09\u306f\u7f8e\u4eba\u3067\u30af\u30fc\u30eb\u306a\u30ae\u30e3\u30eb\uff01\u3067\u3082\u300c\u65e6\u90a3\u300d\u306e\u524d\u3060\u3051\u898b\u305b\u308b\u79d8\u5bc6\u306e\u59ff\u304c\u3042\u3063\u3066\u2026\u2026\uff1fSNS\u7d2f\u8a0837\u4e07\u3044\u3044\u306d\uff01\u306e\u53ef\u611b\u3044\u300c\u30ae\u30e3\u30eb\u5ac1\u300d\u3068\u306e\u30e9\u30d6\u30b3\u30e1\u30c7\u30a3\uff01",
|
||||
"es-la": "\u00a1Fuyuki es una hermosa y atractiva gal! \u00bfPero hay una parte de ella que solo le muestra a su esposo y mantiene en secreto del resto? Una linda comedia rom\u00e1ntica con una esposa que es una gal.",
|
||||
"pt-br": "Fuyuki \u00e9 uma linda gyaru! Mas tem um lado secreto que ela s\u00f3 mostra para o seu marido? Uma com\u00e9dia rom\u00e2ntica fofa com uma esposa gyaru!"
|
||||
},
|
||||
"isLocked": false,
|
||||
"links": {
|
||||
"al": "169734",
|
||||
"ap": "gyaru-yome-no-himitsu",
|
||||
"bw": "series\/495151\/list",
|
||||
"kt": "69730",
|
||||
"mu": "sjl40n9",
|
||||
"amz": "https:\/\/www.amazon.co.jp\/dp\/B0DMT6X4NC",
|
||||
"cdj": "https:\/\/www.cdjapan.co.jp\/product\/NEOBK-3016982",
|
||||
"ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/857988",
|
||||
"mal": "163267",
|
||||
"raw": "https:\/\/ganma.jp\/galyome"
|
||||
},
|
||||
"originalLanguage": "ja",
|
||||
"lastVolume": "",
|
||||
"lastChapter": "",
|
||||
"publicationDemographic": "seinen",
|
||||
"status": "ongoing",
|
||||
"year": 2023,
|
||||
"contentRating": "suggestive",
|
||||
"tags": [
|
||||
{
|
||||
"id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Romance" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "4d32cc48-9f00-4cca-9b5a-a839f0764984",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Comedy" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "92d6d951-ca5e-429c-ac78-451071cbf064",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Office Workers" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Slice of Life" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "fad12b5e-68ba-460e-b933-9ae8318f5b65",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Gyaru" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"state": "published",
|
||||
"chapterNumbersResetOnNewVolume": false,
|
||||
"createdAt": "2023-09-30T01:44:37+00:00",
|
||||
"updatedAt": "2025-01-27T20:50:36+00:00",
|
||||
"version": 24,
|
||||
"availableTranslatedLanguages": [ "en", "pt-br", "th", "es-la", "id", "it", "vi", "ru" ],
|
||||
"latestUploadedChapter": "8f5be93c-b5fa-43ee-8e29-41599a5d9bb5"
|
||||
},
|
||||
"relationships": [
|
||||
{
|
||||
"id": "5c66247f-85bb-4fac-8c08-c207f5ec53ce",
|
||||
"type": "author"
|
||||
},
|
||||
{
|
||||
"id": "5c66247f-85bb-4fac-8c08-c207f5ec53ce",
|
||||
"type": "artist"
|
||||
},
|
||||
{
|
||||
"id": "e8a74e9c-bd63-4424-a1d2-02a9d7286ab5",
|
||||
"type": "cover_art",
|
||||
"attributes": {
|
||||
"description": "",
|
||||
"volume": "2",
|
||||
"fileName": "07d02b26-cbd0-4323-8774-9d83579863d5.jpg",
|
||||
"locale": "ja",
|
||||
"createdAt": "2025-02-18T09:27:22+00:00",
|
||||
"updatedAt": "2025-02-18T09:27:22+00:00",
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "45220025-46fb-44c5-a975-a4754fe512a2",
|
||||
"type": "creator"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cf7b7869-3d9a-4c4d-bd06-249eba113558",
|
||||
"type": "manga",
|
||||
"attributes": {
|
||||
"title": { "en": "Boku to Gal ga Fufu ni Narumade" },
|
||||
"altTitles": [
|
||||
{ "ja": "\u50d5\u3068\u541b\u304c\u592b\u5a66\u306b\u306a\u308b\u307e\u3067" },
|
||||
{ "ja-ro": "Boku to Gal ga Fuufu ni Naru made" },
|
||||
{ "en": "Until the Gal and I Become a Married Couple" },
|
||||
{ "ru": "\u041f\u043e\u043a\u0430 \u043c\u044b \u0441 \u0434\u0435\u0432\u0443\u0448\u043a\u043e\u0439 \u043d\u0435 \u0441\u0442\u0430\u043d\u0435\u043c \u0441\u0443\u043f\u0440\u0443\u0436\u0435\u0441\u043a\u043e\u0439 \u043f\u0430\u0440\u043e\u0439" }
|
||||
],
|
||||
"description": {
|
||||
"en": "Saku Kanakura was a boy born into a poor household. He studied hard and managed to pass the exam to an excellent high school but, it soon turned out that his parents were in debt! When it was time to pay the debt off, a mysterious gal appeared and offered to pay off their debt. A big burden was almost off Kanakura's head, but the gal made a condition in return which stated that\u2014",
|
||||
"id": "Saku Kanakura adalah anak laki-laki yang lahir dari keluarga miskin. Dia belajar dengan giat dan berhasil lulus ujian ke sekolah menengah yang bagus, tetapi ternyata orang tuanya terlilit hutang! Ketika tiba waktunya untuk melunasi hutang, seorang gadis misterius muncul dan menawarkan untuk melunasi hutang mereka. Sebuah beban besar hampir terlepas dari kepala Kanakura, tetapi gadis itu membuat syarat sebagai balasannya yang menyatakan bahwa\u2014",
|
||||
"ja": "\u8ca7\u4e4f\u306a\u5bb6\u5ead\u306b\u80b2\u3063\u305f\u5c11\u5e74\u30fb\u795e\u9577\u5009\u98af\u7a7a\u3002\u731b\u52c9\u5f37\u306e\u7532\u6590\u3042\u3063\u3066\u898b\u4e8b\u5e0c\u671b\u3059\u308b\u9ad8\u6821\u3078\u5408\u683c\u3057\u305f\u5f7c\u3060\u3063\u305f\u304c\u3001\u4e21\u89aa\u306e\u501f\u91d1\u304c\u5224\u660e\uff01 \u501f\u91d1\u3092\u53d6\u308a\u7acb\u3066\u3089\u308c\u3066\u3044\u305f\u3068\u3053\u308d\u3001\u8b0e\u306e\u30ae\u30e3\u30eb\u304c\u73fe\u308c\u3066\u305d\u308c\u3089\u3092\u8fd4\u6e08\u3057\u3066\u304f\u308c\u308b\u3053\u3068\u306b\uff01 \u7aae\u5730\u3092\u8131\u3057\u305f\u795e\u9577\u5009\u3060\u3063\u305f\u304c\u3001\u30ae\u30e3\u30eb\u304c\u501f\u91d1\u3092\u80a9\u4ee3\u308f\u308a\u3059\u308b\u969b\u306b\u5f7c\u306b\u51fa\u3057\u305f\u6761\u4ef6\u306f\u2015\u2015\u3002"
|
||||
},
|
||||
"isLocked": false,
|
||||
"links": {
|
||||
"al": "159308",
|
||||
"ap": "boku-to-gal-ga-fuufu-ni-naru-made",
|
||||
"bw": "series\/431780\/list",
|
||||
"kt": "boku-to-gal-ga-fuufu-ni-naru-made",
|
||||
"mu": "tobfmjc",
|
||||
"amz": "https:\/\/www.amazon.co.jp\/dp\/B0CHMJMTNY",
|
||||
"ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/781303\/",
|
||||
"mal": "154486",
|
||||
"raw": "https:\/\/comic-walker.com\/detail\/KC_001788_S"
|
||||
},
|
||||
"originalLanguage": "ja",
|
||||
"lastVolume": "",
|
||||
"lastChapter": "",
|
||||
"publicationDemographic": "shounen",
|
||||
"status": "ongoing",
|
||||
"year": 2022,
|
||||
"contentRating": "suggestive",
|
||||
"tags": [
|
||||
{
|
||||
"id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Romance" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "4d32cc48-9f00-4cca-9b5a-a839f0764984",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Comedy" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "caaa44eb-cd40-4177-b930-79d3ef2afe87",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "School Life" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Slice of Life" },
|
||||
"description": {},
|
||||
"group": "genre",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
},
|
||||
{
|
||||
"id": "fad12b5e-68ba-460e-b933-9ae8318f5b65",
|
||||
"type": "tag",
|
||||
"attributes": {
|
||||
"name": { "en": "Gyaru" },
|
||||
"description": {},
|
||||
"group": "theme",
|
||||
"version": 1
|
||||
},
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"state": "published",
|
||||
"chapterNumbersResetOnNewVolume": false,
|
||||
"createdAt": "2022-12-25T17:05:32+00:00",
|
||||
"updatedAt": "2024-11-02T18:34:30+00:00",
|
||||
"version": 28,
|
||||
"availableTranslatedLanguages": [ "ru", "en", "es-la", "pt-br", "id", "tr", "vi" ],
|
||||
"latestUploadedChapter": "37170d3e-108c-4c5e-b287-ef2394d7a3e8"
|
||||
},
|
||||
"relationships": [
|
||||
{
|
||||
"id": "08ee3ef5-2878-46d4-9040-2aef23fabf74",
|
||||
"type": "author"
|
||||
},
|
||||
{
|
||||
"id": "08ee3ef5-2878-46d4-9040-2aef23fabf74",
|
||||
"type": "artist"
|
||||
},
|
||||
{
|
||||
"id": "4243e338-6306-4d6a-a6cc-b1fdcb30c7cb",
|
||||
"type": "cover_art",
|
||||
"attributes": {
|
||||
"description": "",
|
||||
"volume": "2",
|
||||
"fileName": "5b978b74-b18f-4039-a972-fa51d15e5d12.jpg",
|
||||
"locale": "ja",
|
||||
"createdAt": "2024-09-21T10:34:15+00:00",
|
||||
"updatedAt": "2024-09-21T10:34:15+00:00",
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e7dac780-4b88-4e0d-ac14-aa3b3a7f08a6",
|
||||
"type": "creator"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"limit": 5,
|
||||
"offset": 0,
|
||||
"total": 363
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using MangaReader.Core.HttpService;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Sources.MangaDex.Api;
|
||||
using MangaReader.Tests.Utilities;
|
||||
using NSubstitute;
|
||||
@@ -22,6 +22,116 @@ 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 mangaDexCollectionResponse = (mangaDexResponse as MangaDexCollectionResponse)!;
|
||||
mangaDexCollectionResponse.Data.Count.ShouldBe(3);
|
||||
|
||||
mangaDexCollectionResponse.Data[0].ShouldBeOfType<MangaEntity>();
|
||||
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 coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!;
|
||||
coverArtEntity.Attributes.ShouldNotBeNull();
|
||||
coverArtEntity.Attributes.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
|
||||
|
||||
mangaEntity.Attributes.Year.ShouldBe(2021);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_Manga_2()
|
||||
{
|
||||
string searchResultJson = await ReadJsonResourceAsync("Manga-Search-Response-2.json");
|
||||
|
||||
IHttpService httpService = Substitute.For<IHttpService>();
|
||||
|
||||
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
|
||||
.Returns(Task.FromResult(searchResultJson));
|
||||
|
||||
MangaDexClient mangaDexClient = new(httpService);
|
||||
MangaDexResponse? mangaDexResponse = await mangaDexClient.SearchMangaByTitleAsync("Some random text", CancellationToken.None);
|
||||
|
||||
// Testing here
|
||||
mangaDexResponse.ShouldNotBeNull();
|
||||
mangaDexResponse.Response.ShouldBe("collection");
|
||||
mangaDexResponse.ShouldBeOfType<MangaDexCollectionResponse>();
|
||||
|
||||
MangaDexCollectionResponse mangaDexCollectionResponse = (mangaDexResponse as MangaDexCollectionResponse)!;
|
||||
mangaDexCollectionResponse.Data.Count.ShouldBe(5);
|
||||
|
||||
mangaDexCollectionResponse.Data[3].ShouldBeOfType<MangaEntity>();
|
||||
MangaEntity mangaEntity = (mangaDexCollectionResponse.Data[3] as MangaEntity)!;
|
||||
|
||||
mangaEntity.Attributes.ShouldNotBeNull();
|
||||
|
||||
mangaEntity.Attributes.Title.ShouldContainKey("en");
|
||||
mangaEntity.Attributes.Title["en"].ShouldBe("Gal Yome no Himitsu");
|
||||
|
||||
mangaEntity.Attributes.Description.ShouldContainKey("en");
|
||||
mangaEntity.Attributes.Description["en"].ShouldBe("Fuyuki is a beautiful and cool gal! But there's a secret side of her that she only shows in front of her husband...?");
|
||||
|
||||
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 coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!;
|
||||
coverArtEntity.Attributes.ShouldNotBeNull();
|
||||
coverArtEntity.Attributes.FileName.ShouldBe("07d02b26-cbd0-4323-8774-9d83579863d5.jpg");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -154,6 +264,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<IHttpService>();
|
||||
|
||||
httpService.GetStringAsync(Arg.Any<string>(), 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>();
|
||||
|
||||
MangaDexCollectionResponse mangaDexEntityResponse = (mangaDexResponse as MangaDexCollectionResponse)!;
|
||||
|
||||
List<CoverArtEntity> coverArtEntities = [.. mangaDexEntityResponse.Data.Where(entity => entity is CoverArtEntity).Cast<CoverArtEntity>()];
|
||||
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<string> ReadJsonResourceAsync(string resourceName)
|
||||
{
|
||||
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.MangaDex.Api.{resourceName}");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Common;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Sources.MangaDex.Api;
|
||||
using MangaReader.Core.Sources.MangaDex.Metadata;
|
||||
using NSubstitute;
|
||||
@@ -11,7 +12,7 @@ public class MangaDexMetadataTests
|
||||
[Fact]
|
||||
public async Task Get_Manga()
|
||||
{
|
||||
MangaDexEntityResponse entityResponse = new()
|
||||
MangaDexEntityResponse mangaEntityResponse = new()
|
||||
{
|
||||
Result = "ok",
|
||||
Response = "entity",
|
||||
@@ -141,7 +142,7 @@ public class MangaDexMetadataTests
|
||||
}
|
||||
};
|
||||
|
||||
MangaDexCollectionResponse collectionResponse = new()
|
||||
MangaDexCollectionResponse feedCollectionResponse = new()
|
||||
{
|
||||
Result = "ok",
|
||||
Response = "collection",
|
||||
@@ -186,38 +187,68 @@ 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<IMangaDexClient>();
|
||||
|
||||
mangaDexClient.GetMangaAsync(Arg.Any<Guid>(), CancellationToken.None)
|
||||
.Returns(entityResponse);
|
||||
.Returns(mangaEntityResponse);
|
||||
|
||||
mangaDexClient.GetFeedAsync(Arg.Any<Guid>(), CancellationToken.None)
|
||||
.Returns(collectionResponse);
|
||||
.Returns(feedCollectionResponse);
|
||||
|
||||
mangaDexClient.GetCoverArtAsync(Arg.Any<Guid>(), 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);
|
||||
|
||||
sourceManga.ShouldNotBeNull();
|
||||
sourceManga.Title.ShouldBe("Gals Can’t Be Kind to Otaku!?");
|
||||
sourceManga.Title.Name.ShouldBe("Gals Can’t Be Kind to Otaku!?");
|
||||
|
||||
sourceManga.AlternateTitles.Count.ShouldBe(5);
|
||||
sourceManga.AlternateTitles.Length.ShouldBe(5);
|
||||
|
||||
sourceManga.AlternateTitles[0].Title.ShouldBe("オタクに優しいギャルはいない!?");
|
||||
sourceManga.AlternateTitles[0].Language.ShouldBe(SourceMangaLanguage.Japanese);
|
||||
sourceManga.AlternateTitles[0].Name.ShouldBe("オタクに優しいギャルはいない!?");
|
||||
sourceManga.AlternateTitles[0].Language.ShouldBe(Language.Japanese);
|
||||
|
||||
sourceManga.AlternateTitles[1].Title.ShouldBe("Otaku ni Yasashii Gal wa Inai!?");
|
||||
sourceManga.AlternateTitles[1].Language.ShouldBe(SourceMangaLanguage.Romanji);
|
||||
sourceManga.AlternateTitles[1].Name.ShouldBe("Otaku ni Yasashii Gal wa Inai!?");
|
||||
sourceManga.AlternateTitles[1].Language.ShouldBe(Language.Romaji);
|
||||
|
||||
sourceManga.AlternateTitles[2].Title.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?");
|
||||
sourceManga.AlternateTitles[2].Language.ShouldBe(SourceMangaLanguage.Romanji);
|
||||
sourceManga.AlternateTitles[2].Name.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?");
|
||||
sourceManga.AlternateTitles[2].Language.ShouldBe(Language.Romaji);
|
||||
|
||||
sourceManga.AlternateTitles[3].Title.ShouldBe("Gal Can't Be Kind to Otaku!?");
|
||||
sourceManga.AlternateTitles[3].Language.ShouldBe(SourceMangaLanguage.English);
|
||||
sourceManga.AlternateTitles[3].Name.ShouldBe("Gal Can't Be Kind to Otaku!?");
|
||||
sourceManga.AlternateTitles[3].Language.ShouldBe(Language.English);
|
||||
|
||||
sourceManga.AlternateTitles[4].Title.ShouldBe("Gals Can't Be Kind To A Geek!?");
|
||||
sourceManga.AlternateTitles[4].Language.ShouldBe(SourceMangaLanguage.English);
|
||||
sourceManga.AlternateTitles[4].Name.ShouldBe("Gals Can't Be Kind To A Geek!?");
|
||||
sourceManga.AlternateTitles[4].Language.ShouldBe(Language.English);
|
||||
|
||||
sourceManga.Genres.Count.ShouldBe(5);
|
||||
sourceManga.Genres.Length.ShouldBe(5);
|
||||
sourceManga.Genres[0].ShouldBe("Romance");
|
||||
sourceManga.Genres[1].ShouldBe("Comedy");
|
||||
sourceManga.Genres[2].ShouldBe("School Life");
|
||||
@@ -227,12 +258,12 @@ public class MangaDexMetadataTests
|
||||
sourceManga.Contributors.Length.ShouldBe(2);
|
||||
|
||||
sourceManga.Contributors[0].Name.ShouldBe("Norishiro-chan");
|
||||
sourceManga.Contributors[0].Role.ShouldBe(SourceMangaContributorRole.Author);
|
||||
sourceManga.Contributors[0].Role.ShouldBe(ContributorRole.Author);
|
||||
|
||||
sourceManga.Contributors[1].Name.ShouldBe("Sakana Uozimi");
|
||||
sourceManga.Contributors[1].Role.ShouldBe(SourceMangaContributorRole.Artist);
|
||||
sourceManga.Contributors[1].Role.ShouldBe(ContributorRole.Artist);
|
||||
|
||||
sourceManga.Chapters.Count.ShouldBe(3);
|
||||
sourceManga.Chapters.Length.ShouldBe(3);
|
||||
|
||||
sourceManga.Chapters[0].Volume.ShouldBeNull();
|
||||
sourceManga.Chapters[0].Number.ShouldBe(69);
|
||||
@@ -248,5 +279,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.CoverArtUrls.Length.ShouldBe(2);
|
||||
sourceManga.CoverArtUrls[0].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/2569ffd8-4ba1-4030-8d08-b7a21333a7a6.jpg");
|
||||
sourceManga.CoverArtUrls[1].ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/d2314e9b-4287-4e65-8045-b713d97c0b28.jpg");
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,36 @@
|
||||
using HtmlAgilityPack;
|
||||
using MangaReader.Core.Common;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Sources.MangaNato.Metadata;
|
||||
using MangaReader.Tests.Utilities;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
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]
|
||||
public async Task Get_Manga()
|
||||
{
|
||||
var webCrawler = new TestMangaNatoWebCrawler();
|
||||
var manga = await webCrawler.GetMangaAsync(mangaNatoSampleFilePath, CancellationToken.None);
|
||||
string mangaHtml = await ReadJsonResourceAsync("Manga-Response.html");
|
||||
|
||||
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.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",
|
||||
"Yankee Musume",
|
||||
"ヤンキー娘",
|
||||
@@ -49,7 +38,7 @@ public class UnitTest1
|
||||
|
||||
SourceMangaContributor[] expectedContributors =
|
||||
[
|
||||
new() { Name = "Nagaoka Taichi", Role = SourceMangaContributorRole.Author }
|
||||
new() { Name = "Nagaoka Taichi", Role = ContributorRole.Author }
|
||||
];
|
||||
|
||||
manga.Contributors.ShouldBeEquivalentTo(expectedContributors);
|
||||
@@ -61,11 +50,12 @@ public class UnitTest1
|
||||
manga.RatingPercent.ShouldBe(97);
|
||||
manga.Votes.ShouldBe(15979);
|
||||
|
||||
manga.Descriptions.Length.ShouldBe(1);
|
||||
//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.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935");
|
||||
manga.Descriptions[0].Name.ShouldStartWith("Ooyama-kun normally doesn’t get involved with Akutsu-san, a delinquent girl in his class");
|
||||
manga.Descriptions[0].Name.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935");
|
||||
|
||||
manga.Chapters.Count.ShouldBe(236);
|
||||
manga.Chapters.Length.ShouldBe(236);
|
||||
|
||||
manga.Chapters[0].Url.ShouldBe("https://chapmanganato.to/manga-hf984788/chapter-186");
|
||||
manga.Chapters[0].Number.ShouldBe(186);
|
||||
@@ -79,4 +69,9 @@ public class UnitTest1
|
||||
manga.Chapters[235].Views.ShouldBe(232_200);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using MangaReader.Core.HttpService;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Sources.NatoManga.Api;
|
||||
using MangaReader.Tests.Utilities;
|
||||
using NSubstitute;
|
||||
@@ -34,6 +34,9 @@ public class NatoMangaClientTests
|
||||
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
|
||||
.Returns(Task.FromResult(searchResultJson));
|
||||
|
||||
httpService.GetStringAsync(Arg.Any<string>(), Arg.Any<IDictionary<string,string>>(), CancellationToken.None)
|
||||
.Returns(Task.FromResult(searchResultJson));
|
||||
|
||||
NatoMangaClient natoMangaClient = new(httpService);
|
||||
NatoMangaSearchResult[] searchResults = await natoMangaClient.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
|
||||
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
using HtmlAgilityPack;
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Sources.NatoManga.Metadata;
|
||||
using MangaReader.Tests.Utilities;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace MangaReader.Tests.WebCrawlers.NatoManga;
|
||||
namespace MangaReader.Tests.Sources.NatoManga.Metadata;
|
||||
|
||||
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]
|
||||
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();
|
||||
var manga = await webCrawler.GetMangaAsync(sampleFilePath, CancellationToken.None);
|
||||
IHttpService httpService = Substitute.For<IHttpService>();
|
||||
|
||||
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.Title.ShouldBe("Gal Can’t Be Kind to Otaku!?");
|
||||
manga.Title.Name.ShouldBe("Gal Can’t Be Kind to Otaku!?");
|
||||
|
||||
//manga.AlternateTitles.ShouldBe([
|
||||
// "Kaette kudasai! Akutsu-san",
|
||||
@@ -49,7 +46,7 @@ public class NatoMangaWebCrawlerTests
|
||||
//manga.Description.ShouldStartWith("Ooyama-kun normally doesn’t 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.Chapters.Count.ShouldBe(83);
|
||||
manga.Chapters.Length.ShouldBe(83);
|
||||
|
||||
manga.Chapters[0].Url.ShouldBe("https://www.natomanga.com/manga/gal-cant-be-kind-to-otaku/chapter-69");
|
||||
manga.Chapters[0].Number.ShouldBe(69);
|
||||
@@ -63,4 +60,9 @@ public class NatoMangaWebCrawlerTests
|
||||
//manga.Chapters[235].Views.ShouldBe(232_200);
|
||||
//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}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MangaReader.Core.Http;
|
||||
using MangaReader.Core.Sources.NatoManga.Pages;
|
||||
using MangaReader.Tests.Utilities;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace MangaReader.Tests.Sources.NatoManga.Pages;
|
||||
|
||||
public class NatoMangaPageTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Get_Pages()
|
||||
{
|
||||
string mangaHtml = await ReadJsonResourceAsync("Manga-Chapter-Response.html");
|
||||
|
||||
IHttpService httpService = Substitute.For<IHttpService>();
|
||||
|
||||
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
|
||||
.Returns(Task.FromResult(mangaHtml));
|
||||
|
||||
HtmlLoader htmlLoader = new(httpService);
|
||||
|
||||
NatoMangaPageProvider pageProvider = new(htmlLoader);
|
||||
IReadOnlyList<string> pageImageUrls = await pageProvider.GetPageImageUrlsAsync("/test-url", CancellationToken.None);
|
||||
|
||||
pageImageUrls.Count.ShouldBe(13);
|
||||
pageImageUrls[0].ShouldBe("https://img-r1.2xstorage.com/gal-cant-be-kind-to-otaku/69/0.webp");
|
||||
pageImageUrls[12].ShouldBe("https://img-r1.2xstorage.com/gal-cant-be-kind-to-otaku/69/12.webp");
|
||||
}
|
||||
|
||||
private static async Task<string> ReadJsonResourceAsync(string resourceName)
|
||||
{
|
||||
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.NatoManga.Pages.{resourceName}");
|
||||
}
|
||||
}
|
||||
38
MangaReader.Tests/Utilities/TestDbContextFactory.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using MangaReader.Core.Data;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MangaReader.Tests.Utilities;
|
||||
|
||||
public class TestDbContextFactory : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly DbContextOptions<MangaContext> _options;
|
||||
|
||||
public TestDbContextFactory()
|
||||
{
|
||||
_connection = new SqliteConnection("DataSource=:memory:");
|
||||
_connection.Open();
|
||||
|
||||
_options = new DbContextOptionsBuilder<MangaContext>()
|
||||
.UseSqlite(_connection)
|
||||
.EnableSensitiveDataLogging() // Optional: helps with debugging
|
||||
.Options;
|
||||
|
||||
using MangaContext context = new(_options);
|
||||
context.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public MangaContext CreateContext()
|
||||
{
|
||||
return new MangaContext(_options);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Close();
|
||||
_connection.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
20
MangaReader.WinUI/App.xaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application
|
||||
x:Class="MangaReader.WinUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:MangaReader.WinUI">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="/Resources/Converters.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Fonts.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Styles.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/ViewModels.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
49
MangaReader.WinUI/App.xaml.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using MangaReader.Core.Data;
|
||||
using MangaReader.WinUI.ViewModels;
|
||||
using MangaReader.WinUI.Views;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace MangaReader.WinUI;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _window;
|
||||
|
||||
public static IServiceProvider ServiceProvider { get; private set; }
|
||||
|
||||
static App()
|
||||
{
|
||||
ServiceCollection services = new();
|
||||
|
||||
services.AddSingleton<MainWindow>();
|
||||
|
||||
services.AddSingleton<MainViewModel>();
|
||||
services.AddSingleton<SearchViewModel>();
|
||||
services.AddSingleton<LibraryViewModel>();
|
||||
|
||||
services.AddMangaReader();
|
||||
|
||||
ServiceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Ensure the database is created
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<MangaContext>();
|
||||
dbContext.Database.EnsureCreated();
|
||||
dbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
_window = ServiceProvider.GetRequiredService<MainWindow>();
|
||||
_window.Activate();
|
||||
}
|
||||
}
|
||||
BIN
MangaReader.WinUI/Assets/Fonts/Poppins-Medium.otf
Normal file
BIN
MangaReader.WinUI/Assets/Fonts/Poppins-Regular.otf
Normal file
BIN
MangaReader.WinUI/Assets/Fonts/Poppins-SemiBold.otf
Normal file
BIN
MangaReader.WinUI/Assets/Images/MangaReader.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
MangaReader.WinUI/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
MangaReader.WinUI/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
MangaReader.WinUI/Assets/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
MangaReader.WinUI/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
MangaReader.WinUI/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
MangaReader.WinUI/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
20
MangaReader.WinUI/Converters/UppercaseConverter.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
|
||||
namespace MangaReader.WinUI.Converters;
|
||||
|
||||
public partial class UppercaseConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
return value?.ToString()?.ToUpperInvariant();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
34
MangaReader.WinUI/MainWindow.xaml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window
|
||||
x:Class="MangaReader.WinUI.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:MangaReader.WinUI"
|
||||
xmlns:views="using:MangaReader.WinUI.Views"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="MangaReader.WinUI">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<!-- Title Bar -->
|
||||
<Grid x:Name="AppTitleBar" Grid.Row="0" VerticalAlignment="Center" Padding="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Grid.Column="0" x:Name="TitleBarIcon" Source="ms-appx:///Assets/Images/MangaReader.png" Width="20" Height="20" Margin="0 0 10 0" />
|
||||
<TextBlock Grid.Column="1" x:Name="AppTitle" Text="{x:Bind Title, Mode=OneWay}" Style="{StaticResource CaptionTextBlockStyle}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<!-- Main View -->
|
||||
<views:MainView Grid.Row="1"></views:MainView>
|
||||
</Grid>
|
||||
</Window>
|
||||
17
MangaReader.WinUI/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace MangaReader.WinUI;
|
||||
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
private const string ApplicationTitle = "Manga Reader";
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = ApplicationTitle;
|
||||
ExtendsContentIntoTitleBar = true; // enable custom titlebar
|
||||
SetTitleBar(AppTitleBar); // set user ui element as titlebar
|
||||
}
|
||||
}
|
||||
103
MangaReader.WinUI/MangaReader.WinUI.csproj
Normal file
@@ -0,0 +1,103 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>MangaReader.WinUI</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<Nullable>enable</Nullable>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Fonts\Poppins-Medium.otf" />
|
||||
<None Remove="Assets\Fonts\Poppins-Regular.otf" />
|
||||
<None Remove="Assets\Fonts\Poppins-SemiBold.otf" />
|
||||
<None Remove="Assets\Images\MangaReader.png" />
|
||||
<None Remove="Resources\ViewModels.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="Assets\StoreLogo.png" />
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
<Content Include="MangaReader.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="SkiaSharp" Version="3.119.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MangaReader.Core\MangaReader.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Resources\Converters.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Resources\Styles.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Resources\Fonts.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Views\MainView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Views\LibraryView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Views\SearchView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Resources\ViewModels.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Publish Properties -->
|
||||
<PropertyGroup>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
<ApplicationIcon>MangaReader.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
BIN
MangaReader.WinUI/MangaReader.ico
Normal file
|
After Width: | Height: | Size: 206 KiB |
51
MangaReader.WinUI/Package.appxmanifest
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="cdba7326-1a2b-45f0-a233-463a21c91c91"
|
||||
Publisher="CN=Brian"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="cdba7326-1a2b-45f0-a233-463a21c91c91" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>MangaReader.WinUI</DisplayName>
|
||||
<PublisherDisplayName>Brian</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="MangaReader.WinUI"
|
||||
Description="MangaReader.WinUI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
10
MangaReader.WinUI/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"MangaReader.WinUI (Package)": {
|
||||
"commandName": "MsixPackage"
|
||||
},
|
||||
"MangaReader.WinUI (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
MangaReader.WinUI/Resources/Converters.xaml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:MangaReader.WinUI.Converters">
|
||||
|
||||
<converters:UppercaseConverter x:Key="UppercaseConverter" />
|
||||
|
||||
</ResourceDictionary>
|
||||
10
MangaReader.WinUI/Resources/Fonts.xaml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<FontFamily x:Key="PoppinsRegular">ms-appx:///Assets/Fonts/Poppins-Regular.otf#Poppins Regular</FontFamily>
|
||||
<FontFamily x:Key="PoppinsMedium">ms-appx:///Assets/Fonts/Poppins-Medium.otf#Poppins Medium</FontFamily>
|
||||
<FontFamily x:Key="PoppinsSemiBold">ms-appx:///Assets/Fonts/Poppins-SemiBold.otf#Poppins SemiBold</FontFamily>
|
||||
|
||||
</ResourceDictionary>
|
||||
13
MangaReader.WinUI/Resources/Styles.xaml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:Media="using:CommunityToolkit.WinUI.Media">
|
||||
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource PoppinsRegular}" />
|
||||
</Style>
|
||||
|
||||
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
|
||||
|
||||
</ResourceDictionary>
|
||||
9
MangaReader.WinUI/Resources/ViewModels.xaml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:MangaReader.WinUI.ViewModels">
|
||||
|
||||
<vm:ViewModelLocator x:Key="Locator" />
|
||||
|
||||
</ResourceDictionary>
|
||||
121
MangaReader.WinUI/ViewModels/LibraryViewModel.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MangaReader.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace MangaReader.WinUI.ViewModels;
|
||||
|
||||
public partial class LibraryViewModel(MangaContext context) : ViewModelBase
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
private string? _keyword;
|
||||
|
||||
public string? Keyword
|
||||
{
|
||||
get
|
||||
{
|
||||
return _keyword;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _keyword, value);
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableCollection<ObservableMangaItem> _mangaItems = [];
|
||||
|
||||
public ObservableCollection<ObservableMangaItem> MangaItems
|
||||
{
|
||||
get
|
||||
{
|
||||
return _mangaItems;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _mangaItems, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync);
|
||||
//public ICommand ImportCommand => new AsyncRelayCommand(ImportAsync);
|
||||
|
||||
public async Task SearchAsync()
|
||||
{
|
||||
//if (string.IsNullOrWhiteSpace(Keyword))
|
||||
// return;
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
ObservableMangaItem[] mangaItems = await GetMangaItemsAsync(_cancellationTokenSource.Token);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
|
||||
{
|
||||
MangaItems = new(mangaItems);
|
||||
});
|
||||
|
||||
//MangaItems = new(mangaItems);
|
||||
}
|
||||
|
||||
public async Task<ObservableMangaItem[]> GetMangaItemsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Manga[] mangas = await context.Mangas
|
||||
.Include(x => x.Titles)
|
||||
.Include(x => x.Descriptions)
|
||||
.Include(x => x.Genres).ThenInclude(x => x.Genre)
|
||||
//.Include(x => x.Sources)
|
||||
//.ThenInclude(x => x.Chapters)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
List<ObservableMangaItem> mangaItems = [];
|
||||
|
||||
foreach (Manga manga in mangas)
|
||||
{
|
||||
ObservableMangaItem mangaItem = new()
|
||||
{
|
||||
MangaId = manga.MangaId,
|
||||
Title = manga.Titles.OrderByDescending(title => title.IsPrimary).FirstOrDefault()?.Name,
|
||||
Description = manga.Descriptions.FirstOrDefault()?.Text,
|
||||
Genres = [.. manga.Genres.Select(x => x.Genre.Name)]
|
||||
};
|
||||
|
||||
mangaItems.Add(mangaItem);
|
||||
}
|
||||
|
||||
return [.. mangaItems];
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ObservableMangaItem : ObservableObject
|
||||
{
|
||||
public int MangaId { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Thumbnail { get; init; }
|
||||
public string[] Genres { get; init; } = [];
|
||||
|
||||
private BitmapImage? _thumbnailBitmap;
|
||||
|
||||
public BitmapImage? ThumbnailBitmap
|
||||
{
|
||||
get
|
||||
{
|
||||
return _thumbnailBitmap;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _thumbnailBitmap, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
MangaReader.WinUI/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Pipeline;
|
||||
using MangaReader.Core.Search;
|
||||
using MangaReader.WinUI.Views;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace MangaReader.WinUI.ViewModels;
|
||||
|
||||
public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
private string? _keyword;
|
||||
|
||||
public string? Keyword
|
||||
{
|
||||
get
|
||||
{
|
||||
return _keyword;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _keyword, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
218
MangaReader.WinUI/ViewModels/SearchViewModel.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MangaReader.Core.Metadata;
|
||||
using MangaReader.Core.Pipeline;
|
||||
using MangaReader.Core.Search;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace MangaReader.WinUI.ViewModels;
|
||||
|
||||
public partial class SearchViewModel(
|
||||
IMangaSearchCoordinator searchCoordinator,
|
||||
IMangaMetadataCoordinator metadataCoordinator,
|
||||
IMangaPipeline pipeline) : ViewModelBase
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
private string? _keyword;
|
||||
|
||||
public string? Keyword
|
||||
{
|
||||
get
|
||||
{
|
||||
return _keyword;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _keyword, value);
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableCollection<MangaSearchResult> _searchResults = [];
|
||||
|
||||
public ObservableCollection<MangaSearchResult> SearchResults
|
||||
{
|
||||
get
|
||||
{
|
||||
return _searchResults;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _searchResults, value);
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableCollection<ObservableMangaSearchResult> _searchResults2 = [];
|
||||
|
||||
public ObservableCollection<ObservableMangaSearchResult> SearchResults2
|
||||
{
|
||||
get
|
||||
{
|
||||
return _searchResults2;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _searchResults2, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync);
|
||||
//public ICommand ImportCommand => new AsyncRelayCommand(ImportAsync);
|
||||
|
||||
public async Task SearchAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Keyword))
|
||||
return;
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
Dictionary<string, MangaSearchResult[]> result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token);
|
||||
|
||||
List<MangaSearchResult> searchResults = [];
|
||||
List<ObservableMangaSearchResult> mangaSearchResults = [];
|
||||
|
||||
foreach (var item in result)
|
||||
{
|
||||
foreach (MangaSearchResult searchResult in item.Value)
|
||||
{
|
||||
//searchResults.Add(searchResult);
|
||||
|
||||
ObservableMangaSearchResult mangaSearchResult = new()
|
||||
{
|
||||
Source = searchResult.Source,
|
||||
Url = searchResult.Url,
|
||||
Title = searchResult.Title,
|
||||
Thumbnail = searchResult.Thumbnail,
|
||||
Description = searchResult.Description,
|
||||
Genres = searchResult.Genres
|
||||
};
|
||||
|
||||
Task.Run(() => mangaSearchResult.LoadThumbnailAsync(_dispatcherQueue)); // or defer this if you want lazy loading
|
||||
|
||||
searchResults.Add(searchResult);
|
||||
mangaSearchResults.Add(mangaSearchResult);
|
||||
}
|
||||
}
|
||||
|
||||
SearchResults = new(searchResults);
|
||||
SearchResults2 = new(mangaSearchResults);
|
||||
}
|
||||
|
||||
public static async Task<BitmapImage?> LoadWebpAsBitmapImageAsync(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return null;
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
|
||||
using var webpStream = await httpClient.GetStreamAsync(url);
|
||||
|
||||
using var image = await Image.LoadAsync(webpStream); // from SixLabors.ImageSharp
|
||||
using var ms = new MemoryStream();
|
||||
//await image.SaveAsPngAsync(ms); // Convert to PNG in memory
|
||||
await image.SaveAsJpegAsync(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
var bitmap = new BitmapImage();
|
||||
await bitmap.SetSourceAsync(ms.AsRandomAccessStream());
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public async Task ImportAsync(ObservableMangaSearchResult searchResult, CancellationToken cancellationToken)
|
||||
{
|
||||
IMangaMetadataProvider metadataProvider = metadataCoordinator.GetProvider(searchResult.Source);
|
||||
SourceManga? sourceManga = await metadataProvider.GetMangaAsync(searchResult.Url, cancellationToken);
|
||||
|
||||
if (sourceManga == null)
|
||||
return;
|
||||
|
||||
MangaMetadataPipelineRequest request = new()
|
||||
{
|
||||
SourceName = searchResult.Source,
|
||||
SourceUrl = searchResult.Url,
|
||||
SourceManga = sourceManga,
|
||||
};
|
||||
|
||||
await pipeline.RunMetadataAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ObservableMangaSearchResult : ObservableObject
|
||||
{
|
||||
public required string Source { get; init; }
|
||||
public required string Url { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Thumbnail { get; init; }
|
||||
public string[] Genres { get; init; } = [];
|
||||
|
||||
private BitmapImage? _thumbnailBitmap;
|
||||
|
||||
public BitmapImage? ThumbnailBitmap
|
||||
{
|
||||
get
|
||||
{
|
||||
return _thumbnailBitmap;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _thumbnailBitmap, value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadThumbnailAsync(DispatcherQueue dispatchQueue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Thumbnail))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0");
|
||||
|
||||
using var stream = await httpClient.GetStreamAsync(Thumbnail);
|
||||
using var image = await Image.LoadAsync(stream); // Important: use a pixel type
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(ms); // or SaveAsPngAsync
|
||||
ms.Position = 0;
|
||||
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
|
||||
dispatchQueue.TryEnqueue(async () => {
|
||||
var bitmap = new BitmapImage();
|
||||
await bitmap.SetSourceAsync(ms.AsRandomAccessStream());
|
||||
|
||||
ThumbnailBitmap = bitmap;
|
||||
|
||||
taskCompletionSource.SetResult();
|
||||
});
|
||||
|
||||
taskCompletionSource.Task.GetAwaiter().GetResult();
|
||||
|
||||
//var bitmap = new BitmapImage();
|
||||
//await bitmap.SetSourceAsync(ms.AsRandomAccessStream());
|
||||
|
||||
//ThumbnailBitmap = bitmap;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Thumbnail Load Failed] {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
8
MangaReader.WinUI/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace MangaReader.WinUI.ViewModels;
|
||||
|
||||
public partial class ViewModelBase : ObservableObject
|
||||
{
|
||||
|
||||
}
|
||||
15
MangaReader.WinUI/ViewModels/ViewModelLocator.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MangaReader.WinUI.ViewModels;
|
||||
|
||||
public class ViewModelLocator
|
||||
{
|
||||
public static MainViewModel MainViewModel
|
||||
=> App.ServiceProvider.GetRequiredService<MainViewModel>();
|
||||
|
||||
public static SearchViewModel SearchViewModel
|
||||
=> App.ServiceProvider.GetRequiredService<SearchViewModel>();
|
||||
|
||||
public static LibraryViewModel LibraryViewModel
|
||||
=> App.ServiceProvider.GetRequiredService<LibraryViewModel>();
|
||||
}
|
||||
97
MangaReader.WinUI/Views/LibraryView.xaml
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Page
|
||||
x:Class="MangaReader.WinUI.Views.LibraryView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:MangaReader.WinUI.Views"
|
||||
xmlns:vm="using:MangaReader.WinUI.ViewModels"
|
||||
xmlns:search="using:MangaReader.Core.Search"
|
||||
xmlns:UI="using:CommunityToolkit.WinUI"
|
||||
xmlns:Controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:Media="using:CommunityToolkit.WinUI.Media"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
DataContext="{Binding Source={StaticResource Locator}, Path=LibraryViewModel}"
|
||||
d:DataContext="{d:DesignInstance Type=vm:LibraryViewModel, IsDesignTimeCreatable=True}"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
|
||||
|
||||
<DataTemplate x:Key="MangaItemTemplate" x:DataType="vm:ObservableMangaItem">
|
||||
<Grid Padding="20" ColumnSpacing="20" Height="400" VerticalAlignment="Stretch" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}" VerticalAlignment="Top" HorizontalAlignment="Left">
|
||||
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8">
|
||||
<!--<Image Source="{x:Bind ThumbnailBitmap, Mode=OneWay}" MaxWidth="300"></Image>-->
|
||||
<Canvas Background="#19000000"></Canvas>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid Grid.Column="1" RowSpacing="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Border>
|
||||
<TextBlock Grid.Row="0" Text="{x:Bind Title, Mode=OneTime}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock>
|
||||
</Border>
|
||||
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind Genres, Mode=OneTime}" ItemTemplate="{StaticResource GenreTemplate}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Controls:WrapPanel Orientation="Horizontal" HorizontalSpacing="10" VerticalSpacing="10" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<TextBlock Text="{x:Bind Description}" Foreground="{StaticResource TextFillColorSecondaryBrush}" FontSize="16" TextWrapping="Wrap" LineStackingStrategy="BlockLineHeight" LineHeight="22"></TextBlock>
|
||||
</ScrollViewer>
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<!--<Button
|
||||
Content="Import"
|
||||
MinWidth="100"
|
||||
Tag="{x:Bind}"
|
||||
Click="Button_Click">
|
||||
</Button>-->
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GenreTemplate" x:DataType="x:String">
|
||||
<Border>
|
||||
<TextBlock FontSize="12" Foreground="{StaticResource TextFillColorTertiary}" Text="{x:Bind Mode=OneTime, Converter={StaticResource UppercaseConverter}}" FontFamily="{StaticResource PoppinsSemiBold}"></TextBlock>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
</Page.Resources>
|
||||
<Grid Padding="0" RowSpacing="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Border HorizontalAlignment="Stretch">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Padding="10">
|
||||
<TextBox Text="{Binding Keyword, Mode=TwoWay}" Width="300"></TextBox>
|
||||
<Button Content="Search" Command="{Binding SearchCommand}"></Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1" RenderTransformOrigin=".5,.5" Padding="50">
|
||||
<ScrollViewer.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||
</ScrollViewer.RenderTransform>
|
||||
<ItemsRepeater ItemsSource="{Binding MangaItems, Mode=OneWay}" ItemTemplate="{StaticResource MangaItemTemplate}">
|
||||
<ItemsRepeater.Layout>
|
||||
<UniformGridLayout MinRowSpacing="50" MinColumnSpacing="50" ItemsStretch="Fill" MinItemWidth="800"></UniformGridLayout>
|
||||
</ItemsRepeater.Layout>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
23
MangaReader.WinUI/Views/LibraryView.xaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using MangaReader.WinUI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MangaReader.WinUI.Views;
|
||||
|
||||
public sealed partial class LibraryView : Page
|
||||
{
|
||||
private readonly LibraryViewModel viewModel;
|
||||
|
||||
public LibraryView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
viewModel = (LibraryViewModel)DataContext;
|
||||
}
|
||||
|
||||
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Task.Run(viewModel.SearchAsync);
|
||||
}
|
||||
}
|
||||
26
MangaReader.WinUI/Views/MainView.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<UserControl
|
||||
x:Class="MangaReader.WinUI.Views.MainView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:MangaReader.WinUI.Views"
|
||||
xmlns:vm="using:MangaReader.WinUI.ViewModels"
|
||||
xmlns:search="using:MangaReader.Core.Search"
|
||||
xmlns:UI="using:CommunityToolkit.WinUI"
|
||||
xmlns:Controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:Media="using:CommunityToolkit.WinUI.Media"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
DataContext="{Binding Source={StaticResource Locator}, Path=MainViewModel}"
|
||||
d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<NavigationView IsBackEnabled="False" IsBackButtonVisible="Collapsed" SelectionChanged="nvSample_SelectionChanged">
|
||||
<NavigationView.MenuItems>
|
||||
<NavigationViewItem Icon="Library" Content="Library" Tag="Library" />
|
||||
<NavigationViewItem Icon="Find" Content="Search" Tag="Search" />
|
||||
</NavigationView.MenuItems>
|
||||
<Frame x:Name="ContentFrame"/>
|
||||
</NavigationView>
|
||||
|
||||
</UserControl>
|
||||
50
MangaReader.WinUI/Views/MainView.xaml.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using MangaReader.WinUI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MangaReader.WinUI.Views;
|
||||
|
||||
public sealed partial class MainView : UserControl
|
||||
{
|
||||
private readonly MainViewModel viewModel;
|
||||
|
||||
private readonly List<(string Tag, Type ViewType)> _tagViewMap =
|
||||
[
|
||||
("Search", typeof(SearchView)),
|
||||
("Library", typeof(LibraryView))
|
||||
];
|
||||
|
||||
public MainView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
viewModel = (MainViewModel)DataContext;
|
||||
}
|
||||
|
||||
private void nvSample_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.SelectedItem is not NavigationViewItem navigationViewItem)
|
||||
return;
|
||||
|
||||
if (navigationViewItem.Tag is not string tagName)
|
||||
return;
|
||||
|
||||
UpdateContent(tagName, null);
|
||||
}
|
||||
|
||||
private void UpdateContent(string tag, NavigationTransitionInfo? transitionInfo)
|
||||
{
|
||||
Type? viewType = _tagViewMap.FirstOrDefault(x => x.Tag == tag).ViewType;
|
||||
|
||||
if (viewType == null)
|
||||
return;
|
||||
|
||||
ContentFrame.Navigate(viewType);
|
||||
}
|
||||
}
|
||||
95
MangaReader.WinUI/Views/SearchView.xaml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Page
|
||||
x:Class="MangaReader.WinUI.Views.SearchView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:MangaReader.WinUI.Views"
|
||||
xmlns:vm="using:MangaReader.WinUI.ViewModels"
|
||||
xmlns:search="using:MangaReader.Core.Search"
|
||||
xmlns:UI="using:CommunityToolkit.WinUI"
|
||||
xmlns:Controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:Media="using:CommunityToolkit.WinUI.Media"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
DataContext="{Binding Source={StaticResource Locator}, Path=SearchViewModel}"
|
||||
d:DataContext="{d:DesignInstance Type=vm:SearchViewModel, IsDesignTimeCreatable=True}"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
|
||||
|
||||
<DataTemplate x:Key="MangaSearchResultTemplate" x:DataType="vm:ObservableMangaSearchResult">
|
||||
<Grid Padding="20" ColumnSpacing="20" Height="400" VerticalAlignment="Stretch" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}" VerticalAlignment="Top" HorizontalAlignment="Left">
|
||||
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8">
|
||||
<Image Source="{x:Bind ThumbnailBitmap, Mode=OneWay}" MaxWidth="300"></Image>
|
||||
<Canvas Background="#19000000"></Canvas>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid Grid.Column="1" RowSpacing="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Border>
|
||||
<TextBlock Grid.Row="0" Text="{x:Bind Title, Mode=OneTime}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock>
|
||||
</Border>
|
||||
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind Genres, Mode=OneTime}" ItemTemplate="{StaticResource GenreTemplate}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Controls:WrapPanel Orientation="Horizontal" HorizontalSpacing="10" VerticalSpacing="10" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<TextBlock Text="{x:Bind Description}" Foreground="{StaticResource TextFillColorSecondaryBrush}" FontSize="16" TextWrapping="Wrap" LineStackingStrategy="BlockLineHeight" LineHeight="22"></TextBlock>
|
||||
</ScrollViewer>
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button
|
||||
Content="Import"
|
||||
MinWidth="100"
|
||||
Tag="{x:Bind}"
|
||||
Click="Button_Click">
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GenreTemplate" x:DataType="x:String">
|
||||
<Border>
|
||||
<TextBlock FontSize="12" Foreground="{StaticResource TextFillColorTertiary}" Text="{x:Bind Mode=OneTime, Converter={StaticResource UppercaseConverter}}" FontFamily="{StaticResource PoppinsSemiBold}"></TextBlock>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
</Page.Resources>
|
||||
<Grid Padding="0" RowSpacing="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Border HorizontalAlignment="Stretch">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Padding="10">
|
||||
<TextBox Text="{Binding Keyword, Mode=TwoWay}" Width="300"></TextBox>
|
||||
<Button Content="Search" Command="{Binding SearchCommand}"></Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1" RenderTransformOrigin=".5,.5" Padding="50">
|
||||
<ScrollViewer.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1" />
|
||||
</ScrollViewer.RenderTransform>
|
||||
<ItemsRepeater ItemsSource="{Binding SearchResults2, Mode=OneWay}" ItemTemplate="{StaticResource MangaSearchResultTemplate}">
|
||||
<ItemsRepeater.Layout>
|
||||
<UniformGridLayout MinRowSpacing="50" MinColumnSpacing="50" ItemsStretch="Fill" MinItemWidth="800"></UniformGridLayout>
|
||||
</ItemsRepeater.Layout>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
30
MangaReader.WinUI/Views/SearchView.xaml.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using MangaReader.WinUI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MangaReader.WinUI.Views;
|
||||
|
||||
public sealed partial class SearchView : Page
|
||||
{
|
||||
private readonly SearchViewModel viewModel;
|
||||
|
||||
public SearchView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
viewModel = (SearchViewModel)DataContext;
|
||||
}
|
||||
|
||||
private async void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button button)
|
||||
return;
|
||||
|
||||
if (button.Tag is not ObservableMangaSearchResult searchResult)
|
||||
return;
|
||||
|
||||
await viewModel.ImportAsync(searchResult, CancellationToken.None);
|
||||
}
|
||||
}
|
||||