From ed154e4768de2408f2e6dd1a338d198e9502ac20 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Fri, 25 Apr 2025 07:26:48 -0600 Subject: [PATCH] Kavita+ Comic Metadata Matching (#3740) --- API/Controllers/SeriesController.cs | 6 +- .../SeriesDetailPlusApiDto.cs | 1 + .../KavitaPlus/Metadata/ExternalChapterDto.cs | 36 + .../Metadata/ExternalSeriesDetailDto.cs | 9 + .../Metadata/MetadataSettingsDto.cs | 23 + API/DTOs/Scrobbling/PlusSeriesDto.cs | 4 + .../20250415194829_KavitaPlusCBR.Designer.cs | 3433 +++++++++++++++++ .../20250415194829_KavitaPlusCBR.cs | 106 + .../Migrations/DataContextModelSnapshot.cs | 26 +- API/Data/Repositories/ChapterRepository.cs | 12 + .../ExternalSeriesMetadataRepository.cs | 4 +- API/Data/Seed.cs | 5 + API/Entities/Chapter.cs | 21 + .../Metadata/ExternalSeriesMetadata.cs | 1 + .../MetadataMatching/MetadataSettingField.cs | 16 +- .../MetadataMatching/MetadataSettings.cs | 26 + API/Entities/Person/ChapterPeople.cs | 9 + API/Services/Plus/ExternalMetadataService.cs | 235 +- API/Services/Plus/LicenseService.cs | 1 + API/Services/Plus/ScrobblingService.cs | 3 + API/Services/SettingsService.cs | 6 + API/Services/Tasks/Metadata/CoverDbService.cs | 46 + API/Services/Tasks/Scanner/ProcessSeries.cs | 26 +- .../series-detail/external-series-detail.ts | 5 +- .../app/_pipes/metadata-setting-filed.pipe.ts | 14 +- .../src/app/_pipes/plus-media-format.pipe.ts | 4 +- UI/Web/src/app/_pipes/provider-image.pipe.ts | 2 + UI/Web/src/app/_pipes/provider-name.pipe.ts | 23 - .../app/_pipes/scrobble-provider-name.pipe.ts | 1 + .../src/app/_services/scrobbling.service.ts | 5 +- UI/Web/src/app/_services/series.service.ts | 2 +- .../match-series-modal.component.ts | 2 +- .../admin/_models/metadata-setting-field.ts | 9 +- .../app/admin/_models/metadata-settings.ts | 8 + .../manage-metadata-settings.component.html | 65 + .../manage-metadata-settings.component.ts | 7 + .../all-collections.component.ts | 1 + .../collection-detail.component.html | 2 +- .../collection-detail.component.ts | 12 +- .../collection-owner.component.html | 4 +- .../collection-owner.component.ts | 6 +- .../external-rating.component.html | 10 +- .../external-rating.component.ts | 19 +- .../library-settings-modal.component.ts | 28 +- .../ExternalServices/ComicBookRoundup.png | Bin 0 -> 3720 bytes UI/Web/src/assets/langs/en.json | 21 +- 46 files changed, 4207 insertions(+), 98 deletions(-) create mode 100644 API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs create mode 100644 API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs create mode 100644 API/Data/Migrations/20250415194829_KavitaPlusCBR.cs delete mode 100644 UI/Web/src/app/_pipes/provider-name.pipe.ts create mode 100644 UI/Web/src/assets/images/ExternalServices/ComicBookRoundup.png diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index f64e5e1b3..94f9c084f 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -649,13 +649,13 @@ public class SeriesController : BaseApiController /// /// This will perform the fix match /// - /// + /// /// /// [HttpPost("update-match")] - public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int aniListId, [FromQuery] long? malId) + public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int? aniListId, [FromQuery] long? malId, [FromQuery] int? cbrId) { - BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId)); + BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId)); return Ok(); } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs index f67fb1f4c..26411bce7 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs @@ -13,4 +13,5 @@ internal class SeriesDetailPlusApiDto public ExternalSeriesDetailDto? Series { get; set; } public int? AniListId { get; set; } public long? MalId { get; set; } + public int? CbrId { get; set; } } diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs new file mode 100644 index 000000000..6b711513c --- /dev/null +++ b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using API.DTOs.SeriesDetail; + +namespace API.DTOs.KavitaPlus.Metadata; + +/// +/// Information about an individual issue/chapter/book from Kavita+ +/// +public class ExternalChapterDto +{ + public string Title { get; set; } + + public string IssueNumber { get; set; } + + public decimal? CriticRating { get; set; } + + public decimal? UserRating { get; set; } + + public string? Summary { get; set; } + + public IList? Writers { get; set; } + + public IList? Artists { get; set; } + + public DateTime? ReleaseDate { get; set; } + + public string? Publisher { get; set; } + + public string? CoverImageUrl { get; set; } + + public string? IssueUrl { get; set; } + + public IList CriticReviews { get; set; } + public IList UserReviews { get; set; } +} diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs index a5a037cc3..2ea746214 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs @@ -15,6 +15,7 @@ public class ExternalSeriesDetailDto public string Name { get; set; } public int? AniListId { get; set; } public long? MALId { get; set; } + public int? CbrId { get; set; } public IList Synonyms { get; set; } = []; public PlusMediaFormat PlusMediaFormat { get; set; } public string? SiteUrl { get; set; } @@ -33,5 +34,13 @@ public class ExternalSeriesDetailDto public IList? Relations { get; set; } = []; public IList? Characters { get; set; } = []; + #region Comic Only + public string? Publisher { get; set; } + /// + /// Only from CBR for . Full metadata about issues + /// + public IList? ChapterDtos { get; set; } + #endregion + } diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs b/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs index d2e8247cb..1dd26a7bc 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs @@ -43,6 +43,29 @@ public class MetadataSettingsDto /// public bool EnableCoverImage { get; set; } + #region Chapter Metadata + /// + /// Allow Summary to be set within Chapter/Issue + /// + public bool EnableChapterSummary { get; set; } + /// + /// Allow Release Date to be set within Chapter/Issue + /// + public bool EnableChapterReleaseDate { get; set; } + /// + /// Allow Title to be set within Chapter/Issue + /// + public bool EnableChapterTitle { get; set; } + /// + /// Allow Publisher to be set within Chapter/Issue + /// + public bool EnableChapterPublisher { get; set; } + /// + /// Allow setting the cover image for the Chapter/Issue + /// + public bool EnableChapterCoverImage { get; set; } + #endregion + // Need to handle the Genre/tags stuff public bool EnableGenres { get; set; } = true; public bool EnableTags { get; set; } = true; diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/API/DTOs/Scrobbling/PlusSeriesDto.cs index c36516837..dca9aca92 100644 --- a/API/DTOs/Scrobbling/PlusSeriesDto.cs +++ b/API/DTOs/Scrobbling/PlusSeriesDto.cs @@ -10,6 +10,10 @@ public record PlusSeriesRequestDto public long? MalId { get; set; } public string? GoogleBooksId { get; set; } public string? MangaDexId { get; set; } + /// + /// ComicBookRoundup Id + /// + public int? CbrId { get; set; } public string SeriesName { get; set; } public string? AltSeriesName { get; set; } public PlusMediaFormat MediaFormat { get; set; } diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs b/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs new file mode 100644 index 000000000..fd287c085 --- /dev/null +++ b/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs @@ -0,0 +1,3433 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250415194829_KavitaPlusCBR")] + partial class KavitaPlusCBR + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs b/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs new file mode 100644 index 000000000..188969476 --- /dev/null +++ b/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs @@ -0,0 +1,106 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class KavitaPlusCBR : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EnableChapterCoverImage", + table: "MetadataSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "EnableChapterPublisher", + table: "MetadataSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "EnableChapterReleaseDate", + table: "MetadataSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "EnableChapterSummary", + table: "MetadataSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "EnableChapterTitle", + table: "MetadataSettings", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "CbrId", + table: "ExternalSeriesMetadata", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "KavitaPlusConnection", + table: "ChapterPeople", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "OrderWeight", + table: "ChapterPeople", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EnableChapterCoverImage", + table: "MetadataSettings"); + + migrationBuilder.DropColumn( + name: "EnableChapterPublisher", + table: "MetadataSettings"); + + migrationBuilder.DropColumn( + name: "EnableChapterReleaseDate", + table: "MetadataSettings"); + + migrationBuilder.DropColumn( + name: "EnableChapterSummary", + table: "MetadataSettings"); + + migrationBuilder.DropColumn( + name: "EnableChapterTitle", + table: "MetadataSettings"); + + migrationBuilder.DropColumn( + name: "CbrId", + table: "ExternalSeriesMetadata"); + + migrationBuilder.DropColumn( + name: "KavitaPlusConnection", + table: "ChapterPeople"); + + migrationBuilder.DropColumn( + name: "OrderWeight", + table: "ChapterPeople"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 232f8c2a8..ab2115091 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -1429,6 +1429,9 @@ namespace API.Data.Migrations b.Property("AverageExternalRating") .HasColumnType("INTEGER"); + b.Property("CbrId") + .HasColumnType("INTEGER"); + b.Property("GoogleBooksId") .HasColumnType("TEXT"); @@ -1645,6 +1648,21 @@ namespace API.Data.Migrations b.Property("Blacklist") .HasColumnType("TEXT"); + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + b.Property("EnableCoverImage") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") @@ -1707,6 +1725,12 @@ namespace API.Data.Migrations b.Property("Role") .HasColumnType("INTEGER"); + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + b.HasKey("ChapterId", "PersonId", "Role"); b.HasIndex("PersonId"); diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 52ded9e94..af3ca9c4d 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -47,6 +47,7 @@ public interface IChapterRepository Task> GetCoverImagesForLockedChaptersAsync(); Task AddChapterModifiers(int userId, ChapterDto chapter); IEnumerable GetChaptersForSeries(int seriesId); + Task> GetAllChaptersForSeries(int seriesId); } public class ChapterRepository : IChapterRepository { @@ -298,4 +299,15 @@ public class ChapterRepository : IChapterRepository .Include(c => c.Volume) .AsEnumerable(); } + + public async Task> GetAllChaptersForSeries(int seriesId) + { + return await _context.Chapter + .Where(c => c.Volume.SeriesId == seriesId) + .OrderBy(c => c.SortOrder) + .Include(c => c.Volume) + .Include(c => c.People) + .ThenInclude(cp => cp.Person) + .ToListAsync(); + } } diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index 84b8b3a7c..45882b5c4 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -157,8 +157,8 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .OrderByDescending(r => r.Score); } - IEnumerable ratings = new List(); - if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any()) + IEnumerable ratings = []; + if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0) { ratings = seriesDetailDto.ExternalRatings .Select(r => _mapper.Map(r)); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 2c385a852..74bfbb296 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -312,6 +312,11 @@ public static class Seed EnableLocalizedName = false, FirstLastPeopleNaming = true, EnableCoverImage = true, + EnableChapterTitle = false, + EnableChapterSummary = true, + EnableChapterPublisher = true, + EnableChapterCoverImage = false, + EnableChapterReleaseDate = true, PersonRoles = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character] }; await context.MetadataSettings.AddAsync(existing); diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index 3f96472ca..83a547fd7 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -234,4 +234,25 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage PrimaryColor = string.Empty; SecondaryColor = string.Empty; } + + public bool IsPersonRoleLocked(PersonRole role) + { + return role switch + { + PersonRole.Character => CharacterLocked, + PersonRole.Writer => WriterLocked, + PersonRole.Penciller => PencillerLocked, + PersonRole.Inker => InkerLocked, + PersonRole.Colorist => ColoristLocked, + PersonRole.Letterer => LettererLocked, + PersonRole.CoverArtist => CoverArtistLocked, + PersonRole.Editor => EditorLocked, + PersonRole.Publisher => PublisherLocked, + PersonRole.Translator => TranslatorLocked, + PersonRole.Imprint => ImprintLocked, + PersonRole.Team => TeamLocked, + PersonRole.Location => LocationLocked, + _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) + }; + } } diff --git a/API/Entities/Metadata/ExternalSeriesMetadata.cs b/API/Entities/Metadata/ExternalSeriesMetadata.cs index ccbe53c10..1ab37ba3c 100644 --- a/API/Entities/Metadata/ExternalSeriesMetadata.cs +++ b/API/Entities/Metadata/ExternalSeriesMetadata.cs @@ -26,6 +26,7 @@ public class ExternalSeriesMetadata public int AverageExternalRating { get; set; } = -1; public int AniListId { get; set; } + public int CbrId { get; set; } public long MalId { get; set; } public string GoogleBooksId { get; set; } diff --git a/API/Entities/MetadataMatching/MetadataSettingField.cs b/API/Entities/MetadataMatching/MetadataSettingField.cs index 89ca5ee3e..9333c269e 100644 --- a/API/Entities/MetadataMatching/MetadataSettingField.cs +++ b/API/Entities/MetadataMatching/MetadataSettingField.cs @@ -5,6 +5,7 @@ /// public enum MetadataSettingField { + #region Series Metadata Summary = 1, PublicationStatus = 2, StartDate = 3, @@ -13,5 +14,18 @@ public enum MetadataSettingField LocalizedName = 6, Covers = 7, AgeRating = 8, - People = 9 + People = 9, + #endregion + + #region Chapter Metadata + + ChapterTitle = 10, + ChapterSummary = 11, + ChapterReleaseDate = 12, + ChapterPublisher = 13, + ChapterCovers = 14, + + #endregion + + } diff --git a/API/Entities/MetadataMatching/MetadataSettings.cs b/API/Entities/MetadataMatching/MetadataSettings.cs index bdf7f979f..aeb44b619 100644 --- a/API/Entities/MetadataMatching/MetadataSettings.cs +++ b/API/Entities/MetadataMatching/MetadataSettings.cs @@ -14,6 +14,8 @@ public class MetadataSettings /// public bool Enabled { get; set; } + #region Series Metadata + /// /// Allow the Summary to be written /// @@ -42,6 +44,30 @@ public class MetadataSettings /// Allow setting the cover image /// public bool EnableCoverImage { get; set; } + #endregion + + #region Chapter Metadata + /// + /// Allow Summary to be set within Chapter/Issue + /// + public bool EnableChapterSummary { get; set; } + /// + /// Allow Release Date to be set within Chapter/Issue + /// + public bool EnableChapterReleaseDate { get; set; } + /// + /// Allow Title to be set within Chapter/Issue + /// + public bool EnableChapterTitle { get; set; } + /// + /// Allow Publisher to be set within Chapter/Issue + /// + public bool EnableChapterPublisher { get; set; } + /// + /// Allow setting the cover image for the Chapter/Issue + /// + public bool EnableChapterCoverImage { get; set; } + #endregion // Need to handle the Genre/tags stuff public bool EnableGenres { get; set; } = true; diff --git a/API/Entities/Person/ChapterPeople.cs b/API/Entities/Person/ChapterPeople.cs index 15da3994d..c6a08a7dd 100644 --- a/API/Entities/Person/ChapterPeople.cs +++ b/API/Entities/Person/ChapterPeople.cs @@ -10,5 +10,14 @@ public class ChapterPeople public int PersonId { get; set; } public virtual Person Person { get; set; } + /// + /// The source of this connection. If not Kavita, this implies Metadata Download linked this and it can be removed between matches + /// + public bool KavitaPlusConnection { get; set; } + /// + /// A weight that allows lower numbers to sort first + /// + public int OrderWeight { get; set; } + public required PersonRole Role { get; set; } } diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index b681224c6..424331e09 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -50,7 +50,7 @@ public interface IExternalMetadataService Task> GetStacksForUser(int userId); Task> MatchSeries(MatchSeriesDto dto); - Task FixSeriesMatch(int seriesId, int anilistId, long? malId); + Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId); Task UpdateSeriesDontMatch(int seriesId, bool dontMatch); Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId); } @@ -66,7 +66,7 @@ public class ExternalMetadataService : IExternalMetadataService private readonly ICoverDbService _coverDbService; private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30); public static readonly HashSet NonEligibleLibraryTypes = - [LibraryType.Comic, LibraryType.Book, LibraryType.Image, LibraryType.ComicVine]; + [LibraryType.Comic, LibraryType.Book, LibraryType.Image]; private readonly SeriesDetailPlusDto _defaultReturn = new() { Series = null, @@ -203,7 +203,7 @@ public class ExternalMetadataService : IExternalMetadataService { var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, - SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata); + SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library); if (series == null) return []; var potentialAnilistId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); @@ -217,7 +217,7 @@ public class ExternalMetadataService : IExternalMetadataService var matchRequest = new MatchSeriesRequestDto() { - Format = series.Format == MangaFormat.Epub ? PlusMediaFormat.LightNovel : PlusMediaFormat.Manga, + Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), Query = dto.Query, SeriesName = series.Name, AlternativeNames = altNames.Where(s => !string.IsNullOrEmpty(s)).ToList(), @@ -319,8 +319,10 @@ public class ExternalMetadataService : IExternalMetadataService /// This will override any sort of matching that was done prior and force it to be what the user Selected /// /// - /// - public async Task FixSeriesMatch(int seriesId, int anilistId, long? malId) + /// + /// + /// + public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); if (series == null) return; @@ -336,15 +338,17 @@ public class ExternalMetadataService : IExternalMetadataService var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, new PlusSeriesRequestDto() { - AniListId = anilistId, + AniListId = aniListId, MalId = malId, + CbrId = cbrId, + MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed }); if (metadata.Series == null) { - _logger.LogError("Unable to Match {SeriesName} with Kavita+ Series AniList Id: {AniListId}", - series.Name, anilistId); + _logger.LogError("Unable to Match {SeriesName} with Kavita+ Series with Id: {AniListId}/{MalId}/{CbrId}", + series.Name, aniListId, malId, cbrId); return; } @@ -428,8 +432,7 @@ public class ExternalMetadataService : IExternalMetadataService result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") .WithKavitaPlusHeaders(license, token) .PostJsonAsync(data) - .ReceiveJson< - SeriesDetailPlusApiDto>(); // This returns an AniListSeries and Match returns ExternalSeriesDto + .ReceiveJson(); // This returns an AniListSeries and Match returns ExternalSeriesDto } catch (FlurlHttpException ex) { @@ -482,6 +485,7 @@ public class ExternalMetadataService : IExternalMetadataService { var rating = _mapper.Map(r); rating.SeriesId = externalSeriesMetadata.SeriesId; + rating.ProviderUrl = r.ProviderUrl; return rating; }).ToList(); @@ -500,6 +504,7 @@ public class ExternalMetadataService : IExternalMetadataService if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value; if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value; + if (result.CbrId.HasValue) externalSeriesMetadata.CbrId = result.CbrId.Value; // If there is metadata and the user has metadata download turned on var madeMetadataModification = false; @@ -622,6 +627,8 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = await UpdateRelationships(series, settings, externalMetadata.Relations, defaultAdmin) || madeModification; madeModification = await UpdateCoverImage(series, settings, externalMetadata) || madeModification; + madeModification = await UpdateChapters(series, settings, externalMetadata) || madeModification; + return madeModification; } @@ -848,7 +855,6 @@ public class ExternalMetadataService : IExternalMetadataService } } - // Download the image and save it _unitOfWork.SeriesRepository.Update(series); await _unitOfWork.CommitAsync(); @@ -1044,6 +1050,199 @@ public class ExternalMetadataService : IExternalMetadataService return false; } + + private async Task UpdateChapters(Series series, MetadataSettingsDto settings, + ExternalSeriesDetailDto externalMetadata) + { + if (externalMetadata.PlusMediaFormat != PlusMediaFormat.Comic) return false; + + if (externalMetadata.ChapterDtos == null || externalMetadata.ChapterDtos.Count == 0) return false; + + // Get all volumes and chapters + var madeModification = false; + var allChapters = await _unitOfWork.ChapterRepository.GetAllChaptersForSeries(series.Id); + + var matchedChapters = allChapters + .Join( + externalMetadata.ChapterDtos, + chapter => chapter.Range, + dto => dto.IssueNumber, + (chapter, dto) => (chapter, dto) // Create a tuple of matched pairs + ) + .ToList(); + + foreach (var (chapter, potentialMatch) in matchedChapters) + { + _logger.LogDebug("Updating {ChapterNumber} with metadata", chapter.Range); + + // Write the metadata + madeModification = UpdateChapterTitle(chapter, settings, potentialMatch.Title, series.Name) || madeModification; + madeModification = UpdateChapterSummary(chapter, settings, potentialMatch.Summary) || madeModification; + madeModification = UpdateChapterReleaseDate(chapter, settings, potentialMatch.ReleaseDate) || madeModification; + madeModification = await UpdateChapterPublisher(chapter, settings, potentialMatch.Publisher) || madeModification; + + madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.CoverArtist, potentialMatch.Artists) || madeModification; + madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; + + madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification; + + _unitOfWork.ChapterRepository.Update(chapter); + await _unitOfWork.CommitAsync(); + } + + + return madeModification; + } + + + private static bool UpdateChapterSummary(Chapter chapter, MetadataSettingsDto settings, string? summary) + { + if (!settings.EnableChapterSummary) return false; + + if (string.IsNullOrEmpty(summary)) return false; + + if (chapter.SummaryLocked && !settings.HasOverride(MetadataSettingField.ChapterSummary)) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(summary) && !settings.HasOverride(MetadataSettingField.ChapterSummary)) + { + return false; + } + + chapter.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(summary)); + return true; + } + + private static bool UpdateChapterTitle(Chapter chapter, MetadataSettingsDto settings, string? title, string seriesName) + { + if (!settings.EnableChapterTitle) return false; + + if (string.IsNullOrEmpty(title)) return false; + + if (chapter.TitleNameLocked && !settings.HasOverride(MetadataSettingField.ChapterTitle)) + { + return false; + } + + if (!title.Contains(seriesName) && !settings.HasOverride(MetadataSettingField.ChapterTitle)) + { + return false; + } + + chapter.TitleName = title; + return true; + } + + private static bool UpdateChapterReleaseDate(Chapter chapter, MetadataSettingsDto settings, DateTime? releaseDate) + { + if (!settings.EnableChapterReleaseDate) return false; + + if (releaseDate == null || releaseDate == DateTime.MinValue) return false; + + if (chapter.ReleaseDateLocked && !settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) + { + return false; + } + + if (!settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) + { + return false; + } + + chapter.ReleaseDate = releaseDate.Value; + return true; + } + + private async Task UpdateChapterPublisher(Chapter chapter, MetadataSettingsDto settings, string? publisher) + { + if (!settings.EnableChapterPublisher) return false; + + if (string.IsNullOrEmpty(publisher)) return false; + + if (chapter.PublisherLocked && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(publisher) && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) + { + return false; + } + + return await UpdateChapterPeople(chapter, settings, PersonRole.Publisher, [publisher]); + } + + private async Task UpdateChapterCoverImage(Chapter chapter, MetadataSettingsDto settings, string? coverUrl) + { + if (!settings.EnableChapterCoverImage) return false; + + if (string.IsNullOrEmpty(coverUrl)) return false; + + if (chapter.CoverImageLocked && !settings.HasOverride(MetadataSettingField.ChapterCovers)) + { + return false; + } + + if (string.IsNullOrEmpty(coverUrl)) + { + return false; + } + + await DownloadChapterCovers(chapter, coverUrl); + return true; + } + + private async Task UpdateChapterPeople(Chapter chapter, MetadataSettingsDto settings, PersonRole role, IList? staff) + { + if (!settings.EnablePeople) return false; + + if (staff?.Count == 0) return false; + + if (chapter.IsPersonRoleLocked(role) && !settings.HasOverride(MetadataSettingField.People)) + { + return false; + } + + if (!settings.IsPersonAllowed(role) && role != PersonRole.Publisher) + { + return false; + } + + chapter.People ??= []; + var people = staff! + .Select(w => new PersonDto() + { + Name = w, + }) + .Concat(chapter.People + .Where(p => p.Role == role) + .Where(p => !p.KavitaPlusConnection) + .Select(p => _mapper.Map(p.Person)) + ) + .DistinctBy(p => Parser.Normalize(p.Name)) + .ToList(); + + await PersonHelper.UpdateChapterPeopleAsync(chapter, staff, role, _unitOfWork); + + foreach (var person in chapter.People.Where(p => p.Role == role)) + { + var meta = people.FirstOrDefault(c => c.Name == person.Person.Name); + person.OrderWeight = 0; + + if (meta != null) + { + person.KavitaPlusConnection = true; + } + } + + _unitOfWork.ChapterRepository.Update(chapter); + await _unitOfWork.CommitAsync(); + + return true; + } + private async Task UpdateCoverImage(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) { if (!settings.EnableCoverImage) return false; @@ -1166,6 +1365,18 @@ public class ExternalMetadataService : IExternalMetadataService } } + private async Task DownloadChapterCovers(Chapter chapter, string coverUrl) + { + try + { + await _coverDbService.SetChapterCoverByUrl(chapter, coverUrl, false, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception downloading cover image for Chapter {ChapterName} ({SeriesId})", chapter.Range, chapter.Id); + } + } + private async Task DownloadAndSetPersonCovers(List people) { foreach (var staff in people) diff --git a/API/Services/Plus/LicenseService.cs b/API/Services/Plus/LicenseService.cs index 6f2a76f0d..774103518 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/API/Services/Plus/LicenseService.cs @@ -263,6 +263,7 @@ public class LicenseService( if (cacheValue.HasValue) return cacheValue.Value; } + // TODO: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking try { diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index de3f8465a..ef22736d2 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -38,6 +38,9 @@ public enum ScrobbleProvider Kavita = 0, AniList = 1, Mal = 2, + [Obsolete] + GoogleBooks = 3, + Cbr = 4 } public interface IScrobblingService diff --git a/API/Services/SettingsService.cs b/API/Services/SettingsService.cs index a7b8cc490..fd44b5962 100644 --- a/API/Services/SettingsService.cs +++ b/API/Services/SettingsService.cs @@ -65,6 +65,12 @@ public class SettingsService : ISettingsService existingMetadataSetting.FirstLastPeopleNaming = dto.FirstLastPeopleNaming; existingMetadataSetting.EnableCoverImage = dto.EnableCoverImage; + existingMetadataSetting.EnableChapterPublisher = dto.EnableChapterPublisher; + existingMetadataSetting.EnableChapterSummary = dto.EnableChapterSummary; + existingMetadataSetting.EnableChapterTitle = dto.EnableChapterTitle; + existingMetadataSetting.EnableChapterReleaseDate = dto.EnableChapterReleaseDate; + existingMetadataSetting.EnableChapterCoverImage = dto.EnableChapterCoverImage; + existingMetadataSetting.AgeRatingMappings = dto.AgeRatingMappings ?? []; existingMetadataSetting.Blacklist = (dto.Blacklist ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? []; diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index b90bdcc38..c76bb99d1 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -32,6 +32,7 @@ public interface ICoverDbService Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url); Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false); Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false); + Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false); } @@ -580,6 +581,51 @@ public class CoverDbService : ICoverDbService } } + public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false) + { + if (!string.IsNullOrEmpty(url)) + { + var filePath = await CreateThumbnail(url, $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}", fromBase64); + + if (!string.IsNullOrEmpty(filePath)) + { + // Additional check to see if downloaded image is similar and we have a higher resolution + if (chooseBetterImage && !string.IsNullOrEmpty(chapter.CoverImage)) + { + try + { + var betterImage = Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage) + .GetBetterImage(Path.Join(_directoryService.CoverImageDirectory, filePath))!; + filePath = Path.GetFileName(betterImage); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue trying to choose a better cover image for Chapter: {FileName} ({ChapterId})", chapter.Range, chapter.Id); + } + } + + chapter.CoverImage = filePath; + chapter.CoverImageLocked = true; + _imageService.UpdateColorScape(chapter); + _unitOfWork.ChapterRepository.Update(chapter); + } + } + else + { + chapter.CoverImage = null; + chapter.CoverImageLocked = false; + _imageService.UpdateColorScape(chapter); + _unitOfWork.ChapterRepository.Update(chapter); + } + + if (_unitOfWork.HasChanges()) + { + await _unitOfWork.CommitAsync(); + await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, + MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false); + } + } + private async Task CreateThumbnail(string url, string filename, bool fromBase64 = true) { var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 61199d106..59721fe61 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -977,26 +977,26 @@ public class ProcessSeries : IProcessSeries chapter.ReleaseDate = new DateTime(comicInfo.Year, month, day); } - if (!chapter.ColoristLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Colorist)) { var people = TagHelper.GetTagValues(comicInfo.Colorist); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Colorist); } - if (!chapter.CharacterLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Character)) { var people = TagHelper.GetTagValues(comicInfo.Characters); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Character); } - if (!chapter.TranslatorLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Translator)) { var people = TagHelper.GetTagValues(comicInfo.Translator); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Translator); } - if (!chapter.WriterLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Writer)) { var personSw = Stopwatch.StartNew(); var people = TagHelper.GetTagValues(comicInfo.Writer); @@ -1004,55 +1004,55 @@ public class ProcessSeries : IProcessSeries _logger.LogTrace("[TIME] Kavita took {Time} ms to process writer on Chapter: {File} for {Count} people", personSw.ElapsedMilliseconds, chapter.Files.First().FileName, people.Count); } - if (!chapter.EditorLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Editor)) { var people = TagHelper.GetTagValues(comicInfo.Editor); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Editor); } - if (!chapter.InkerLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Inker)) { var people = TagHelper.GetTagValues(comicInfo.Inker); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Inker); } - if (!chapter.LettererLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Letterer)) { var people = TagHelper.GetTagValues(comicInfo.Letterer); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Letterer); } - if (!chapter.PencillerLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Penciller)) { var people = TagHelper.GetTagValues(comicInfo.Penciller); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Penciller); } - if (!chapter.CoverArtistLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.CoverArtist)) { var people = TagHelper.GetTagValues(comicInfo.CoverArtist); await UpdateChapterPeopleAsync(chapter, people, PersonRole.CoverArtist); } - if (!chapter.PublisherLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Publisher)) { var people = TagHelper.GetTagValues(comicInfo.Publisher); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Publisher); } - if (!chapter.ImprintLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Imprint)) { var people = TagHelper.GetTagValues(comicInfo.Imprint); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Imprint); } - if (!chapter.TeamLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Team)) { var people = TagHelper.GetTagValues(comicInfo.Teams); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Team); } - if (!chapter.LocationLocked) + if (!chapter.IsPersonRoleLocked(PersonRole.Location)) { var people = TagHelper.GetTagValues(comicInfo.Locations); await UpdateChapterPeopleAsync(chapter, people, PersonRole.Location); diff --git a/UI/Web/src/app/_models/series-detail/external-series-detail.ts b/UI/Web/src/app/_models/series-detail/external-series-detail.ts index aa62d3960..db25782ca 100644 --- a/UI/Web/src/app/_models/series-detail/external-series-detail.ts +++ b/UI/Web/src/app/_models/series-detail/external-series-detail.ts @@ -27,8 +27,9 @@ export interface MetadataTagDto { export interface ExternalSeriesDetail { name: string; - aniListId?: number; - malId?: number; + aniListId?: number | null; + malId?: number | null; + cbrId?: number | null; synonyms: Array; plusMediaFormat: PlusMediaFormat; siteUrl?: string; diff --git a/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts b/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts index a9b763c50..dcaed4f69 100644 --- a/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts +++ b/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts @@ -1,4 +1,4 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {MetadataSettingField} from "../admin/_models/metadata-setting-field"; import {translate} from "@jsverse/transloco"; @@ -10,9 +10,19 @@ export class MetadataSettingFiledPipe implements PipeTransform { transform(value: MetadataSettingField): string { switch (value) { + case MetadataSettingField.ChapterTitle: + return translate('metadata-setting-field-pipe.chapter-title'); + case MetadataSettingField.ChapterSummary: + return translate('metadata-setting-field-pipe.chapter-summary'); + case MetadataSettingField.ChapterReleaseDate: + return translate('metadata-setting-field-pipe.chapter-release-date'); + case MetadataSettingField.ChapterPublisher: + return translate('metadata-setting-field-pipe.chapter-publisher'); + case MetadataSettingField.ChapterCovers: + return translate('metadata-setting-field-pipe.chapter-covers'); case MetadataSettingField.AgeRating: return translate('metadata-setting-field-pipe.age-rating'); - case MetadataSettingField.People: + case MetadataSettingField.People: return translate('metadata-setting-field-pipe.people'); case MetadataSettingField.Covers: return translate('metadata-setting-field-pipe.covers'); diff --git a/UI/Web/src/app/_pipes/plus-media-format.pipe.ts b/UI/Web/src/app/_pipes/plus-media-format.pipe.ts index e76488b3b..b72822e33 100644 --- a/UI/Web/src/app/_pipes/plus-media-format.pipe.ts +++ b/UI/Web/src/app/_pipes/plus-media-format.pipe.ts @@ -1,4 +1,4 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {PlusMediaFormat} from "../_models/series-detail/external-series-detail"; import {translate} from "@jsverse/transloco"; @@ -13,7 +13,7 @@ export class PlusMediaFormatPipe implements PipeTransform { case PlusMediaFormat.Manga: return translate('library-type-pipe.manga'); case PlusMediaFormat.Comic: - return translate('library-type-pipe.comic'); + return translate('library-type-pipe.comicVine'); case PlusMediaFormat.LightNovel: return translate('library-type-pipe.lightNovel'); case PlusMediaFormat.Book: diff --git a/UI/Web/src/app/_pipes/provider-image.pipe.ts b/UI/Web/src/app/_pipes/provider-image.pipe.ts index 80574ef3b..5d845a672 100644 --- a/UI/Web/src/app/_pipes/provider-image.pipe.ts +++ b/UI/Web/src/app/_pipes/provider-image.pipe.ts @@ -17,6 +17,8 @@ export class ProviderImagePipe implements PipeTransform { return `assets/images/ExternalServices/GoogleBooks${large ? '-lg' : ''}.png`; case ScrobbleProvider.Kavita: return `assets/images/logo-${large ? '64' : '32'}.png`; + case ScrobbleProvider.Cbr: + return `assets/images/ExternalServices/ComicBookRoundup.png`; } } diff --git a/UI/Web/src/app/_pipes/provider-name.pipe.ts b/UI/Web/src/app/_pipes/provider-name.pipe.ts deleted file mode 100644 index 1947b3f5f..000000000 --- a/UI/Web/src/app/_pipes/provider-name.pipe.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import {ScrobbleProvider} from "../_services/scrobbling.service"; - -@Pipe({ - name: 'providerName', - standalone: true -}) -export class ProviderNamePipe implements PipeTransform { - - transform(value: ScrobbleProvider): string { - switch (value) { - case ScrobbleProvider.AniList: - return 'AniList'; - case ScrobbleProvider.Mal: - return 'MAL'; - case ScrobbleProvider.Kavita: - return 'Kavita'; - case ScrobbleProvider.GoogleBooks: - return 'Google Books'; - } - } - -} diff --git a/UI/Web/src/app/_pipes/scrobble-provider-name.pipe.ts b/UI/Web/src/app/_pipes/scrobble-provider-name.pipe.ts index 7617f04ec..cc6e01449 100644 --- a/UI/Web/src/app/_pipes/scrobble-provider-name.pipe.ts +++ b/UI/Web/src/app/_pipes/scrobble-provider-name.pipe.ts @@ -12,6 +12,7 @@ export class ScrobbleProviderNamePipe implements PipeTransform { case ScrobbleProvider.AniList: return 'AniList'; case ScrobbleProvider.Mal: return 'MAL'; case ScrobbleProvider.Kavita: return 'Kavita'; + case ScrobbleProvider.Cbr: return 'Comicbook Roundup'; case ScrobbleProvider.GoogleBooks: return 'Google Books'; } } diff --git a/UI/Web/src/app/_services/scrobbling.service.ts b/UI/Web/src/app/_services/scrobbling.service.ts index f6c99732e..76b9212f4 100644 --- a/UI/Web/src/app/_services/scrobbling.service.ts +++ b/UI/Web/src/app/_services/scrobbling.service.ts @@ -12,9 +12,10 @@ import {UtilityService} from "../shared/_services/utility.service"; export enum ScrobbleProvider { Kavita = 0, - AniList= 1, + AniList = 1, Mal = 2, - GoogleBooks = 3 + GoogleBooks = 3, + Cbr = 4 } @Injectable({ diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index f8644748b..f221b2f1a 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -242,7 +242,7 @@ export class SeriesService { } updateMatch(seriesId: number, series: ExternalSeriesDetail) { - return this.httpClient.post(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId}${series.malId ? '&malId=' + series.malId : ''}`, {}, TextResonse); + return this.httpClient.post(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId || 0}&malId=${series.malId || 0}&cbrId=${series.cbrId || 0}`, {}, TextResonse); } updateDontMatch(seriesId: number, dontMatch: boolean) { diff --git a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts index 1c9dda05f..793737923 100644 --- a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts +++ b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts @@ -91,7 +91,7 @@ export class MatchSeriesModalComponent implements OnInit { data.tags = data.tags || []; data.genres = data.genres || []; - this.seriesService.updateMatch(this.series.id, data).subscribe(_ => { + this.seriesService.updateMatch(this.series.id, item.series).subscribe(_ => { this.save(); }); } diff --git a/UI/Web/src/app/admin/_models/metadata-setting-field.ts b/UI/Web/src/app/admin/_models/metadata-setting-field.ts index 0448e1af2..fb99e8a07 100644 --- a/UI/Web/src/app/admin/_models/metadata-setting-field.ts +++ b/UI/Web/src/app/admin/_models/metadata-setting-field.ts @@ -7,7 +7,14 @@ export enum MetadataSettingField { LocalizedName = 6, Covers = 7, AgeRating = 8, - People = 9 + People = 9, + + // Chapter fields + ChapterTitle = 10, + ChapterSummary = 11, + ChapterReleaseDate = 12, + ChapterPublisher = 13, + ChapterCovers = 14, } export const allMetadataSettingField = Object.keys(MetadataSettingField) diff --git a/UI/Web/src/app/admin/_models/metadata-settings.ts b/UI/Web/src/app/admin/_models/metadata-settings.ts index d88e14312..9743dd578 100644 --- a/UI/Web/src/app/admin/_models/metadata-settings.ts +++ b/UI/Web/src/app/admin/_models/metadata-settings.ts @@ -25,6 +25,14 @@ export interface MetadataSettings { enableStartDate: boolean; enableCoverImage: boolean; enableLocalizedName: boolean; + + enableChapterSummary: boolean; + enableChapterReleaseDate: boolean; + enableChapterTitle: boolean; + enableChapterPublisher: boolean; + enableChapterCoverImage: boolean; + + enableGenres: boolean; enableTags: boolean; firstLastPeopleNaming: boolean; diff --git a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.html b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.html index dd55d8069..658dfd054 100644 --- a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.html +++ b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.html @@ -89,6 +89,70 @@ } +
+ + +
{{t('chapter-header')}}
+
+ @if(settingsForm.get('enableChapterTitle'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('enableChapterSummary'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('enableChapterReleaseDate'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('enableChapterPublisher'); as formControl) { + + +
+ +
+
+
+ } +
+ +
+ @if(settingsForm.get('enableChapterCoverImage'); as formControl) { + + +
+ +
+
+
+ } +
+ @if(settingsForm.get('enablePeople'); as formControl) {
@@ -133,6 +197,7 @@ +
diff --git a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts index 0febac43e..5d0ed6566 100644 --- a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts +++ b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts @@ -79,6 +79,13 @@ export class ManageMetadataSettingsComponent implements OnInit { this.settingsForm.addControl('enableStartDate', new FormControl(settings.enableStartDate, [])); this.settingsForm.addControl('enableCoverImage', new FormControl(settings.enableCoverImage, [])); + + this.settingsForm.addControl('enableChapterTitle', new FormControl(settings.enableChapterTitle, [])); + this.settingsForm.addControl('enableChapterSummary', new FormControl(settings.enableChapterSummary, [])); + this.settingsForm.addControl('enableChapterReleaseDate', new FormControl(settings.enableChapterReleaseDate, [])); + this.settingsForm.addControl('enableChapterPublisher', new FormControl(settings.enableChapterPublisher, [])); + this.settingsForm.addControl('enableChapterCoverImage', new FormControl(settings.enableChapterCoverImage, [])); + this.settingsForm.addControl('blacklist', new FormControl((settings.blacklist || '').join(','), [])); this.settingsForm.addControl('whitelist', new FormControl((settings.whitelist || '').join(','), [])); this.settingsForm.addControl('firstLastPeopleNaming', new FormControl((settings.firstLastPeopleNaming), [])); diff --git a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts index 0b441d740..7628143d6 100644 --- a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts +++ b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts @@ -43,6 +43,7 @@ import {DefaultModalOptions} from "../../../_models/default-modal-options"; templateUrl: './all-collections.component.html', styleUrls: ['./all-collections.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, imports: [SideNavCompanionBarComponent, CardDetailLayoutComponent, CardItemComponent, AsyncPipe, DecimalPipe, TranslocoDirective, CollectionOwnerComponent, BulkOperationsComponent] }) diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html index 990fec904..861f87115 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html @@ -32,7 +32,7 @@
+ [ngbTooltip]="collectionTag.source | scrobbleProviderName" tabindex="0"> {{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}
diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts index bc45e0f2f..11a2df6f4 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts @@ -56,20 +56,20 @@ import {User} from "../../../_models/user"; import {ScrobbleProvider} from "../../../_services/scrobbling.service"; import {DefaultDatePipe} from "../../../_pipes/default-date.pipe"; import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe"; -import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe"; import { SmartCollectionDrawerComponent } from "../../../_single-module/smart-collection-drawer/smart-collection-drawer.component"; import {DefaultModalOptions} from "../../../_models/default-modal-options"; +import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe"; @Component({ - selector: 'app-collection-detail', - templateUrl: './collection-detail.component.html', - styleUrls: ['./collection-detail.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-collection-detail', + templateUrl: './collection-detail.component.html', + styleUrls: ['./collection-detail.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, imports: [SideNavCompanionBarComponent, CardActionablesComponent, ImageComponent, ReadMoreComponent, BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, TranslocoDirective, NgbTooltip, - DatePipe, DefaultDatePipe, ProviderImagePipe, ProviderNamePipe, AsyncPipe] + DatePipe, DefaultDatePipe, ProviderImagePipe, AsyncPipe, ScrobbleProviderNamePipe] }) export class CollectionDetailComponent implements OnInit, AfterContentChecked { diff --git a/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html b/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html index f6972f01d..55b91b4b0 100644 --- a/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html +++ b/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.html @@ -6,8 +6,8 @@ {{t('collection-via-label')}} + [ngbTooltip]="collection.source | scrobbleProviderName" + [attr.aria-label]="collection.source | scrobbleProviderName"> } } diff --git a/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.ts b/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.ts index 4e9756baa..009be4a46 100644 --- a/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.ts +++ b/UI/Web/src/app/collections/_components/collection-owner/collection-owner.component.ts @@ -1,23 +1,23 @@ import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; import {ScrobbleProvider} from "../../../_services/scrobbling.service"; import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe"; -import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe"; import {UserCollection} from "../../../_models/collection-tag"; import {TranslocoDirective} from "@jsverse/transloco"; import {AsyncPipe} from "@angular/common"; import {AccountService} from "../../../_services/account.service"; import {ImageComponent} from "../../../shared/image/image.component"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; +import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe"; @Component({ selector: 'app-collection-owner', imports: [ ProviderImagePipe, - ProviderNamePipe, TranslocoDirective, AsyncPipe, ImageComponent, - NgbTooltip + NgbTooltip, + ScrobbleProviderNamePipe ], templateUrl: './collection-owner.component.html', styleUrl: './collection-owner.component.scss', diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html index 00a6ceed5..cd0194fd8 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html @@ -17,7 +17,7 @@ @for (rating of ratings; track rating.provider + rating.averageScore) {
+ [popoverTitle]="rating.provider | scrobbleProviderName" popoverClass="sm-popover"> {{rating.averageScore}}% @@ -64,9 +64,11 @@ -
- {{rating.favoriteCount}} -
+ @if (rating.favoriteCount > 0) { +
+ {{rating.favoriteCount}} +
+ } @if (rating.providerUrl) { {{t('entry-label')}} diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts index eafe35812..f2a1c7f4c 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, DestroyRef, + Component, + DestroyRef, inject, Input, OnInit, @@ -13,7 +14,6 @@ import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe"; import {NgbModal, NgbPopover} from "@ng-bootstrap/ng-bootstrap"; import {LoadingComponent} from "../../../shared/loading/loading.component"; import {LibraryType} from "../../../_models/library/library"; -import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe"; import {NgxStarsModule} from "ngx-stars"; import {ThemeService} from "../../../_services/theme.service"; import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service"; @@ -23,15 +23,16 @@ import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; import {ImageService} from "../../../_services/image.service"; import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common"; import {RatingModalComponent} from "../rating-modal/rating-modal.component"; +import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe"; @Component({ - selector: 'app-external-rating', - imports: [ProviderImagePipe, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule, ImageComponent, - TranslocoDirective, SafeHtmlPipe, NgOptimizedImage, AsyncPipe, NgTemplateOutlet], - templateUrl: './external-rating.component.html', - styleUrls: ['./external-rating.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None + selector: 'app-external-rating', + imports: [ProviderImagePipe, NgbPopover, LoadingComponent, NgxStarsModule, ImageComponent, + TranslocoDirective, SafeHtmlPipe, NgOptimizedImage, AsyncPipe, NgTemplateOutlet, ScrobbleProviderNamePipe], + templateUrl: './external-rating.component.html', + styleUrls: ['./external-rating.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None }) export class ExternalRatingComponent implements OnInit { diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index 5c83db623..d8a0ff752 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -128,6 +128,11 @@ export class LibrarySettingsModalComponent implements OnInit { return libType === LibraryType.Manga || libType === LibraryType.LightNovel; } + get IsMetadataDownloadEligible() { + const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType; + return libType === LibraryType.Manga || libType === LibraryType.LightNovel || libType === LibraryType.ComicVine; + } + ngOnInit(): void { if (this.library === undefined) { this.isAddLibrary = true; @@ -141,11 +146,19 @@ export class LibrarySettingsModalComponent implements OnInit { if (this.library && !(this.library.type === LibraryType.Manga || this.library.type === LibraryType.LightNovel) ) { this.libraryForm.get('allowScrobbling')?.setValue(false); - this.libraryForm.get('allowMetadataMatching')?.setValue(false); this.libraryForm.get('allowScrobbling')?.disable(); - this.libraryForm.get('allowMetadataMatching')?.disable(); + + if (this.IsMetadataDownloadEligible) { + this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching); + this.libraryForm.get('allowMetadataMatching')?.enable(); + } else { + this.libraryForm.get('allowMetadataMatching')?.setValue(false); + this.libraryForm.get('allowMetadataMatching')?.disable(); + } } + + this.libraryForm.get('name')?.valueChanges.pipe( debounceTime(100), distinctUntilChanged(), @@ -208,11 +221,16 @@ export class LibrarySettingsModalComponent implements OnInit { if (!this.IsKavitaPlusEligible) { this.libraryForm.get('allowScrobbling')?.disable(); - this.libraryForm.get('allowMetadataMatching')?.disable(); } else { this.libraryForm.get('allowScrobbling')?.enable(); - this.libraryForm.get('allowMetadataMatching')?.enable(); } + + if (this.IsMetadataDownloadEligible) { + this.libraryForm.get('allowMetadataMatching')?.enable(); + } else { + this.libraryForm.get('allowMetadataMatching')?.disable(); + } + this.cdRef.markForCheck(); }), takeUntilDestroyed(this.destroyRef) @@ -231,7 +249,7 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('manageReadingLists')?.setValue(this.library.manageReadingLists); this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships); this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false); - this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsKavitaPlusEligible ? this.library.allowMetadataMatching : false); + this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false); this.selectedFolders = this.library.folders; this.madeChanges = false; diff --git a/UI/Web/src/assets/images/ExternalServices/ComicBookRoundup.png b/UI/Web/src/assets/images/ExternalServices/ComicBookRoundup.png new file mode 100644 index 0000000000000000000000000000000000000000..93d29c25e99bd54d59dac7a83f29b0a2e25af0cf GIT binary patch literal 3720 zcmaJ^c{r478=nZ-E3$>g&|;g-SZ0uAW-Kvcs3g=dgTc%&GuDbQ$i8Mtg-S@I)IlR1 z%1(Sqhz5lgYD7$-eB+!t-}gt?d9UkzpXYww`*;6-_x;?@dtGmmql2}in35O(0Fbn` z!QlC$(dNBPnE$LuFjnOcicG94)0yVO3?tA<01G0`iv+Z#5PV5^5`h>I+(j}000e!= zF0M>hoIT2$MgbExeZb+AAU+xZFfj`cB6tUom_RR*FPUlz;<6usfMlX6=$JkZiVH%M z{Kz(ubdqzVgNt`$fH#r|GCK$~2}kh*P)JMyFq{%dWuU@ML4UgqS_sn=#8>)v3X~um?w`O^#?M0WO9lxi1VLb6D1<`U%wm^8WzjTZQ; z104NmOd7+F76e2)>jQBFZ!&e$-rSzQv&E3;fq^~(iqzxt zqkpgcZ!GaQHT#L()c$)cgf9lN*&P4ZEWeic9k@CDr}g>9KZl1z<)0Wj|7Zm2(rN(! zu?Aa=g-iI$c{g#FoMDxX2@Q=H4VLvbX(3sx`5jQJ_&L{uN-Cm7k2C*}+lU4xGmRVDr>PzX36h zBg!-W4$WLPZ|yB-V~NL>kG;1AuvTle>JeAe{9^+Bo^e6&nojYs^}E$TXhIR7JzS=t zKw;dSJ*OU*y8PUFe0{~Q>#auy`&~oI!6`6mR`=@&tb+S4_C)#X z9bI4&vGYkgCJr;dZ01q&&grDiqqEWuGiN0B&X7(aLZug^687XaH$(TUZOJ=(T9cKq z^)tt?NVnFP8&y$LIpfKwp0teFZiz-u7M9CfwmR$iX=!ATS_o>2==v;W8{^pg+n#%m zR)yHuJRUnCW+xXda}0b^%#uKJA*vc%9PI60JWs4JJmuf*?pnVhg3(!t)X-4>AW|yK zp8Zth<~Aj5yuljMAG$;xPsN7OAF$Jc)`td^*?b!q?5ZrsT-YK95R6x z;u_xfE%#;{;WABleoNMUf)o$YxNuK?k6B7@@uN(zy^UVk z`K^@#j@66%IXhu+bypp^)Rgqt<9fc?m&P+ntEyyVcJ8#8c<&B8ywrSXuc)|`RT8GR zE2B^exAJi!<$-~U;U8sXLOooU?D)zg8>?rr`!0TTn7J~091I4l%ISPndZq&)`0v6T zzH$zfl__tAPm(_{eQ-O{DQ%G4SkZ7&@@Rnc)>X~%fGcjA2-mi>Fq6i@0)TJ-$;q12 zk+%j$#$!D^tTKD9KfYVi5sKt$;kLPzUtM2cAAQ~M+*DUfpcQ>Y8aGthvSqS7H|}V; z$R1i!V%B=&MSGv9?(zm!SBbbXvSNkXq@CX3KEGe1R}G|OC-XQqspqw-SXon|7(hQfaeYmtvlYRSn2sn{g3`s~xh{?~FPj0)KWe2aOL_!rFT;f^8XvvCUS?yxq zq&ixLB>yD}Y>Est*7I*H*Q`;D0R^GiU zun67=7-4pKOFS!?g&%{s3n4;xSFpGJM=Hu1D!ik$wbe;uUR(d3&Z(3?;(hSf25%l) zTKe4Dc3hSf9YXA8p=^Y_k`(N%a#YOx70s0;UkLY`tVyPyE$R7^$_#$ne>wi<%{3{E z;3%>Yk+c8YBm6<)1B72Tq0$21(W)~ zg5-HzN-|4@9lG7)#BScLJ9oC&b#t@hmwcp=UEzUAqIKas*%OvU%>mPCHsu(CYGPFG zN=EVR)Yzn?%gY9YF`Dq^M`Y9%<-Vs0p=T2r?)LUNTZOke z+H0#c3f(n;>=kvp2WiPll`gscRc65de$dJ|(rD>@x$=tn{I!PlY}Vvl70ZWWTyZ2y zaC(n%-jwHRbBmwr{5Iylv}V6`Jg^8-EG+bcDts+B zdN?LJU;oO-$)D`r6fS!CUf;Kbsxw7>v-@Onngb623s4(mP`)N~;AGdI?U~;uYhNy< z6qjKm1c;YSgm&$!M;=?uRU*NWj&zASpFZJaZwperYE=Z9g0GatzlE?0$iavyQO%7@ zMyqRAC2^<&S`Q^>>bpxEoCQ-O>e}FsZOS;$y<)bjs*2|9!%f|Vjy>B}IvpO#+{$Zi z8IUxd_%1%qj-L4bq(XVm9z_fWLl^TJPIc+%u!`xP2>T-6xmVx=b?AQDz`(7tq>d*a zUj-?j`&MZxH+o%QJr{~JVFH})KT}}i9sT>y3(814C!^D!9orJ@tZqK5uJS^;%jWVz zsA6gb7qLBOv~^K%Z<{4C$4QIGs#hD5?qcrl8yUIkEgsz4rdix}U;fDUcoM5y$!zL7 z{t7r)Zai#ZPf3{O4e!!BBPW+WXg>^rMzu4E@3yuNoIGCZ3{G-|N3kSp#&|v{%Nu%K z^DiHYc%EjcH{B=guDtmliESLah~dfmlDoTJ7B@?=H&(;E{#fAc;~qT-OdK=|I%s@> zanYUqL-DW!Oh{;BJ<}K*cQ)~vVoy-F@4`oU+D7`VjZvQwwpki<;Fa0&rszqi%Q%~B z4>#tVm3E(PG41c{^_m0jo_HxxucfsXI`lzFX|j#06aTfr)ubSl)luiKFPWKlMGA7; zJ!Y()BGmptS{c=wQhBsGFMZIcc*qcO%9){eINi!wd_Vo*YgO3`vS?|B=vw&Eg%e}U zuMdxCk7~lG6U(0fo*gT`nzdost;wf(Bh8W-(l4ArmrQ)6z&4jf)}(I)CmINd%u9-k zi-go(biE~aWp=5?J*LsN3reHHovH*TJIq$qG_^DdVB>j2jTC;cl}az-U@dnPUWUqW zTpy+Tg-{%?RYqTjPjR`{mYS40*Xpxu|6VL-^oMo(tDu>?Bom+BeHuAEmv|fw{`$*A z&*v7Gh7$TE03HX52Y2SEt4{?*3Xg>4jeklIjTcBh*Hl@~RI`7#U-0q?{76_=eoo_%r%DRe7TY&$S6|$2CF3u`nYt? zAZm5LR^iOvlBxJ%$-4 z+sf(#4aHI@l+|>L89hb*Co}G8R+q20dvVsCAH1t?qThchi}8PRX2Etg?hP3A>FVK{ elpogTwg7ZRb>>fAx{2KUH?+k%U}`K+#r_u{TZPyF literal 0 HcmV?d00001 diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index f9221bea9..c63967973 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -806,6 +806,17 @@ "enable-cover-image-tooltip": "Allow Kavita to write the cover image for the Series", "enable-start-date-label": "Start Date", "enable-start-date-tooltip": "Allow Start Date of Series to be written to the Series", + + "enable-chapter-title-label": "Title", + "enable-chapter-title-tooltip": "Allow Title of Chapter/Issue to be written", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}", + "enable-chapter-release-date-label": "Release Date", + "enable-chapter-release-date-tooltip": "Allow Release Date of Chapter/Issue to be written", + "enable-chapter-publisher-label": "Publisher", + "enable-chapter-publisher-tooltip": "Allow Publisher of Chapter/Issue to be written", + "enable-chapter-cover-label": "Chapter Cover", + "enable-chapter-cover-tooltip": "Allow Cover of Chapter/Issue to be set", "enable-genres-label": "Genres", "enable-genres-tooltip": "Allow Series Genres to be written.", "enable-tags-label": "Tags", @@ -827,7 +838,8 @@ "first-last-name-tooltip": "Ensure People's names are written First then Last", "person-roles-label": "Roles", "overrides-label": "Overrides", - "overrides-description": "Allow Kavita to write over locked fields." + "overrides-description": "Allow Kavita to write over locked fields.", + "chapter-header": "Chapter Fields" }, "book-line-overlay": { @@ -2686,7 +2698,12 @@ "start-date": "{{manage-metadata-settings.enable-start-date-label}}", "genres": "{{metadata-fields.genres-title}}", "tags": "{{metadata-fields.tags-title}}", - "localized-name": "{{edit-series-modal.localized-name-label}}" + "localized-name": "{{edit-series-modal.localized-name-label}}", + "chapter-release-date": "Release Date (Chapter)", + "chapter-summary": "Summary (Chapter)", + "chapter-covers": "Covers (Chapter)", + "chapter-publisher": "{{person-role-pipe.publisher}} (Chapter)", + "chapter-title": "Title (Chapter)" },