diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj
index 38ec425fe..d6fd4eb9f 100644
--- a/API.Benchmark/API.Benchmark.csproj
+++ b/API.Benchmark/API.Benchmark.csproj
@@ -26,5 +26,10 @@
Always
+
+
+ PreserveNewest
+
+
diff --git a/API.Benchmark/Data/AesopsFables.epub b/API.Benchmark/Data/AesopsFables.epub
new file mode 100644
index 000000000..d2ab9a8b2
Binary files /dev/null and b/API.Benchmark/Data/AesopsFables.epub differ
diff --git a/API.Benchmark/KoreaderHashBenchmark.cs b/API.Benchmark/KoreaderHashBenchmark.cs
new file mode 100644
index 000000000..c0abfd2ad
--- /dev/null
+++ b/API.Benchmark/KoreaderHashBenchmark.cs
@@ -0,0 +1,41 @@
+using API.Helpers.Builders;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Order;
+using System;
+using API.Entities.Enums;
+
+namespace API.Benchmark
+{
+ [StopOnFirstError]
+ [MemoryDiagnoser]
+ [RankColumn]
+ [Orderer(SummaryOrderPolicy.FastestToSlowest)]
+ [SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)]
+ public class KoreaderHashBenchmark
+ {
+ private const string sourceEpub = "./Data/AesopsFables.epub";
+
+ [Benchmark(Baseline = true)]
+ public void TestBuildManga_baseline()
+ {
+ var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub)
+ .Build();
+ if (file == null)
+ {
+ throw new Exception("Failed to build manga file");
+ }
+ }
+
+ [Benchmark]
+ public void TestBuildManga_withHash()
+ {
+ var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub)
+ .WithHash()
+ .Build();
+ if (file == null)
+ {
+ throw new Exception("Failed to build manga file");
+ }
+ }
+ }
+}
diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index 9e7fc3a02..73b886e13 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -36,4 +36,10 @@
+
+
+ PreserveNewest
+
+
+
diff --git a/API.Tests/Data/AesopsFables.epub b/API.Tests/Data/AesopsFables.epub
new file mode 100644
index 000000000..d2ab9a8b2
Binary files /dev/null and b/API.Tests/Data/AesopsFables.epub differ
diff --git a/API.Tests/Helpers/KoreaderHelperTests.cs b/API.Tests/Helpers/KoreaderHelperTests.cs
new file mode 100644
index 000000000..66d287a5d
--- /dev/null
+++ b/API.Tests/Helpers/KoreaderHelperTests.cs
@@ -0,0 +1,60 @@
+using API.DTOs.Koreader;
+using API.DTOs.Progress;
+using API.Helpers;
+using System.Runtime.CompilerServices;
+using Xunit;
+
+namespace API.Tests.Helpers;
+
+
+public class KoreaderHelperTests
+{
+
+ [Theory]
+ [InlineData("/body/DocFragment[11]/body/div/a", 10, null)]
+ [InlineData("/body/DocFragment[1]/body/div/p[40]", 0, 40)]
+ [InlineData("/body/DocFragment[8]/body/div/p[28]/text().264", 7, 28)]
+ public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber)
+ {
+ var expected = EmptyProgressDto();
+ expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[{pNumber}]" : null;
+ expected.PageNum = page;
+ var actual = EmptyProgressDto();
+
+ KoreaderHelper.UpdateProgressDto(actual, koreaderPosition);
+ Assert.Equal(expected.BookScrollId, actual.BookScrollId);
+ Assert.Equal(expected.PageNum, actual.PageNum);
+ }
+
+
+ [Theory]
+ [InlineData("//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[20]", 5, "/body/DocFragment[6]/body/div/p[20]")]
+ [InlineData(null, 10, "/body/DocFragment[11]/body/div/a")]
+ public void GetKoreaderPosition(string scrollId, int page, string koreaderPosition)
+ {
+ var given = EmptyProgressDto();
+ given.BookScrollId = scrollId;
+ given.PageNum = page;
+
+ Assert.Equal(koreaderPosition, KoreaderHelper.GetKoreaderPosition(given));
+ }
+
+ [Theory]
+ [InlineData("./Data/AesopsFables.epub", "8795ACA4BF264B57C1EEDF06A0CEE688")]
+ public void GetKoreaderHash(string filePath, string hash)
+ {
+ Assert.Equal(KoreaderHelper.HashContents(filePath), hash);
+ }
+
+ private ProgressDto EmptyProgressDto()
+ {
+ return new ProgressDto
+ {
+ ChapterId = 0,
+ PageNum = 0,
+ VolumeId = 0,
+ SeriesId = 0,
+ LibraryId = 0
+ };
+ }
+}
diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs
index a1073a55b..f2c87e1ad 100644
--- a/API.Tests/Services/ImageServiceTests.cs
+++ b/API.Tests/Services/ImageServiceTests.cs
@@ -161,10 +161,10 @@ public class ImageServiceTests
private static void GenerateColorImage(string hexColor, string outputPath)
{
- var color = ImageService.HexToRgb(hexColor);
- using var colorImage = Image.Black(200, 100);
- using var output = colorImage + new[] { color.R / 255.0, color.G / 255.0, color.B / 255.0 };
- output.WriteToFile(outputPath);
+ var (r, g, b) = ImageService.HexToRgb(hexColor);
+ using var blackImage = Image.Black(200, 100);
+ using var colorImage = blackImage.NewFromImage(r, g, b);
+ colorImage.WriteToFile(outputPath);
}
private void GenerateHtmlFileForColorScape()
diff --git a/API/API.csproj b/API/API.csproj
index f9a889d74..4eed66f22 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -97,9 +97,9 @@
+
-
diff --git a/API/Controllers/KoreaderController.cs b/API/Controllers/KoreaderController.cs
new file mode 100644
index 000000000..1ce5e3202
--- /dev/null
+++ b/API/Controllers/KoreaderController.cs
@@ -0,0 +1,118 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Threading.Tasks;
+using API.Data;
+using API.Data.Repositories;
+using API.DTOs.Koreader;
+using API.Entities;
+using API.Services;
+using Kavita.Common;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Logging;
+using static System.Net.WebRequestMethods;
+
+namespace API.Controllers;
+#nullable enable
+
+///
+/// The endpoint to interface with Koreader's Progress Sync plugin.
+///
+///
+/// Koreader uses a different form of authentication. It stores the username and password in headers.
+/// https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua
+///
+[AllowAnonymous]
+public class KoreaderController : BaseApiController
+{
+
+ private readonly IUnitOfWork _unitOfWork;
+ private readonly ILocalizationService _localizationService;
+ private readonly IKoreaderService _koreaderService;
+ private readonly ILogger _logger;
+
+ public KoreaderController(IUnitOfWork unitOfWork, ILocalizationService localizationService,
+ IKoreaderService koreaderService, ILogger logger)
+ {
+ _unitOfWork = unitOfWork;
+ _localizationService = localizationService;
+ _koreaderService = koreaderService;
+ _logger = logger;
+ }
+
+ // We won't allow users to be created from Koreader. Rather, they
+ // must already have an account.
+ /*
+ [HttpPost("/users/create")]
+ public IActionResult CreateUser(CreateUserRequest request)
+ {
+ }
+ */
+
+ [HttpGet("{apiKey}/users/auth")]
+ public async Task Authenticate(string apiKey)
+ {
+ var userId = await GetUserId(apiKey);
+ var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
+ if (user == null) return Unauthorized();
+
+ return Ok(new { username = user.UserName });
+ }
+
+ ///
+ /// Syncs book progress with Kavita. Will attempt to save the underlying reader position if possible.
+ ///
+ ///
+ ///
+ ///
+ [HttpPut("{apiKey}/syncs/progress")]
+ public async Task> UpdateProgress(string apiKey, KoreaderBookDto request)
+ {
+ try
+ {
+ var userId = await GetUserId(apiKey);
+ await _koreaderService.SaveProgress(request, userId);
+
+ return Ok(new KoreaderProgressUpdateDto{ Document = request.Document, Timestamp = DateTime.UtcNow });
+ }
+ catch (KavitaException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ ///
+ /// Gets book progress from Kavita, if not found will return a 400
+ ///
+ ///
+ ///
+ ///
+ [HttpGet("{apiKey}/syncs/progress/{ebookHash}")]
+ public async Task> GetProgress(string apiKey, string ebookHash)
+ {
+ try
+ {
+ var userId = await GetUserId(apiKey);
+ var response = await _koreaderService.GetProgress(ebookHash, userId);
+ _logger.LogDebug("Koreader response progress: {Progress}", response.Progress);
+
+ return Ok(response);
+ }
+ catch (KavitaException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ private async Task GetUserId(string apiKey)
+ {
+ try
+ {
+ return await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
+ }
+ catch
+ {
+ throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist"));
+ }
+ }
+}
diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs
index 10a5f393a..cab33692a 100644
--- a/API/Controllers/MetadataController.cs
+++ b/API/Controllers/MetadataController.cs
@@ -6,8 +6,10 @@ using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
+using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
+using API.DTOs.Metadata.Browse;
using API.DTOs.Person;
using API.DTOs.Recommendation;
using API.DTOs.SeriesDetail;
@@ -46,6 +48,22 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context));
}
+ ///
+ /// Returns a list of Genres with counts for counts when Genre is on Series/Chapter
+ ///
+ ///
+ [HttpPost("genres-with-counts")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)]
+ public async Task>> GetBrowseGenres(UserParams? userParams = null)
+ {
+ userParams ??= UserParams.Default;
+
+ var list = await unitOfWork.GenreRepository.GetBrowseableGenre(User.GetUserId(), userParams);
+ Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
+
+ return Ok(list);
+ }
+
///
/// Fetches people from the instance by role
///
@@ -95,6 +113,22 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId()));
}
+ ///
+ /// Returns a list of Tags with counts for counts when Tag is on Series/Chapter
+ ///
+ ///
+ [HttpPost("tags-with-counts")]
+ [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)]
+ public async Task>> GetBrowseTags(UserParams? userParams = null)
+ {
+ userParams ??= UserParams.Default;
+
+ var list = await unitOfWork.TagRepository.GetBrowseableTag(User.GetUserId(), userParams);
+ Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
+
+ return Ok(list);
+ }
+
///
/// Fetches all age ratings from the instance
///
diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs
index a2ab3bf88..bf3cc1814 100644
--- a/API/Controllers/PersonController.cs
+++ b/API/Controllers/PersonController.cs
@@ -4,6 +4,9 @@ using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
+using API.DTOs.Filtering.v2;
+using API.DTOs.Metadata.Browse;
+using API.DTOs.Metadata.Browse.Requests;
using API.DTOs.Person;
using API.Entities.Enums;
using API.Extensions;
@@ -77,11 +80,13 @@ public class PersonController : BaseApiController
///
///
[HttpPost("all")]
- public async Task>> GetAuthorsForBrowse([FromQuery] UserParams? userParams)
+ public async Task>> GetPeopleForBrowse(BrowsePersonFilterDto filter, [FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
- var list = await _unitOfWork.PersonRepository.GetAllWritersAndSeriesCount(User.GetUserId(), userParams);
+
+ var list = await _unitOfWork.PersonRepository.GetBrowsePersonDtos(User.GetUserId(), filter, userParams);
Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
+
return Ok(list);
}
@@ -112,6 +117,7 @@ public class PersonController : BaseApiController
person.Name = dto.Name?.Trim();
+ person.NormalizedName = person.Name.ToNormalized();
person.Description = dto.Description ?? string.Empty;
person.CoverImageLocked = dto.CoverImageLocked;
diff --git a/API/DTOs/Filtering/PersonSortField.cs b/API/DTOs/Filtering/PersonSortField.cs
new file mode 100644
index 000000000..5268a1bf9
--- /dev/null
+++ b/API/DTOs/Filtering/PersonSortField.cs
@@ -0,0 +1,8 @@
+namespace API.DTOs.Filtering;
+
+public enum PersonSortField
+{
+ Name = 1,
+ SeriesCount = 2,
+ ChapterCount = 3
+}
diff --git a/API/DTOs/Filtering/SortOptions.cs b/API/DTOs/Filtering/SortOptions.cs
index a08e2968e..18f2b17ea 100644
--- a/API/DTOs/Filtering/SortOptions.cs
+++ b/API/DTOs/Filtering/SortOptions.cs
@@ -8,3 +8,12 @@ public sealed record SortOptions
public SortField SortField { get; set; }
public bool IsAscending { get; set; } = true;
}
+
+///
+/// All Sorting Options for a query related to Person Entity
+///
+public sealed record PersonSortOptions
+{
+ public PersonSortField SortField { get; set; }
+ public bool IsAscending { get; set; } = true;
+}
diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs
index 5323f2b48..246a92a90 100644
--- a/API/DTOs/Filtering/v2/FilterField.cs
+++ b/API/DTOs/Filtering/v2/FilterField.cs
@@ -56,5 +56,12 @@ public enum FilterField
/// Last time User Read
///
ReadLast = 32,
-
+}
+
+public enum PersonFilterField
+{
+ Role = 1,
+ Name = 2,
+ SeriesCount = 3,
+ ChapterCount = 4,
}
diff --git a/API/DTOs/Filtering/v2/FilterStatementDto.cs b/API/DTOs/Filtering/v2/FilterStatementDto.cs
index ebe6d16af..8c99bd24c 100644
--- a/API/DTOs/Filtering/v2/FilterStatementDto.cs
+++ b/API/DTOs/Filtering/v2/FilterStatementDto.cs
@@ -1,4 +1,6 @@
-namespace API.DTOs.Filtering.v2;
+using API.DTOs.Metadata.Browse.Requests;
+
+namespace API.DTOs.Filtering.v2;
public sealed record FilterStatementDto
{
@@ -6,3 +8,10 @@ public sealed record FilterStatementDto
public FilterField Field { get; set; }
public string Value { get; set; }
}
+
+public sealed record PersonFilterStatementDto
+{
+ public FilterComparison Comparison { get; set; }
+ public PersonFilterField Field { get; set; }
+ public string Value { get; set; }
+}
diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/API/DTOs/Filtering/v2/FilterV2Dto.cs
index 11dc42a6b..a247a17a6 100644
--- a/API/DTOs/Filtering/v2/FilterV2Dto.cs
+++ b/API/DTOs/Filtering/v2/FilterV2Dto.cs
@@ -16,7 +16,7 @@ public sealed record FilterV2Dto
/// The name of the filter
///
public string? Name { get; set; }
- public ICollection Statements { get; set; } = new List();
+ public ICollection Statements { get; set; } = [];
public FilterCombination Combination { get; set; } = FilterCombination.And;
public SortOptions? SortOptions { get; set; }
diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs
index 8eb38c98a..c394cf8d4 100644
--- a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs
+++ b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs
@@ -15,5 +15,9 @@ public enum MatchStateOption
public sealed record ManageMatchFilterDto
{
public MatchStateOption MatchStateOption { get; set; } = MatchStateOption.All;
+ ///
+ /// Library Type in int form. -1 indicates to ignore the field.
+ ///
+ public int LibraryType { get; set; } = -1;
public string SearchTerm { get; set; } = string.Empty;
}
diff --git a/API/DTOs/Koreader/KoreaderBookDto.cs b/API/DTOs/Koreader/KoreaderBookDto.cs
new file mode 100644
index 000000000..b66b7da3a
--- /dev/null
+++ b/API/DTOs/Koreader/KoreaderBookDto.cs
@@ -0,0 +1,33 @@
+using API.DTOs.Progress;
+
+namespace API.DTOs.Koreader;
+
+///
+/// This is the interface for receiving and sending updates to Koreader. The only fields
+/// that are actually used are the Document and Progress fields.
+///
+public class KoreaderBookDto
+{
+ ///
+ /// This is the Koreader hash of the book. It is used to identify the book.
+ ///
+ public string Document { get; set; }
+ ///
+ /// A randomly generated id from the koreader device. Only used to maintain the Koreader interface.
+ ///
+ public string Device_id { get; set; }
+ ///
+ /// The Koreader device name. Only used to maintain the Koreader interface.
+ ///
+ public string Device { get; set; }
+ ///
+ /// Percent progress of the book. Only used to maintain the Koreader interface.
+ ///
+ public float Percentage { get; set; }
+ ///
+ /// An XPath string read by Koreader to determine the location within the epub.
+ /// Essentially, it is Koreader's equivalent to ProgressDto.BookScrollId.
+ ///
+ ///
+ public string Progress { get; set; }
+}
diff --git a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs b/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs
new file mode 100644
index 000000000..52a1d6cbd
--- /dev/null
+++ b/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace API.DTOs.Koreader;
+
+public class KoreaderProgressUpdateDto
+{
+ ///
+ /// This is the Koreader hash of the book. It is used to identify the book.
+ ///
+ public string Document { get; set; }
+ ///
+ /// UTC Timestamp to return to KOReader
+ ///
+ public DateTime Timestamp { get; set; }
+}
diff --git a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs b/API/DTOs/Metadata/Browse/BrowseGenreDto.cs
new file mode 100644
index 000000000..8044c7914
--- /dev/null
+++ b/API/DTOs/Metadata/Browse/BrowseGenreDto.cs
@@ -0,0 +1,13 @@
+namespace API.DTOs.Metadata.Browse;
+
+public sealed record BrowseGenreDto : GenreTagDto
+{
+ ///
+ /// Number of Series this Entity is on
+ ///
+ public int SeriesCount { get; set; }
+ ///
+ /// Number of Chapters this Entity is on
+ ///
+ public int ChapterCount { get; set; }
+}
diff --git a/API/DTOs/Person/BrowsePersonDto.cs b/API/DTOs/Metadata/Browse/BrowsePersonDto.cs
similarity index 71%
rename from API/DTOs/Person/BrowsePersonDto.cs
rename to API/DTOs/Metadata/Browse/BrowsePersonDto.cs
index c7d318e79..20f84b783 100644
--- a/API/DTOs/Person/BrowsePersonDto.cs
+++ b/API/DTOs/Metadata/Browse/BrowsePersonDto.cs
@@ -1,6 +1,6 @@
using API.DTOs.Person;
-namespace API.DTOs;
+namespace API.DTOs.Metadata.Browse;
///
/// Used to browse writers and click in to see their series
@@ -12,7 +12,7 @@ public class BrowsePersonDto : PersonDto
///
public int SeriesCount { get; set; }
///
- /// Number or Issues this Person is the Writer for
+ /// Number of Issues this Person is the Writer for
///
- public int IssueCount { get; set; }
+ public int ChapterCount { get; set; }
}
diff --git a/API/DTOs/Metadata/Browse/BrowseTagDto.cs b/API/DTOs/Metadata/Browse/BrowseTagDto.cs
new file mode 100644
index 000000000..9a71876e3
--- /dev/null
+++ b/API/DTOs/Metadata/Browse/BrowseTagDto.cs
@@ -0,0 +1,13 @@
+namespace API.DTOs.Metadata.Browse;
+
+public sealed record BrowseTagDto : TagDto
+{
+ ///
+ /// Number of Series this Entity is on
+ ///
+ public int SeriesCount { get; set; }
+ ///
+ /// Number of Chapters this Entity is on
+ ///
+ public int ChapterCount { get; set; }
+}
diff --git a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs b/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs
new file mode 100644
index 000000000..d41cf37f3
--- /dev/null
+++ b/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using API.DTOs.Filtering;
+using API.DTOs.Filtering.v2;
+using API.Entities.Enums;
+
+namespace API.DTOs.Metadata.Browse.Requests;
+#nullable enable
+
+public sealed record BrowsePersonFilterDto
+{
+ ///
+ /// Not used - For parity with Series Filter
+ ///
+ public int Id { get; set; }
+ ///
+ /// Not used - For parity with Series Filter
+ ///
+ public string? Name { get; set; }
+ public ICollection Statements { get; set; } = [];
+ public FilterCombination Combination { get; set; } = FilterCombination.And;
+ public PersonSortOptions? SortOptions { get; set; }
+
+ ///
+ /// Limit the number of rows returned. Defaults to not applying a limit (aka 0)
+ ///
+ public int LimitTo { get; set; } = 0;
+}
diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/API/DTOs/Metadata/GenreTagDto.cs
index 4846048d2..13a339d38 100644
--- a/API/DTOs/Metadata/GenreTagDto.cs
+++ b/API/DTOs/Metadata/GenreTagDto.cs
@@ -1,6 +1,6 @@
namespace API.DTOs.Metadata;
-public sealed record GenreTagDto
+public record GenreTagDto
{
public int Id { get; set; }
public required string Title { get; set; }
diff --git a/API/DTOs/Metadata/TagDto.cs b/API/DTOs/Metadata/TagDto.cs
index f8deb6913..f5c925e1f 100644
--- a/API/DTOs/Metadata/TagDto.cs
+++ b/API/DTOs/Metadata/TagDto.cs
@@ -1,6 +1,6 @@
namespace API.DTOs.Metadata;
-public sealed record TagDto
+public record TagDto
{
public int Id { get; set; }
public required string Title { get; set; }
diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs
index cbc16275d..47a526411 100644
--- a/API/DTOs/ReadingLists/ReadingListDto.cs
+++ b/API/DTOs/ReadingLists/ReadingListDto.cs
@@ -49,6 +49,11 @@ public sealed record ReadingListDto : IHasCoverImage
///
public required AgeRating AgeRating { get; set; } = AgeRating.Unknown;
+ ///
+ /// Username of the User that owns (in the case of a promoted list)
+ ///
+ public string OwnerUserName { get; set; }
+
public void ResetColorScape()
{
PrimaryColor = string.Empty;
diff --git a/API/DTOs/UserReadingProfileDto.cs b/API/DTOs/UserReadingProfileDto.cs
index 23f67ce4d..24dbf1c34 100644
--- a/API/DTOs/UserReadingProfileDto.cs
+++ b/API/DTOs/UserReadingProfileDto.cs
@@ -64,6 +64,9 @@ public sealed record UserReadingProfileDto
///
public int? WidthOverride { get; set; }
+ ///
+ public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never;
+
#endregion
#region EpubReader
diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs b/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs
new file mode 100644
index 000000000..79f6f9504
--- /dev/null
+++ b/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs
@@ -0,0 +1,3574 @@
+//
+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("20250519151126_KoreaderHash")]
+ partial class KoreaderHash
+ {
+ ///
+ 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.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.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("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