More updates.

This commit is contained in:
2025-06-25 10:40:03 -04:00
parent a82eab0ecb
commit 33e521e8bb
28 changed files with 334 additions and 63 deletions

View File

@@ -0,0 +1,8 @@
namespace MangaReader.Core.Common;
public enum ContributorRole
{
Unknown,
Author,
Artist
}

View File

@@ -7,7 +7,7 @@ public class Manga
public virtual ICollection<MangaCover> Covers { get; set; } = [];
public virtual ICollection<MangaTitle> Titles { get; set; } = [];
public virtual ICollection<MangaDescription> Descriptions { 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; } = [];

View File

@@ -37,7 +37,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
//ConfigureMangaChapter(modelBuilder);
//ConfigureChapterSource(modelBuilder);
//ConfigureChapterPage(modelBuilder);
ConfigureSourceChapter(modelBuilder);
ConfigureMangaSourceChapter(modelBuilder);
ConfigureSourcePage(modelBuilder);
}
@@ -114,29 +114,29 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
{
modelBuilder
.Entity<MangaDescription>()
.HasKey(mangaTitle => mangaTitle.MangaTitleId);
.HasKey(mangaDescription => mangaDescription.MangaDescriptionId);
modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Name)
.Property(mangaDescription => mangaDescription.Name)
.IsRequired();
modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Language)
.Property(mangaDescription => mangaDescription.Language)
.IsRequired();
modelBuilder.Entity<MangaDescription>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name, mangaTitle.Language })
.HasIndex(mangaDescription => new { mangaDescription.MangaSourceId, mangaDescription.Name, mangaDescription.Language })
.IsUnique();
modelBuilder
.Entity<MangaDescription>()
.HasIndex(mangaTitle => mangaTitle.Name);
.HasIndex(mangaDescription => mangaDescription.Name);
modelBuilder
.Entity<MangaDescription>()
.HasOne(x => x.Manga)
.HasOne(x => x.MangaSource)
.WithMany(x => x.Descriptions)
.HasForeignKey(x => x.MangaId)
.HasForeignKey(x => x.MangaSourceId)
.OnDelete(DeleteBehavior.Cascade);
}
@@ -273,7 +273,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
// .OnDelete(DeleteBehavior.Cascade);
//}
private static void ConfigureSourceChapter(ModelBuilder modelBuilder)
private static void ConfigureMangaSourceChapter(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<SourceChapter>()

View File

@@ -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; }
}

View File

@@ -1,7 +0,0 @@
namespace MangaReader.Core.Data;
public enum MangaContributorRole
{
Author,
Artist
}

View File

@@ -4,10 +4,10 @@ namespace MangaReader.Core.Data;
public class MangaDescription
{
public int MangaTitleId { get; set; }
public int MangaDescriptionId { get; set; }
public int MangaId { get; set; }
public required Manga Manga { 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; }

View File

@@ -12,5 +12,6 @@ public class MangaSource
public required string Url { get; set; }
public virtual ICollection<MangaDescription> Descriptions { get; set; } = [];
public virtual ICollection<SourceChapter> Chapters { get; set; } = [];
}

View File

@@ -1,5 +1,7 @@
using MangaReader.Core.Http;
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;
@@ -7,6 +9,7 @@ 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;
@@ -14,7 +17,7 @@ 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)
{
// Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
services.AddHttpClient<IHttpService, HttpService>(client =>
@@ -34,9 +37,32 @@ public static class ServiceCollectionExtensions
// MangaDex
services.AddScoped<IMangaDexClient, MangaDexClient>();
services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>();
services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>();
//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;
}

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
</ItemGroup>

View File

@@ -0,0 +1,6 @@
namespace MangaReader.Core.Metadata;
public interface IMangaMetadataCoordinator
{
IMangaMetadataProvider GetProvider(string sourceName);
}

View 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);
}
}

View File

@@ -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; }
}

View File

@@ -1,8 +0,0 @@
namespace MangaReader.Core.Metadata;
public enum SourceMangaContributorRole
{
Unknown,
Author,
Artist
}

View File

@@ -2,6 +2,6 @@
public interface IMangaPipeline
{
Task RunMetadataAsync(MangaMetadataPipelineRequest request);
Task RunPagesAsync(MangaPagePipelineRequest request);
Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken);
Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken);
}

View File

@@ -13,7 +13,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
Secondary
}
public async Task RunMetadataAsync(MangaMetadataPipelineRequest request)
public async Task RunMetadataAsync(MangaMetadataPipelineRequest request, CancellationToken cancellationToken)
{
string sourceName = request.SourceName;
string sourceUrl = request.SourceUrl;
@@ -24,7 +24,7 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
MangaSource mangaSource = await AddMangaSourceAsync(sourceUrl, manga, source);
await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary);
await AddDescriptionAsync(manga, sourceManga.Description);
await AddDescriptionAsync(mangaSource, sourceManga.Description);
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
{
@@ -36,6 +36,11 @@ 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(mangaSource, chapter);
@@ -133,20 +138,23 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
context.MangaTitles.Add(mangaTitle);
}
private async Task AddDescriptionAsync(Manga manga, SourceMangaDescription? sourceMangaDescription)
private async Task AddDescriptionAsync(MangaSource mangaSource, SourceMangaDescription? sourceMangaDescription)
{
if (sourceMangaDescription == null)
return;
MangaDescription? mangaDescription = await context.MangaDescriptions.FirstOrDefaultAsync(md =>
md.Manga == manga && md.Name == sourceMangaDescription.Name);
md.MangaSource == mangaSource && md.Language == sourceMangaDescription.Language);
if (mangaDescription != null)
{
mangaDescription.Name = sourceMangaDescription.Name;
return;
}
mangaDescription = new()
{
Manga = manga,
MangaSource = mangaSource,
Name = sourceMangaDescription.Name,
Language = sourceMangaDescription.Language
};
@@ -189,6 +197,51 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
return genre;
}
private async Task LinkMangaContributorAsync(Manga manga, SourceMangaContributor sourceMangaContributor)
{
Contributor contributor = await GetOrAddContributorAsync(sourceMangaContributor.Name);
MangaContributor? mangaContributor = await context.MangaContributors.FirstOrDefaultAsync(x =>
x.Manga == manga && x.Contributor == contributor && x.Role == sourceMangaContributor.Role);
if (mangaContributor != null)
return;
mangaContributor = new()
{
Manga = manga,
Contributor = contributor,
Role = sourceMangaContributor.Role
};
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)
@@ -225,9 +278,9 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
return sourceChapter;
}
public async Task RunPagesAsync(MangaPagePipelineRequest request)
public async Task RunPagesAsync(MangaPagePipelineRequest request, CancellationToken cancellationToken)
{
SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId);
SourceChapter? sourceChapter = await context.SourceChapters.FirstOrDefaultAsync(x => x.SourceChapterId == request.SourceChapterId, cancellationToken);
if (sourceChapter == null)
return;
@@ -236,14 +289,14 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
foreach (string pageImageUrl in request.PageImageUrls)
{
await AddOrUpdateSourcePageAsync(sourceChapter, currentPageNumber++, pageImageUrl);
await AddOrUpdateSourcePageAsync(sourceChapter, currentPageNumber++, pageImageUrl, cancellationToken);
}
}
private async Task AddOrUpdateSourcePageAsync(SourceChapter sourceChapter, int pageNumber, string pageImageUrl)
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);
x.Chapter == sourceChapter && x.PageNumber == pageNumber, cancellationToken);
if (sourcePage == null)
{
@@ -254,11 +307,11 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
Url = pageImageUrl
};
context.SourcePages.Add(sourcePage);
await context.SourcePages.AddAsync(sourcePage, cancellationToken);
}
else
{
sourcePage.Url = pageImageUrl;
}
}
}
}

View File

@@ -137,7 +137,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
SourceMangaContributor contributor = new()
{
Name = authorEntity.Attributes.Name,
Role = SourceMangaContributorRole.Author
Role = ContributorRole.Author
};
contributors.Add(contributor);
@@ -151,7 +151,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
SourceMangaContributor contributor = new()
{
Name = artistEntity.Attributes.Name,
Role = SourceMangaContributorRole.Artist
Role = ContributorRole.Artist
};
contributors.Add(contributor);

View File

@@ -79,7 +79,7 @@ public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
SourceMangaContributor contributor = new()
{
Name = name,
Role = SourceMangaContributorRole.Author
Role = ContributorRole.Author
};
contributors.Add(contributor);