diff --git a/MangaReader.Core/Data/Manga.cs b/MangaReader.Core/Data/Manga.cs index e682811..dc3ad08 100644 --- a/MangaReader.Core/Data/Manga.cs +++ b/MangaReader.Core/Data/Manga.cs @@ -11,5 +11,5 @@ public class Manga public virtual ICollection Sources { get; set; } = []; public virtual ICollection Contributors { get; set; } = []; public virtual ICollection Genres { get; set; } = []; - public virtual ICollection Chapters { get; set; } = []; + //public virtual ICollection Chapters { get; set; } = []; } \ No newline at end of file diff --git a/MangaReader.Core/Data/MangaContext.cs b/MangaReader.Core/Data/MangaContext.cs index 2dde88d..f9f1624 100644 --- a/MangaReader.Core/Data/MangaContext.cs +++ b/MangaReader.Core/Data/MangaContext.cs @@ -14,9 +14,11 @@ public class MangaContext(DbContextOptions options) : DbContext(options) public DbSet MangaContributors { get; set; } public DbSet Genres { get; set; } public DbSet MangaGenres { get; set; } - public DbSet MangaChapters { get; set; } - public DbSet ChapterSources { get; set; } - public DbSet ChapterPages { get; set; } + //public DbSet MangaChapters { get; set; } + //public DbSet ChapterSources { get; set; } + //public DbSet ChapterPages { get; set; } + public DbSet SourceChapters { get; set; } + public DbSet SourcePages { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -32,9 +34,11 @@ public class MangaContext(DbContextOptions options) : DbContext(options) ConfigureMangaContributor(modelBuilder); ConfigureGenre(modelBuilder); ConfigureMangaGenre(modelBuilder); - ConfigureMangaChapter(modelBuilder); - ConfigureChapterSource(modelBuilder); - ConfigureChapterPage(modelBuilder); + //ConfigureMangaChapter(modelBuilder); + //ConfigureChapterSource(modelBuilder); + //ConfigureChapterPage(modelBuilder); + ConfigureSourceChapter(modelBuilder); + ConfigureSourcePage(modelBuilder); } private static void ConfigureManga(ModelBuilder modelBuilder) @@ -152,7 +156,11 @@ public class MangaContext(DbContextOptions options) : DbContext(options) { modelBuilder .Entity() - .HasKey(mangaSource => new { mangaSource.MangaId, mangaSource.SourceId }); + .HasKey(mangaSource => mangaSource.MangaSourceId); + + //modelBuilder + // .Entity() + // .HasKey(mangaSource => new { mangaSource.MangaId, mangaSource.SourceId }); modelBuilder.Entity() .HasIndex(x => x.Url) @@ -218,50 +226,88 @@ public class MangaContext(DbContextOptions options) : DbContext(options) .OnDelete(DeleteBehavior.Cascade); } - private static void ConfigureMangaChapter(ModelBuilder modelBuilder) + //private static void ConfigureMangaChapter(ModelBuilder modelBuilder) + //{ + // modelBuilder + // .Entity() + // .HasKey(x => x.MangaChapterId); + + // modelBuilder + // .Entity() + // .HasOne(x => x.Manga) + // .WithMany(x => x.Chapters) + // .HasForeignKey(x => x.MangaId) + // .OnDelete(DeleteBehavior.Cascade); + //} + + //private static void ConfigureChapterSource(ModelBuilder modelBuilder) + //{ + // modelBuilder + // .Entity() + // .HasKey(chapterSource => new { chapterSource.MangaChapterId, chapterSource.SourceId }); + + // modelBuilder + // .Entity() + // .HasOne(x => x.Chapter) + // .WithMany(x => x.Sources) + // .HasForeignKey(x => x.MangaChapterId) + // .OnDelete(DeleteBehavior.Cascade); + //} + + //private static void ConfigureChapterPage(ModelBuilder modelBuilder) + //{ + // modelBuilder + // .Entity() + // .HasKey(chapterPage => chapterPage.ChapterPageId); + + // modelBuilder + // .Entity() + // .HasIndex(chapterPage => new { chapterPage.MangaChapterId, chapterPage.PageNumber }) + // .IsUnique(true); + + // modelBuilder + // .Entity() + // .HasOne(x => x.MangaChapter) + // .WithMany(x => x.Pages) + // .HasForeignKey(x => x.MangaChapterId) + // .OnDelete(DeleteBehavior.Cascade); + //} + + private static void ConfigureSourceChapter(ModelBuilder modelBuilder) { modelBuilder - .Entity() - .HasKey(x => x.MangaChapterId); + .Entity() + .HasKey(sourceChapter => sourceChapter.SourceChapterId); modelBuilder - .Entity() - .HasOne(x => x.Manga) - .WithMany(x => x.Chapters) - .HasForeignKey(x => x.MangaId) - .OnDelete(DeleteBehavior.Cascade); - } - - private static void ConfigureChapterSource(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasKey(chapterSource => new { chapterSource.MangaChapterId, chapterSource.SourceId }); - - modelBuilder - .Entity() - .HasOne(x => x.Chapter) - .WithMany(x => x.Sources) - .HasForeignKey(x => x.MangaChapterId) - .OnDelete(DeleteBehavior.Cascade); - } - - private static void ConfigureChapterPage(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasKey(chapterPage => chapterPage.ChapterPageId); - - modelBuilder - .Entity() - .HasIndex(chapterPage => new { chapterPage.MangaChapterId, chapterPage.PageNumber }) + .Entity() + .HasIndex(sourceChapter => new { sourceChapter.MangaSourceId, sourceChapter.ChapterNumber, sourceChapter.Url }) .IsUnique(true); modelBuilder - .Entity() - .HasOne(x => x.MangaChapter) + .Entity() + .HasOne(x => x.MangaSource) + .WithMany(x => x.Chapters) + .HasForeignKey(x => x.MangaSourceId) + .OnDelete(DeleteBehavior.Cascade); + } + + private static void ConfigureSourcePage(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasKey(sourcePage => sourcePage.SourcePageId); + + modelBuilder + .Entity() + .HasIndex(sourcePage => new { sourcePage.SourceChapterId, sourcePage.PageNumber }) + .IsUnique(true); + + modelBuilder + .Entity() + .HasOne(x => x.Chapter) .WithMany(x => x.Pages) - .HasForeignKey(x => x.MangaChapterId) + .HasForeignKey(x => x.SourceChapterId) .OnDelete(DeleteBehavior.Cascade); } } \ No newline at end of file diff --git a/MangaReader.Core/Data/MangaSource.cs b/MangaReader.Core/Data/MangaSource.cs index 380b519..46131be 100644 --- a/MangaReader.Core/Data/MangaSource.cs +++ b/MangaReader.Core/Data/MangaSource.cs @@ -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,6 @@ public class MangaSource public required Source Source { get; set; } public required string Url { get; set; } + + public virtual ICollection Chapters { get; set; } = []; } \ No newline at end of file diff --git a/MangaReader.Core/Data/ScanlationGroup.cs b/MangaReader.Core/Data/ScanlationGroup.cs new file mode 100644 index 0000000..6207b79 --- /dev/null +++ b/MangaReader.Core/Data/ScanlationGroup.cs @@ -0,0 +1,7 @@ +namespace MangaReader.Core.Data; + +public class ScanlationGroup +{ + public int ScanlationGroupId { get; set; } + public required string Name { get; set; } +} \ No newline at end of file diff --git a/MangaReader.Core/Data/SourceChapter.cs b/MangaReader.Core/Data/SourceChapter.cs new file mode 100644 index 0000000..de68e6c --- /dev/null +++ b/MangaReader.Core/Data/SourceChapter.cs @@ -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 Pages { get; set; } = []; +} \ No newline at end of file diff --git a/MangaReader.Core/Data/SourcePage.cs b/MangaReader.Core/Data/SourcePage.cs new file mode 100644 index 0000000..5ad2635 --- /dev/null +++ b/MangaReader.Core/Data/SourcePage.cs @@ -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; } +} \ No newline at end of file diff --git a/MangaReader.Core/MangaReader.Core.csproj b/MangaReader.Core/MangaReader.Core.csproj index 2e8d1f8..87003c0 100644 --- a/MangaReader.Core/MangaReader.Core.csproj +++ b/MangaReader.Core/MangaReader.Core.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/MangaReader.Core/Pipeline/IMangaPipeline.cs b/MangaReader.Core/Pipeline/IMangaPipeline.cs index 313ef66..013ff17 100644 --- a/MangaReader.Core/Pipeline/IMangaPipeline.cs +++ b/MangaReader.Core/Pipeline/IMangaPipeline.cs @@ -2,5 +2,6 @@ public interface IMangaPipeline { - Task RunAsync(MangaPipelineRequest request); + Task RunMetadataAsync(MangaMetadataPipelineRequest request); + Task RunPagesAsync(MangaPagePipelineRequest request); } \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipelineRequest.cs b/MangaReader.Core/Pipeline/MangaMetadataPipelineRequest.cs similarity index 74% rename from MangaReader.Core/Pipeline/MangaPipelineRequest.cs rename to MangaReader.Core/Pipeline/MangaMetadataPipelineRequest.cs index 2894e71..47b74b2 100644 --- a/MangaReader.Core/Pipeline/MangaPipelineRequest.cs +++ b/MangaReader.Core/Pipeline/MangaMetadataPipelineRequest.cs @@ -2,8 +2,9 @@ namespace MangaReader.Core.Pipeline; -public class MangaPipelineRequest +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; } diff --git a/MangaReader.Core/Pipeline/MangaPagePipelineRequest.cs b/MangaReader.Core/Pipeline/MangaPagePipelineRequest.cs new file mode 100644 index 0000000..c46fd41 --- /dev/null +++ b/MangaReader.Core/Pipeline/MangaPagePipelineRequest.cs @@ -0,0 +1,7 @@ +namespace MangaReader.Core.Pipeline; + +public class MangaPagePipelineRequest +{ + public required int SourceChapterId { get; init; } + public required IReadOnlyList PageImageUrls { get; init; } +} \ No newline at end of file diff --git a/MangaReader.Core/Pipeline/MangaPipeline.cs b/MangaReader.Core/Pipeline/MangaPipeline.cs index c6bac7a..c29a5e8 100644 --- a/MangaReader.Core/Pipeline/MangaPipeline.cs +++ b/MangaReader.Core/Pipeline/MangaPipeline.cs @@ -13,7 +13,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline Secondary } - public async Task RunAsync(MangaPipelineRequest request) + public async Task RunMetadataAsync(MangaMetadataPipelineRequest request) { string sourceName = request.SourceName; string sourceUrl = request.SourceUrl; @@ -21,8 +21,8 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline Source source = await GetOrAddSourceAsync(sourceName); Manga manga = await GetOrAddMangaAsync(sourceManga); + MangaSource mangaSource = await AddMangaSourceAsync(sourceUrl, manga, source); - await AddMangaSourceAsync(sourceUrl, manga, source); await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary); await AddDescriptionAsync(manga, sourceManga.Description); @@ -38,7 +38,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline foreach (SourceMangaChapter chapter in sourceManga.Chapters) { - await AddChapterAsync(manga, chapter); + await AddChapterAsync(mangaSource, chapter); } context.SaveChanges(); @@ -94,13 +94,13 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline [GeneratedRegex(@"\s+")] private static partial Regex RemoveSpacesWithDashRegex(); - private async Task AddMangaSourceAsync(string sourceUrl, Manga manga, Source source) + private async Task 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; + return mangaSource; mangaSource = new() { @@ -110,6 +110,8 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline }; context.MangaSources.Add(mangaSource); + + return mangaSource; } private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle, TitleType titleType) @@ -187,32 +189,76 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline return genre; } - private async Task AddChapterAsync(Manga manga, SourceMangaChapter sourceMangaChapter) + private async Task AddChapterAsync(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter) { - MangaChapter mangaChapter = await context.MangaChapters.FirstOrDefaultAsync(x => x.ChapterNumber == sourceMangaChapter.Number) - ?? AddMangaChapter(manga, sourceMangaChapter); + SourceChapter sourceChapter = await GetSourceChapter(mangaSource, sourceMangaChapter) + ?? AddSourceChapter(mangaSource, sourceMangaChapter); - if (mangaChapter.VolumeNumber is null && sourceMangaChapter.Volume is not null) + if (sourceChapter.VolumeNumber is null && sourceMangaChapter.Volume is not null) { - mangaChapter.VolumeNumber = sourceMangaChapter.Volume; + sourceChapter.VolumeNumber = sourceMangaChapter.Volume; } - if (mangaChapter.Title is null && sourceMangaChapter.Title is not null) + if (sourceChapter.Title is null && sourceMangaChapter.Title is not null) { - mangaChapter.Title = sourceMangaChapter.Title; + sourceChapter.Title = sourceMangaChapter.Title; } } - private MangaChapter AddMangaChapter(Manga manga, SourceMangaChapter sourceMangaChapter) + private async Task GetSourceChapter(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter) { - MangaChapter mangaChapter = new() + return await context.SourceChapters.FirstOrDefaultAsync(x => + x.MangaSource == mangaSource && x.ChapterNumber == sourceMangaChapter.Number); + } + + private SourceChapter AddSourceChapter(MangaSource mangaSource, SourceMangaChapter sourceMangaChapter) + { + SourceChapter sourceChapter = new() { - Manga = manga, - ChapterNumber = sourceMangaChapter.Number + MangaSource = mangaSource, + ChapterNumber = sourceMangaChapter.Number, + Url = sourceMangaChapter.Url }; - context.MangaChapters.Add(mangaChapter); + context.SourceChapters.Add(sourceChapter); - return mangaChapter; + return sourceChapter; + } + + public async Task RunPagesAsync(MangaPagePipelineRequest request) + { + SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId); + + if (sourceChapter == null) + return; + + int currentPageNumber = 1; + + foreach (string pageImageUrl in request.PageImageUrls) + { + await AddOrUpdateSourcePageAsync(sourceChapter, currentPageNumber++, pageImageUrl); + } + } + + private async Task AddOrUpdateSourcePageAsync(SourceChapter sourceChapter, int pageNumber, string pageImageUrl) + { + SourcePage? sourcePage = await context.SourcePages.FirstOrDefaultAsync(x => + x.Chapter == sourceChapter && x.PageNumber == pageNumber); + + if (sourcePage == null) + { + sourcePage = new() + { + Chapter = sourceChapter, + PageNumber = pageNumber, + Url = pageImageUrl + }; + + context.SourcePages.Add(sourcePage); + } + else + { + sourcePage.Url = pageImageUrl; + } } } diff --git a/MangaReader.Core/Search/MangaSearchResult.cs b/MangaReader.Core/Search/MangaSearchResult.cs index ce89829..43e28e6 100644 --- a/MangaReader.Core/Search/MangaSearchResult.cs +++ b/MangaReader.Core/Search/MangaSearchResult.cs @@ -2,6 +2,7 @@ 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; } diff --git a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs index 34e59f3..8211d2d 100644 --- a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs @@ -53,7 +53,7 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM return [.. mangaSearchResults]; } - private static MangaSearchResult? GetMangaSearchResult(MangaEntity mangaEntity, CoverArtEntity[] coverArtEntites) + private MangaSearchResult? GetMangaSearchResult(MangaEntity mangaEntity, CoverArtEntity[] coverArtEntites) { MangaAttributes? mangaAttributes = mangaEntity.Attributes; @@ -65,6 +65,7 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM MangaSearchResult mangaSearchResult = new() { + Source = SourceId, Title = title, Description = GetDescription(mangaAttributes), Genres = GetGenres(mangaAttributes), diff --git a/MangaReader.Core/Sources/NatoManga/Search/NatoMangaSearchProvider.cs b/MangaReader.Core/Sources/NatoManga/Search/NatoMangaSearchProvider.cs index cb1fa48..9604c38 100644 --- a/MangaReader.Core/Sources/NatoManga/Search/NatoMangaSearchProvider.cs +++ b/MangaReader.Core/Sources/NatoManga/Search/NatoMangaSearchProvider.cs @@ -17,6 +17,7 @@ public partial class NatoMangaSearchProvider(INatoMangaClient natoMangaClient) : { MangaSearchResult mangaSearchResult = new() { + Source = SourceId, Title = searchResult.Name, Thumbnail = searchResult.Thumb, Url = searchResult.Url diff --git a/MangaReader.Tests/MangaReader.Tests.csproj b/MangaReader.Tests/MangaReader.Tests.csproj index 25a418e..94fd41f 100644 --- a/MangaReader.Tests/MangaReader.Tests.csproj +++ b/MangaReader.Tests/MangaReader.Tests.csproj @@ -42,12 +42,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MangaReader.Tests/Pipeline/MangaPipelineTests.cs b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs index edf7c48..97b4550 100644 --- a/MangaReader.Tests/Pipeline/MangaPipelineTests.cs +++ b/MangaReader.Tests/Pipeline/MangaPipelineTests.cs @@ -43,14 +43,14 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture mt.IsPrimary).First().Name.ShouldBe("Fullmetal Alchemist"); context.MangaTitles.Where(mt => mt.IsPrimary).First().Language.ShouldBe(Language.English); context.Genres.Count().ShouldBe(2); - context.MangaChapters.ShouldHaveSingleItem(); + context.SourceChapters.ShouldHaveSingleItem(); } } \ No newline at end of file diff --git a/MangaReader.Tests/Search/MangaSearchCoordinatorTests.cs b/MangaReader.Tests/Search/MangaSearchCoordinatorTests.cs index febc3b2..8ee6004 100644 --- a/MangaReader.Tests/Search/MangaSearchCoordinatorTests.cs +++ b/MangaReader.Tests/Search/MangaSearchCoordinatorTests.cs @@ -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" } ]); diff --git a/MangaReader.WinUI/MangaReader.WinUI.csproj b/MangaReader.WinUI/MangaReader.WinUI.csproj index 91c97b1..99f64ab 100644 --- a/MangaReader.WinUI/MangaReader.WinUI.csproj +++ b/MangaReader.WinUI/MangaReader.WinUI.csproj @@ -50,7 +50,7 @@ - +