diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index 22904ba4a..12cf2b444 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -11,8 +11,8 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/API/API.csproj b/API/API.csproj
index cf6cf1664..9767a0ec3 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -53,7 +53,7 @@
-
+
all
@@ -66,10 +66,10 @@
-
+
-
+
@@ -93,14 +93,14 @@
-
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
@@ -194,6 +194,7 @@
Always
+
diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs
index 1a3a0376e..2276a5a83 100644
--- a/API/Controllers/AccountController.cs
+++ b/API/Controllers/AccountController.cs
@@ -898,8 +898,6 @@ public class AccountController : BaseApiController
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
- if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled"));
-
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
if (user == null)
{
@@ -914,11 +912,7 @@ public class AccountController : BaseApiController
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
return BadRequest(await _localizationService.Translate(user.Id, "confirm-email"));
- if (!_emailService.IsValidEmail(user.Email))
- {
- _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI", user.Email);
- return Ok(await _localizationService.Translate(user.Id, "invalid-email"));
- }
+
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
@@ -927,6 +921,13 @@ public class AccountController : BaseApiController
await _unitOfWork.CommitAsync();
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
+ if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled"));
+ if (!_emailService.IsValidEmail(user.Email))
+ {
+ _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI or from url above", user.Email);
+ return Ok(await _localizationService.Translate(user.Id, "invalid-email"));
+ }
+
var installId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value;
BackgroundJob.Enqueue(() => _emailService.SendForgotPasswordEmail(new PasswordResetEmailDto()
{
diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs
index bfc6849d6..b49d6aa40 100644
--- a/API/Controllers/CollectionController.cs
+++ b/API/Controllers/CollectionController.cs
@@ -12,8 +12,11 @@ using API.Extensions;
using API.Helpers.Builders;
using API.Services;
using API.Services.Plus;
+using API.SignalR;
+using Hangfire;
using Kavita.Common;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
namespace API.Controllers;
@@ -28,15 +31,23 @@ public class CollectionController : BaseApiController
private readonly ICollectionTagService _collectionService;
private readonly ILocalizationService _localizationService;
private readonly IExternalMetadataService _externalMetadataService;
+ private readonly ISmartCollectionSyncService _collectionSyncService;
+ private readonly ILogger _logger;
+ private readonly IEventHub _eventHub;
///
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService,
- ILocalizationService localizationService, IExternalMetadataService externalMetadataService)
+ ILocalizationService localizationService, IExternalMetadataService externalMetadataService,
+ ISmartCollectionSyncService collectionSyncService, ILogger logger,
+ IEventHub eventHub)
{
_unitOfWork = unitOfWork;
_collectionService = collectionService;
_localizationService = localizationService;
_externalMetadataService = externalMetadataService;
+ _collectionSyncService = collectionSyncService;
+ _logger = logger;
+ _eventHub = eventHub;
}
///
@@ -49,6 +60,18 @@ public class CollectionController : BaseApiController
return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(User.GetUserId(), !ownedOnly));
}
+ ///
+ /// Returns a single Collection tag by Id for a given user
+ ///
+ ///
+ ///
+ [HttpGet("single")]
+ public async Task>> GetTag(int collectionId)
+ {
+ var collections = await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(User.GetUserId(), false);
+ return Ok(collections.FirstOrDefault(c => c.Id == collectionId));
+ }
+
///
/// Returns all collections that contain the Series for the user with the option to allow for promoted collections (non-user owned)
///
@@ -86,6 +109,8 @@ public class CollectionController : BaseApiController
{
if (await _collectionService.UpdateTag(updatedTag, User.GetUserId()))
{
+ await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated,
+ MessageFactory.CollectionUpdatedEvent(updatedTag.Id), false);
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated-successfully"));
}
}
@@ -129,12 +154,12 @@ public class CollectionController : BaseApiController
///
- /// Promote/UnPromote multiple collections in one go
+ /// Delete multiple collections in one go
///
///
///
[HttpPost("delete-multiple")]
- public async Task DeleteMultipleCollections(PromoteCollectionsDto dto)
+ public async Task DeleteMultipleCollections(DeleteCollectionsDto dto)
{
// This needs to take into account owner as I can select other users cards
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Collections);
@@ -178,7 +203,7 @@ public class CollectionController : BaseApiController
return BadRequest(_localizationService.Translate(User.GetUserId(), "collection-doesnt-exists"));
}
- var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds.ToList());
+ var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds.ToList(), false);
foreach (var s in series)
{
if (tag.Items.Contains(s)) continue;
@@ -253,4 +278,45 @@ public class CollectionController : BaseApiController
{
return Ok(await _externalMetadataService.GetStacksForUser(User.GetUserId()));
}
+
+ ///
+ /// Imports a MAL Stack into Kavita
+ ///
+ ///
+ ///
+ [HttpPost("import-stack")]
+ public async Task ImportMalStack(MalStackDto dto)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Collections);
+ if (user == null) return Unauthorized();
+
+ // Validation check to ensure stack doesn't exist already
+ if (await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, user.Id))
+ {
+ return BadRequest(_localizationService.Translate(user.Id, "collection-already-exists"));
+ }
+
+ try
+ {
+ // Create new collection
+ var newCollection = new AppUserCollectionBuilder(dto.Title)
+ .WithSource(ScrobbleProvider.Mal)
+ .WithSourceUrl(dto.Url)
+ .Build();
+ user.Collections.Add(newCollection);
+
+ _unitOfWork.UserRepository.Update(user);
+ await _unitOfWork.CommitAsync();
+
+ // Trigger Stack Refresh for just one stack (not all)
+ BackgroundJob.Enqueue(() => _collectionSyncService.Sync(newCollection.Id));
+ return Ok();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "There was an issue importing MAL Stack");
+ }
+
+ return BadRequest(_localizationService.Translate(user.Id, "error-import-stack"));
+ }
}
diff --git a/API/DTOs/Collection/AppUserCollectionDto.cs b/API/DTOs/Collection/AppUserCollectionDto.cs
index 62d786ca2..f01cd492c 100644
--- a/API/DTOs/Collection/AppUserCollectionDto.cs
+++ b/API/DTOs/Collection/AppUserCollectionDto.cs
@@ -36,4 +36,12 @@ public class AppUserCollectionDto
/// For Non-Kavita sourced collections, the url to sync from
///
public string? SourceUrl { get; set; }
+ ///
+ /// Total number of items as of the last sync. Not applicable for Kavita managed collections.
+ ///
+ public int TotalSourceCount { get; set; }
+ ///
+ /// A
separated string of all missing series
+ ///
+ public string? MissingSeriesFromSource { get; set; }
}
diff --git a/API/DTOs/Collection/DeleteCollectionsDto.cs b/API/DTOs/Collection/DeleteCollectionsDto.cs
index 66bf257ba..c0b94e9a1 100644
--- a/API/DTOs/Collection/DeleteCollectionsDto.cs
+++ b/API/DTOs/Collection/DeleteCollectionsDto.cs
@@ -1,8 +1,10 @@
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
namespace API.DTOs.Collection;
public class DeleteCollectionsDto
{
+ [Required]
public IList CollectionIds { get; set; }
}
diff --git a/API/DTOs/CollectionTags/CollectionTagDto.cs b/API/DTOs/CollectionTags/CollectionTagDto.cs
index 2a1279a35..ec9939ebd 100644
--- a/API/DTOs/CollectionTags/CollectionTagDto.cs
+++ b/API/DTOs/CollectionTags/CollectionTagDto.cs
@@ -1,5 +1,8 @@
-namespace API.DTOs.CollectionTags;
+using System;
+namespace API.DTOs.CollectionTags;
+
+[Obsolete("Use AppUserCollectionDto")]
public class CollectionTagDto
{
public int Id { get; set; }
diff --git a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs
index 9d6f2a035..19e9a11e2 100644
--- a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs
+++ b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs
@@ -1,9 +1,11 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using API.DTOs.Collection;
namespace API.DTOs.CollectionTags;
public class UpdateSeriesForTagDto
{
- public CollectionTagDto Tag { get; init; } = default!;
+ public AppUserCollectionDto Tag { get; init; } = default!;
public IEnumerable SeriesIdsToRemove { get; init; } = default!;
}
diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs
index b8e8e4953..2e2ab9aba 100644
--- a/API/DTOs/UpdateLibraryDto.cs
+++ b/API/DTOs/UpdateLibraryDto.cs
@@ -36,5 +36,6 @@ public class UpdateLibraryDto
///
/// A set of Glob patterns that the scanner will exclude processing
///
+ [Required]
public ICollection ExcludePatterns { get; init; }
}
diff --git a/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs b/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs
new file mode 100644
index 000000000..1dff0c0e5
--- /dev/null
+++ b/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs
@@ -0,0 +1,3025 @@
+//
+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("20240503120147_SmartCollectionFields")]
+ partial class SmartCollectionFields
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.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("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("Promoted")
+ .HasColumnType("INTEGER");
+
+ 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("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.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("AlternateCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternateNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("AlternateSeries")
+ .HasColumnType("TEXT");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("ISBN")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ 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("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .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("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TitleName")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("WebLinks")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("");
+
+ b.Property("WordCount")
+ .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.FolderPath", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("FolderPath");
+ });
+
+ modelBuilder.Entity("API.Entities.Genre", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle")
+ .IsUnique();
+
+ b.ToTable("Genre");
+ });
+
+ modelBuilder.Entity("API.Entities.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AllowScrobbling")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true);
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("FolderWatching")
+ .HasColumnType("INTEGER");
+
+ b.Property("IncludeInDashboard")
+ .HasColumnType("INTEGER");
+
+ b.Property("IncludeInRecommended")
+ .HasColumnType("INTEGER");
+
+ b.Property("IncludeInSearch")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("ManageCollections")
+ .HasColumnType("INTEGER");
+
+ b.Property("ManageReadingLists")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.LibraryExcludePattern", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Pattern")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("LibraryExcludePattern");
+ });
+
+ modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("FileTypeGroup")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("LibraryFileTypeGroup");
+ });
+
+ modelBuilder.Entity("API.Entities.MangaFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Bytes")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property