diff --git a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs b/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs
new file mode 100644
index 000000000..e1f585806
--- /dev/null
+++ b/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs
@@ -0,0 +1,178 @@
+using API.Helpers;
+using Xunit;
+
+namespace API.Tests.Helpers;
+
+public class BookSortTitlePrefixHelperTests
+{
+ [Theory]
+ [InlineData("The Avengers", "Avengers")]
+ [InlineData("A Game of Thrones", "Game of Thrones")]
+ [InlineData("An American Tragedy", "American Tragedy")]
+ public void TestEnglishPrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("El Quijote", "Quijote")]
+ [InlineData("La Casa de Papel", "Casa de Papel")]
+ [InlineData("Los Miserables", "Miserables")]
+ [InlineData("Las Vegas", "Vegas")]
+ [InlineData("Un Mundo Feliz", "Mundo Feliz")]
+ [InlineData("Una Historia", "Historia")]
+ public void TestSpanishPrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("Le Petit Prince", "Petit Prince")]
+ [InlineData("La Belle et la Bête", "Belle et la Bête")]
+ [InlineData("Les Misérables", "Misérables")]
+ [InlineData("Un Amour de Swann", "Amour de Swann")]
+ [InlineData("Une Vie", "Vie")]
+ [InlineData("Des Souris et des Hommes", "Souris et des Hommes")]
+ public void TestFrenchPrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("Der Herr der Ringe", "Herr der Ringe")]
+ [InlineData("Die Verwandlung", "Verwandlung")]
+ [InlineData("Das Kapital", "Kapital")]
+ [InlineData("Ein Sommernachtstraum", "Sommernachtstraum")]
+ [InlineData("Eine Geschichte", "Geschichte")]
+ public void TestGermanPrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("Il Nome della Rosa", "Nome della Rosa")]
+ [InlineData("La Divina Commedia", "Divina Commedia")]
+ [InlineData("Lo Hobbit", "Hobbit")]
+ [InlineData("Gli Ultimi", "Ultimi")]
+ [InlineData("Le Città Invisibili", "Città Invisibili")]
+ [InlineData("Un Giorno", "Giorno")]
+ [InlineData("Una Notte", "Notte")]
+ public void TestItalianPrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("O Alquimista", "Alquimista")]
+ [InlineData("A Moreninha", "Moreninha")]
+ [InlineData("Os Lusíadas", "Lusíadas")]
+ [InlineData("As Meninas", "Meninas")]
+ [InlineData("Um Defeito de Cor", "Defeito de Cor")]
+ [InlineData("Uma História", "História")]
+ public void TestPortuguesePrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("", "")] // Empty string returns empty
+ [InlineData("Book", "Book")] // Single word, no change
+ [InlineData("Avengers", "Avengers")] // No prefix, no change
+ public void TestNoPrefixCases(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("The", "The")] // Just a prefix word alone
+ [InlineData("A", "A")] // Just single letter prefix alone
+ [InlineData("Le", "Le")] // French prefix alone
+ public void TestPrefixWordAlone(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("THE AVENGERS", "AVENGERS")] // All caps
+ [InlineData("the avengers", "avengers")] // All lowercase
+ [InlineData("The AVENGERS", "AVENGERS")] // Mixed case
+ [InlineData("tHe AvEnGeRs", "AvEnGeRs")] // Random case
+ public void TestCaseInsensitivity(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("Then Came You", "Then Came You")] // "The" + "n" = not a prefix
+ [InlineData("And Then There Were None", "And Then There Were None")] // "An" + "d" = not a prefix
+ [InlineData("Elsewhere", "Elsewhere")] // "El" + "sewhere" = not a prefix (no space)
+ [InlineData("Lesson Plans", "Lesson Plans")] // "Les" + "son" = not a prefix (no space)
+ [InlineData("Theory of Everything", "Theory of Everything")] // "The" + "ory" = not a prefix
+ public void TestFalsePositivePrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("The ", "The ")] // Prefix with only space after - returns original
+ [InlineData("La ", "La ")] // Same for other languages
+ [InlineData("El ", "El ")] // Same for Spanish
+ public void TestPrefixWithOnlySpaceAfter(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("The Multiple Spaces", " Multiple Spaces")] // Doesn't trim extra spaces from remainder
+ [InlineData("Le Petit Prince", " Petit Prince")] // Leading space preserved in remainder
+ public void TestSpaceHandling(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("The The Matrix", "The Matrix")] // Removes first "The", leaves second
+ [InlineData("A A Clockwork Orange", "A Clockwork Orange")] // Removes first "A", leaves second
+ [InlineData("El El Cid", "El Cid")] // Spanish version
+ public void TestRepeatedPrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("L'Étranger", "L'Étranger")] // French contraction - no space, no change
+ [InlineData("D'Artagnan", "D'Artagnan")] // Contraction - no space, no change
+ [InlineData("The-Matrix", "The-Matrix")] // Hyphen instead of space - no change
+ [InlineData("The.Avengers", "The.Avengers")] // Period instead of space - no change
+ public void TestNonSpaceSeparators(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("三国演义", "三国演义")] // Chinese - no processing due to CJK detection
+ [InlineData("한국어", "한국어")] // Korean - not in CJK range, would be processed normally
+ public void TestCjkLanguages(string inputString, string expected)
+ {
+ // NOTE: These don't do anything, I am waiting for user input on if these are needed
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("नमस्ते दुनिया", "नमस्ते दुनिया")] // Hindi - not CJK, processed normally
+ [InlineData("مرحبا بالعالم", "مرحبا بالعالم")] // Arabic - not CJK, processed normally
+ [InlineData("שלום עולם", "שלום עולם")] // Hebrew - not CJK, processed normally
+ public void TestNonLatinNonCjkScripts(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+
+ [Theory]
+ [InlineData("в мире", "мире")] // Russian "в" (in) - should be removed
+ [InlineData("на столе", "столе")] // Russian "на" (on) - should be removed
+ [InlineData("с друзьями", "друзьями")] // Russian "с" (with) - should be removed
+ public void TestRussianPrefixes(string inputString, string expected)
+ {
+ Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString));
+ }
+}
diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs
index acc0345b1..c337d2311 100644
--- a/API.Tests/Services/ScannerServiceTests.cs
+++ b/API.Tests/Services/ScannerServiceTests.cs
@@ -972,4 +972,27 @@ public class ScannerServiceTests : AbstractDbTest
Assert.Contains(postLib.Series, x => x.Name == "Immoral Guild");
Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild");
}
+
+ [Fact]
+ public async Task ScanLibrary_SortName_NoPrefix()
+ {
+ const string testcase = "Series with Prefix - Book.json";
+
+ var library = await _scannerHelper.GenerateScannerData(testcase);
+
+ library.RemovePrefixForSortName = true;
+ UnitOfWork.LibraryRepository.Update(library);
+ await UnitOfWork.CommitAsync();
+
+ var scanner = _scannerHelper.CreateServices();
+ await scanner.ScanLibrary(library.Id);
+
+ var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
+
+ Assert.NotNull(postLib);
+ Assert.Equal(1, postLib.Series.Count);
+
+ Assert.Equal("The Avengers", postLib.Series.First().Name);
+ Assert.Equal("Avengers", postLib.Series.First().SortName);
+ }
}
diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json
new file mode 100644
index 000000000..fc2bee18c
--- /dev/null
+++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json
@@ -0,0 +1,3 @@
+[
+ "The Avengers/The Avengers vol 1.pdf"
+]
\ No newline at end of file
diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs
index c09011b77..8f9b18317 100644
--- a/API/Controllers/LibraryController.cs
+++ b/API/Controllers/LibraryController.cs
@@ -624,6 +624,8 @@ public class LibraryController : BaseApiController
library.AllowScrobbling = dto.AllowScrobbling;
library.AllowMetadataMatching = dto.AllowMetadataMatching;
library.EnableMetadata = dto.EnableMetadata;
+ library.RemovePrefixForSortName = dto.RemovePrefixForSortName;
+
library.LibraryFileTypes = dto.FileGroupTypes
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
.Distinct()
diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs
index 7b38379c9..bd72ad2f0 100644
--- a/API/DTOs/LibraryDto.cs
+++ b/API/DTOs/LibraryDto.cs
@@ -70,4 +70,8 @@ public sealed record LibraryDto
/// Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF)
///
public bool EnableMetadata { get; set; } = true;
+ ///
+ /// Should Kavita remove sort articles "The" for the sort name
+ ///
+ public bool RemovePrefixForSortName { get; set; } = false;
}
diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs
index 68d2417ec..d7f314208 100644
--- a/API/DTOs/UpdateLibraryDto.cs
+++ b/API/DTOs/UpdateLibraryDto.cs
@@ -30,6 +30,8 @@ public sealed record UpdateLibraryDto
public bool AllowMetadataMatching { get; init; }
[Required]
public bool EnableMetadata { get; init; }
+ [Required]
+ public bool RemovePrefixForSortName { get; init; }
///
/// What types of files to allow the scanner to pickup
///
diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs
new file mode 100644
index 000000000..165663f3d
--- /dev/null
+++ b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs
@@ -0,0 +1,3724 @@
+//
+using System;
+using System.Collections.Generic;
+using API.Data;
+using API.Entities.MetadataMatching;
+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("20250629153840_LibraryRemoveSortPrefix")]
+ partial class LibraryRemoveSortPrefix
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
+
+ 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.AppUserChapterRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("HasBeenRated")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("REAL");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserChapterRating");
+ });
+
+ 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.AppUserReadingProfile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AllowAutomaticWebtoonReaderDetection")
+ .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("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("DisableWidthOverride")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmulateBook")
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibraryIds")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfScrollMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfSpreadMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PdfTheme")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesIds")
+ .HasColumnType("TEXT");
+
+ b.Property("ShowScreenHints")
+ .HasColumnType("INTEGER");
+
+ b.Property("SwipeToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("WidthOverride")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserReadingProfiles");
+ });
+
+ 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("AverageExternalRating")
+ .HasColumnType("REAL");
+
+ 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("KPlusOverrides")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("[]");
+
+ 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