diff --git a/API.Tests/Helpers/OrderableHelperTests.cs b/API.Tests/Helpers/OrderableHelperTests.cs
index a6d741be1..15f9e6268 100644
--- a/API.Tests/Helpers/OrderableHelperTests.cs
+++ b/API.Tests/Helpers/OrderableHelperTests.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using API.Entities;
using API.Helpers;
@@ -49,17 +50,14 @@ public class OrderableHelperTests
[Fact]
public void ReorderItems_InvalidPosition_NoChange()
{
- // Arrange
var items = new List
{
new AppUserSideNavStream { Id = 1, Order = 0, Name = "A" },
new AppUserSideNavStream { Id = 2, Order = 1, Name = "A" },
};
- // Act
OrderableHelper.ReorderItems(items, 2, 3); // Position 3 is out of range
- // Assert
Assert.Equal(1, items[0].Id); // Item 1 should remain at position 0
Assert.Equal(2, items[1].Id); // Item 2 should remain at position 1
}
@@ -80,7 +78,6 @@ public class OrderableHelperTests
[Fact]
public void ReorderItems_DoubleMove()
{
- // Arrange
var items = new List
{
new AppUserSideNavStream { Id = 1, Order = 0, Name = "0" },
@@ -94,7 +91,6 @@ public class OrderableHelperTests
// Move 4 -> 1
OrderableHelper.ReorderItems(items, 5, 1);
- // Assert
Assert.Equal(1, items[0].Id);
Assert.Equal(0, items[0].Order);
Assert.Equal(5, items[1].Id);
@@ -109,4 +105,98 @@ public class OrderableHelperTests
Assert.Equal("034125", string.Join("", items.Select(s => s.Name)));
}
+
+ private static List CreateTestReadingListItems(int count = 4)
+ {
+ var items = new List();
+
+ for (var i = 0; i < count; i++)
+ {
+ items.Add(new ReadingListItem() { Id = i + 1, Order = count, ReadingListId = i + 1});
+ }
+
+ return items;
+ }
+
+ [Fact]
+ public void ReorderItems_MoveItemToBeginning_CorrectOrder()
+ {
+ var items = CreateTestReadingListItems();
+
+ OrderableHelper.ReorderItems(items, 3, 0);
+
+ Assert.Equal(3, items[0].Id);
+ Assert.Equal(1, items[1].Id);
+ Assert.Equal(2, items[2].Id);
+ Assert.Equal(4, items[3].Id);
+
+ for (var i = 0; i < items.Count; i++)
+ {
+ Assert.Equal(i, items[i].Order);
+ }
+ }
+
+ [Fact]
+ public void ReorderItems_MoveItemToEnd_CorrectOrder()
+ {
+ var items = CreateTestReadingListItems();
+
+ OrderableHelper.ReorderItems(items, 1, 3);
+
+ Assert.Equal(2, items[0].Id);
+ Assert.Equal(3, items[1].Id);
+ Assert.Equal(4, items[2].Id);
+ Assert.Equal(1, items[3].Id);
+
+ for (var i = 0; i < items.Count; i++)
+ {
+ Assert.Equal(i, items[i].Order);
+ }
+ }
+
+ [Fact]
+ public void ReorderItems_MoveItemToMiddle_CorrectOrder()
+ {
+ var items = CreateTestReadingListItems();
+
+ OrderableHelper.ReorderItems(items, 4, 2);
+
+ Assert.Equal(1, items[0].Id);
+ Assert.Equal(2, items[1].Id);
+ Assert.Equal(4, items[2].Id);
+ Assert.Equal(3, items[3].Id);
+
+ for (var i = 0; i < items.Count; i++)
+ {
+ Assert.Equal(i, items[i].Order);
+ }
+ }
+
+ [Fact]
+ public void ReorderItems_MoveItemToOutOfBoundsPosition_MovesToEnd()
+ {
+ var items = CreateTestReadingListItems();
+
+ OrderableHelper.ReorderItems(items, 2, 10);
+
+ Assert.Equal(1, items[0].Id);
+ Assert.Equal(3, items[1].Id);
+ Assert.Equal(4, items[2].Id);
+ Assert.Equal(2, items[3].Id);
+
+ for (var i = 0; i < items.Count; i++)
+ {
+ Assert.Equal(i, items[i].Order);
+ }
+ }
+
+ [Fact]
+ public void ReorderItems_NegativePosition_ThrowsArgumentException()
+ {
+ var items = CreateTestReadingListItems();
+
+ Assert.Throws(() =>
+ OrderableHelper.ReorderItems(items, 2, -1)
+ );
+ }
}
diff --git a/API.Tests/Helpers/StringHelperTests.cs b/API.Tests/Helpers/StringHelperTests.cs
index 1eac0a6ed..8f845c9b0 100644
--- a/API.Tests/Helpers/StringHelperTests.cs
+++ b/API.Tests/Helpers/StringHelperTests.cs
@@ -10,6 +10,10 @@ public class StringHelperTests
"A Perfect Marriage Becomes a Perfect Affair!
Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?
",
"A Perfect Marriage Becomes a Perfect Affair!
Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?
"
)]
+ [InlineData(
+ "Blog | Twitter | Pixiv | Pawoo
",
+ "Blog | Twitter | Pixiv | Pawoo
"
+ )]
public void TestSquashBreaklines(string input, string expected)
{
Assert.Equal(expected, StringHelper.SquashBreaklines(input));
@@ -28,4 +32,15 @@ public class StringHelperTests
{
Assert.Equal(expected, StringHelper.RemoveSourceInDescription(input));
}
+
+
+ [Theory]
+ [InlineData(
+"""Pawoo
""",
+"""Pawoo"""
+ )]
+ public void TestCorrectUrls(string input, string expected)
+ {
+ Assert.Equal(expected, StringHelper.CorrectUrls(input));
+ }
}
diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs
index 5405513d6..2f12aa1fe 100644
--- a/API/Controllers/LibraryController.cs
+++ b/API/Controllers/LibraryController.cs
@@ -213,7 +213,6 @@ public class LibraryController : BaseApiController
var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username);
await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24));
- _logger.LogDebug("Caching libraries for {Key}", cacheKey);
return Ok(ret);
}
@@ -419,8 +418,7 @@ public class LibraryController : BaseApiController
.Distinct()
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
- var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
- new List() {dto.FolderPath});
+ var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]);
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
diff --git a/API/Controllers/LocaleController.cs b/API/Controllers/LocaleController.cs
index 3117a9b41..6e3a2ec78 100644
--- a/API/Controllers/LocaleController.cs
+++ b/API/Controllers/LocaleController.cs
@@ -46,8 +46,8 @@ public class LocaleController : BaseApiController
}
var ret = _localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f);
- await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(7));
+ await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(1));
- return Ok();
+ return Ok(ret);
}
}
diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs
index d4bc8a1fe..38a5ad482 100644
--- a/API/Controllers/ReaderController.cs
+++ b/API/Controllers/ReaderController.cs
@@ -803,7 +803,7 @@ public class ReaderController : BaseApiController
///
///
[HttpGet("time-left")]
- [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = ["seriesId"])]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId"])]
public async Task> GetEstimateToCompletion(int seriesId)
{
var userId = User.GetUserId();
diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs
index 6ec781758..6c9be6c75 100644
--- a/API/Controllers/ReadingListController.cs
+++ b/API/Controllers/ReadingListController.cs
@@ -6,6 +6,7 @@ using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.ReadingLists;
+using API.Entities.Enums;
using API.Extensions;
using API.Helpers;
using API.Services;
@@ -23,13 +24,15 @@ public class ReadingListController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IReadingListService _readingListService;
private readonly ILocalizationService _localizationService;
+ private readonly IReaderService _readerService;
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService,
- ILocalizationService localizationService)
+ ILocalizationService localizationService, IReaderService readerService)
{
_unitOfWork = unitOfWork;
_readingListService = readingListService;
_localizationService = localizationService;
+ _readerService = readerService;
}
///
@@ -128,7 +131,7 @@ public class ReadingListController : BaseApiController
}
///
- /// Deletes a list item from the list. Will reorder all item positions afterwards
+ /// Deletes a list item from the list. Item orders will update as a result.
///
///
///
@@ -452,26 +455,38 @@ public class ReadingListController : BaseApiController
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
}
+
///
- /// Returns a list of characters associated with the reading list
+ /// Returns a list of a given role associated with the reading list
+ ///
+ ///
+ /// PersonRole
+ ///
+ [HttpGet("people")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId", "role"])]
+ public ActionResult> GetPeopleByRoleForList(int readingListId, PersonRole role)
+ {
+ return Ok(_unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role));
+ }
+
+ ///
+ /// Returns all people in given roles for a reading list
///
///
///
- [HttpGet("characters")]
- [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)]
- public ActionResult> GetCharactersForList(int readingListId)
+ [HttpGet("all-people")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId"])]
+ public async Task>> GetAllPeopleForList(int readingListId)
{
- return Ok(_unitOfWork.ReadingListRepository.GetReadingListCharactersAsync(readingListId));
+ return Ok(await _unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId));
}
-
-
///
/// Returns the next chapter within the reading list
///
///
///
- /// Chapter Id for next item, -1 if nothing exists
+ /// Chapter ID for next item, -1 if nothing exists
[HttpGet("next-chapter")]
public async Task> GetNextChapter(int currentChapterId, int readingListId)
{
@@ -577,4 +592,26 @@ public class ReadingListController : BaseApiController
return Ok();
}
+
+ ///
+ /// Returns random information about a Reading List
+ ///
+ ///
+ ///
+ [HttpGet("info")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["readingListId"])]
+ public async Task> GetReadingListInfo(int readingListId)
+ {
+ var result = await _unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId);
+
+ if (result == null) return Ok(null);
+
+ var timeEstimate = _readerService.GetTimeEstimate(result.WordCount, result.Pages, result.IsAllEpub);
+
+ result.MinHoursToRead = timeEstimate.MinHours;
+ result.AvgHoursToRead = timeEstimate.AvgHours;
+ result.MaxHoursToRead = timeEstimate.MaxHours;
+
+ return Ok(result);
+ }
}
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index dadfc74b7..f64e5e1b3 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -238,7 +238,8 @@ public class SeriesController : BaseApiController
// Trigger a refresh when we are moving from a locked image to a non-locked
needsRefreshMetadata = true;
series.CoverImage = null;
- series.CoverImageLocked = updateSeries.CoverImageLocked;
+ series.CoverImageLocked = false;
+ _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id);
series.ResetColorScape();
}
diff --git a/API/DTOs/ReadingLists/ReadingListCast.cs b/API/DTOs/ReadingLists/ReadingListCast.cs
new file mode 100644
index 000000000..4532df7d5
--- /dev/null
+++ b/API/DTOs/ReadingLists/ReadingListCast.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace API.DTOs.ReadingLists;
+
+public class ReadingListCast
+{
+ public ICollection Writers { get; set; } = [];
+ public ICollection CoverArtists { get; set; } = [];
+ public ICollection Publishers { get; set; } = [];
+ public ICollection Characters { get; set; } = [];
+ public ICollection Pencillers { get; set; } = [];
+ public ICollection Inkers { get; set; } = [];
+ public ICollection Imprints { get; set; } = [];
+ public ICollection Colorists { get; set; } = [];
+ public ICollection Letterers { get; set; } = [];
+ public ICollection Editors { get; set; } = [];
+ public ICollection Translators { get; set; } = [];
+ public ICollection Teams { get; set; } = [];
+ public ICollection Locations { get; set; } = [];
+}
diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs
index 139039bf5..6508e7bd4 100644
--- a/API/DTOs/ReadingLists/ReadingListDto.cs
+++ b/API/DTOs/ReadingLists/ReadingListDto.cs
@@ -1,4 +1,5 @@
using System;
+using API.Entities.Enums;
using API.Entities.Interfaces;
namespace API.DTOs.ReadingLists;
@@ -43,6 +44,10 @@ public class ReadingListDto : IHasCoverImage
/// Maximum Month the Reading List starts
///
public int EndingMonth { get; set; }
+ ///
+ /// The highest age rating from all Series within the reading list
+ ///
+ public required AgeRating AgeRating { get; set; } = AgeRating.Unknown;
public void ResetColorScape()
{
diff --git a/API/DTOs/ReadingLists/ReadingListInfoDto.cs b/API/DTOs/ReadingLists/ReadingListInfoDto.cs
new file mode 100644
index 000000000..bd95b9226
--- /dev/null
+++ b/API/DTOs/ReadingLists/ReadingListInfoDto.cs
@@ -0,0 +1,26 @@
+using API.DTOs.Reader;
+using API.Entities.Interfaces;
+
+namespace API.DTOs.ReadingLists;
+
+public class ReadingListInfoDto : IHasReadTimeEstimate
+{
+ ///
+ /// Total Pages across all Reading List Items
+ ///
+ public int Pages { get; set; }
+ ///
+ /// Total Word count across all Reading List Items
+ ///
+ public long WordCount { get; set; }
+ ///
+ /// Are ALL Reading List Items epub
+ ///
+ public bool IsAllEpub { get; set; }
+ ///
+ public int MinHoursToRead { get; set; }
+ ///
+ public int MaxHoursToRead { get; set; }
+ ///
+ public float AvgHoursToRead { get; set; }
+}
diff --git a/API/DTOs/ReadingLists/ReadingListItemDto.cs b/API/DTOs/ReadingLists/ReadingListItemDto.cs
index f1238d333..4fca5360c 100644
--- a/API/DTOs/ReadingLists/ReadingListItemDto.cs
+++ b/API/DTOs/ReadingLists/ReadingListItemDto.cs
@@ -25,7 +25,7 @@ public class ReadingListItemDto
///
/// Release Date from Chapter
///
- public DateTime ReleaseDate { get; set; }
+ public DateTime? ReleaseDate { get; set; }
///
/// Used internally only
///
@@ -33,7 +33,7 @@ public class ReadingListItemDto
///
/// The last time a reading list item (underlying chapter) was read by current authenticated user
///
- public DateTime LastReadingProgressUtc { get; set; }
+ public DateTime? LastReadingProgressUtc { get; set; }
///
/// File size of underlying item
///
diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs
index 92b1b6be5..14987ae77 100644
--- a/API/DTOs/UserPreferencesDto.cs
+++ b/API/DTOs/UserPreferencesDto.cs
@@ -63,6 +63,13 @@ public class UserPreferencesDto
///
[Required]
public bool ShowScreenHints { get; set; } = true;
+ ///
+ /// Manga Reader Option: Allow Automatic Webtoon detection
+ ///
+ [Required]
+ public bool AllowAutomaticWebtoonReaderDetection { get; set; }
+
+
///
/// Book Reader Option: Override extra Margin
///
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index babdada4a..4533a5dbf 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -133,6 +133,9 @@ public sealed class DataContext : IdentityDbContext()
.Property(b => b.WantToReadSync)
.HasDefaultValue(true);
+ builder.Entity()
+ .Property(b => b.AllowAutomaticWebtoonReaderDetection)
+ .HasDefaultValue(true);
builder.Entity()
.Property(b => b.AllowScrobbling)
diff --git a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs b/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs
new file mode 100644
index 000000000..be3d5e3f9
--- /dev/null
+++ b/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs
@@ -0,0 +1,3403 @@
+//
+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("20250328125012_AutomaticWebtoonReaderMode")]
+ partial class AutomaticWebtoonReaderMode
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+ 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("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("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