diff --git a/.gitignore b/.gitignore index 7da76a034..bb124fc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -527,8 +527,7 @@ API/config/stats/* API/config/stats/app_stats.json API/config/pre-metadata/ API/config/post-metadata/ -API/config/relations-imported.csv -API/config/relations.csv +API/config/*.csv API.Tests/TestResults/ UI/Web/.vscode/settings.json /API.Tests/Services/Test Data/ArchiveService/CoverImages/output/* diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index d1aa96fb2..8c29c5c18 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -488,15 +488,21 @@ public class CleanupServiceTests : AbstractDbTest var user = new AppUser() { UserName = "CleanupWantToRead_ShouldRemoveFullyReadSeries", - WantToRead = new List() - { - s - } }; _context.AppUser.Add(user); await _unitOfWork.CommitAsync(); + // Add want to read + user.WantToRead = new List() + { + new AppUserWantToRead() + { + SeriesId = s.Id + } + }; + await _unitOfWork.CommitAsync(); + await _readerService.MarkSeriesAsRead(user, s.Id); await _unitOfWork.CommitAsync(); diff --git a/API/API.csproj b/API/API.csproj index 806467f6f..74dabfe44 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -53,6 +53,7 @@ + all diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index c80b425e7..2482ef714 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1250,7 +1250,7 @@ public class OpdsController : BaseApiController if (progress != null) { link.LastRead = progress.PageNum; - link.LastReadDate = progress.LastModifiedUtc.ToString("o"); // Adhere to ISO 8601 + link.LastReadDate = progress.LastModifiedUtc.ToString("s"); // Adhere to ISO 8601 } link.IsPageStream = true; return link; diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index b0de79c4c..fd497608e 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -38,18 +38,16 @@ public class ServerController : BaseApiController private readonly IStatsService _statsService; private readonly ICleanupService _cleanupService; private readonly IScannerService _scannerService; - private readonly IAccountService _accountService; private readonly ITaskScheduler _taskScheduler; private readonly IUnitOfWork _unitOfWork; private readonly IEasyCachingProviderFactory _cachingProviderFactory; private readonly ILocalizationService _localizationService; - private readonly IEmailService _emailService; public ServerController(ILogger logger, - IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService, - ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService, + IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, + IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory, - ILocalizationService localizationService, IEmailService emailService) + ILocalizationService localizationService) { _logger = logger; _backupService = backupService; @@ -58,12 +56,10 @@ public class ServerController : BaseApiController _statsService = statsService; _cleanupService = cleanupService; _scannerService = scannerService; - _accountService = accountService; _taskScheduler = taskScheduler; _unitOfWork = unitOfWork; _cachingProviderFactory = cachingProviderFactory; _localizationService = localizationService; - _emailService = emailService; } /// @@ -180,11 +176,22 @@ public class ServerController : BaseApiController } } + /// + /// Checks for updates and pushes an event to the UI + /// + /// Some users have websocket issues so this is not always reliable to alert the user + [HttpGet("check-for-updates")] + public async Task CheckForAnnouncements() + { + await _taskScheduler.CheckForUpdate(); + return Ok(); + } + /// /// Checks for updates, if no updates that are > current version installed, returns null /// [HttpGet("check-update")] - public async Task> CheckForUpdates() + public async Task> CheckForUpdates() { return Ok(await _versionUpdaterService.CheckForUpdate()); } @@ -268,14 +275,4 @@ public class ServerController : BaseApiController return Ok(); } - /// - /// Checks for updates and pushes an event to the UI - /// - /// - [HttpGet("check-for-updates")] - public async Task CheckForAnnouncements() - { - await _taskScheduler.CheckForUpdate(); - return Ok(); - } } diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index 116215a36..b80607b56 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -7,6 +7,7 @@ using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.WantToRead; +using API.Entities; using API.Extensions; using API.Helpers; using API.Services; @@ -91,15 +92,15 @@ public class WantToReadController : BaseApiController AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); - var existingIds = user.WantToRead.Select(s => s.Id).ToList(); - existingIds.AddRange(dto.SeriesIds); + var existingIds = user.WantToRead.Select(s => s.SeriesId).ToList(); + var idsToAdd = dto.SeriesIds.Except(existingIds); - var idsToAdd = existingIds.Distinct().ToList(); - - var seriesToAdd = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(idsToAdd); - foreach (var series in seriesToAdd) + foreach (var id in idsToAdd) { - user.WantToRead.Add(series); + user.WantToRead.Add(new AppUserWantToRead() + { + SeriesId = id + }); } if (!_unitOfWork.HasChanges()) return Ok(); @@ -127,7 +128,9 @@ public class WantToReadController : BaseApiController AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); - user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList(); + user.WantToRead = user.WantToRead + .Where(s => !dto.SeriesIds.Contains(s.SeriesId)) + .ToList(); if (!_unitOfWork.HasChanges()) return Ok(); if (await _unitOfWork.CommitAsync()) diff --git a/API/DTOs/Filtering/SortField.cs b/API/DTOs/Filtering/SortField.cs index f30b617df..b072819f4 100644 --- a/API/DTOs/Filtering/SortField.cs +++ b/API/DTOs/Filtering/SortField.cs @@ -30,4 +30,8 @@ public enum SortField /// Last time the user had any reading progress /// ReadProgress = 7, + /// + /// Kavita+ Only - External Average Rating + /// + AverageRating = 8 } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 62af083b3..b73c9d737 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -62,6 +62,7 @@ public sealed class DataContext : IdentityDbContext ExternalRating { get; set; } = null!; public DbSet ExternalSeriesMetadata { get; set; } = null!; public DbSet ExternalRecommendation { get; set; } = null!; + public DbSet ManualMigrationHistory { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) diff --git a/API/Data/ManualMigrations/MigrateManualHistory.cs b/API/Data/ManualMigrations/MigrateManualHistory.cs new file mode 100644 index 000000000..6b1b11a6c --- /dev/null +++ b/API/Data/ManualMigrations/MigrateManualHistory.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using API.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.7.14, will store history so that going forward, migrations can just check against the history +/// and I don't need to remove old migrations +/// +public static class MigrateManualHistory +{ + public static async Task Migrate(DataContext dataContext, ILogger logger) + { + logger.LogCritical( + "Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error"); + + if (await dataContext.ManualMigrationHistory.AnyAsync()) + { + logger.LogCritical( + "Running MigrateManualHistory migration - Completed. This is not an error"); + return; + } + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateUserLibrarySideNavStream", + ProductVersion = "0.7.9.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateSmartFilterEncoding", + ProductVersion = "0.7.11.0", + RanAt = DateTime.UtcNow + }); + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateLibrariesToHaveAllFileTypes", + ProductVersion = "0.7.11.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateEmailTemplates", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateVolumeNumber", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateWantToReadExport", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateWantToReadImport", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateManualHistory", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + await dataContext.SaveChangesAsync(); + + logger.LogCritical( + "Running MigrateManualHistory migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateWantToReadExport.cs b/API/Data/ManualMigrations/MigrateWantToReadExport.cs new file mode 100644 index 000000000..46f8e7a3e --- /dev/null +++ b/API/Data/ManualMigrations/MigrateWantToReadExport.cs @@ -0,0 +1,81 @@ +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using API.Services; +using CsvHelper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + + +/// +/// v0.7.13.12/v0.7.14 - Want to read is extracted and saved in a csv +/// +/// This must run BEFORE any DB migrations +public static class MigrateWantToReadExport +{ + public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger logger) + { + logger.LogCritical( + "Running MigrateWantToReadExport migration - Please be patient, this may take some time. This is not an error"); + + var columnExists = false; + await using var command = dataContext.Database.GetDbConnection().CreateCommand(); + command.CommandText = "PRAGMA table_info('Series')"; + + await dataContext.Database.OpenConnectionAsync(); + await using var result = await command.ExecuteReaderAsync(); + while (await result.ReadAsync()) + { + var columnName = result["name"].ToString(); + if (columnName != "AppUserId") continue; + + logger.LogInformation("Column 'AppUserId' exists in the 'Series' table. Running migration..."); + // Your migration logic here + columnExists = true; + break; + } + + await result.CloseAsync(); + + if (!columnExists) + { + logger.LogCritical( + "Running MigrateWantToReadExport migration - Completed. This is not an error"); + return; + } + + await using var command2 = dataContext.Database.GetDbConnection().CreateCommand(); + command.CommandText = "Select AppUserId, Id from Series WHERE AppUserId IS NOT NULL ORDER BY AppUserId;"; + + await dataContext.Database.OpenConnectionAsync(); + await using var result2 = await command.ExecuteReaderAsync(); + + await using var writer = new StreamWriter(Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv")); + await using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture); + + // Write header + csvWriter.WriteField("AppUserId"); + csvWriter.WriteField("Id"); + await csvWriter.NextRecordAsync(); + + // Write data + while (await result2.ReadAsync()) + { + var appUserId = result2["AppUserId"].ToString(); + var id = result2["Id"].ToString(); + + csvWriter.WriteField(appUserId); + csvWriter.WriteField(id); + await csvWriter.NextRecordAsync(); + } + + + await result2.CloseAsync(); + writer.Close(); + + logger.LogCritical( + "Running MigrateWantToReadExport migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateWantToReadImport.cs b/API/Data/ManualMigrations/MigrateWantToReadImport.cs new file mode 100644 index 000000000..0a3e87d35 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateWantToReadImport.cs @@ -0,0 +1,60 @@ +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.Data.Repositories; +using API.Entities; +using API.Services; +using CsvHelper; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// v0.7.13.12/v0.7.14 - Want to read is imported from a csv +/// +public static class MigrateWantToReadImport +{ + public static async Task Migrate(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger logger) + { + var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"); + var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv"); + + logger.LogCritical( + "Running MigrateWantToReadImport migration - Please be patient, this may take some time. This is not an error"); + + if (!File.Exists(importFile) || File.Exists(outputFile)) + { + logger.LogCritical( + "Running MigrateWantToReadImport migration - Completed. This is not an error"); + return; + } + + using var reader = new StreamReader(importFile); + using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture); + // Read the records from the CSV file + await csvReader.ReadAsync(); + csvReader.ReadHeader(); // Skip the header row + + while (await csvReader.ReadAsync()) + { + // Read the values of AppUserId and Id columns + var appUserId = csvReader.GetField("AppUserId"); + var seriesId = csvReader.GetField("Id"); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(appUserId, AppUserIncludes.WantToRead); + if (user == null || user.WantToRead.Any(w => w.SeriesId == seriesId)) continue; + + user.WantToRead.Add(new AppUserWantToRead() + { + SeriesId = seriesId + }); + } + + await unitOfWork.CommitAsync(); + reader.Close(); + + File.WriteAllLines(outputFile, await File.ReadAllLinesAsync(importFile)); + logger.LogCritical( + "Running MigrateWantToReadImport migration - Completed. This is not an error"); + } +} diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs b/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs new file mode 100644 index 000000000..a4203171c --- /dev/null +++ b/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs @@ -0,0 +1,2844 @@ +// +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("20240130190617_WantToReadFix")] + partial class WantToReadFix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + 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("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.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("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("MinHoursToRead") + .HasColumnType("INTEGER"); + + 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("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("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("LastUpdatedUtc") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .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("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.cs b/API/Data/Migrations/20240130190617_WantToReadFix.cs new file mode 100644 index 000000000..386160db3 --- /dev/null +++ b/API/Data/Migrations/20240130190617_WantToReadFix.cs @@ -0,0 +1,106 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class WantToReadFix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Series_AspNetUsers_AppUserId", + table: "Series"); + + migrationBuilder.DropIndex( + name: "IX_Series_AppUserId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "AppUserId", + table: "Series"); + + migrationBuilder.CreateTable( + name: "AppUserWantToRead", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SeriesId = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserWantToRead", x => x.Id); + table.ForeignKey( + name: "FK_AppUserWantToRead_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserWantToRead_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ManualMigrationHistory", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProductVersion = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: true), + RanAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ManualMigrationHistory", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserWantToRead_AppUserId", + table: "AppUserWantToRead", + column: "AppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserWantToRead_SeriesId", + table: "AppUserWantToRead", + column: "SeriesId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUserWantToRead"); + + migrationBuilder.DropTable( + name: "ManualMigrationHistory"); + + migrationBuilder.AddColumn( + name: "AppUserId", + table: "Series", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Series_AppUserId", + table: "Series", + column: "AppUserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Series_AspNetUsers_AppUserId", + table: "Series", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 6ccc3759f..0c1d2e116 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -602,6 +602,27 @@ namespace API.Data.Migrations 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") @@ -980,6 +1001,26 @@ namespace API.Data.Migrations b.ToTable("MangaFile"); }); + modelBuilder.Entity("API.Entities.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + modelBuilder.Entity("API.Entities.MediaError", b => { b.Property("Id") @@ -1551,9 +1592,6 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AppUserId") - .HasColumnType("INTEGER"); - b.Property("AvgHoursToRead") .HasColumnType("INTEGER"); @@ -1634,8 +1672,6 @@ namespace API.Data.Migrations b.HasKey("Id"); - b.HasIndex("AppUserId"); - b.HasIndex("LibraryId"); b.ToTable("Series"); @@ -2252,6 +2288,25 @@ namespace API.Data.Migrations b.Navigation("Series"); }); + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + modelBuilder.Entity("API.Entities.Chapter", b => { b.HasOne("API.Entities.Volume", "Volume") @@ -2479,10 +2534,6 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Series", b => { - b.HasOne("API.Entities.AppUser", null) - .WithMany("WantToRead") - .HasForeignKey("AppUserId"); - b.HasOne("API.Entities.Library", "Library") .WithMany("Series") .HasForeignKey("LibraryId") diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index f84719e55..fd3d639b6 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -363,8 +363,8 @@ public class SeriesRepository : ISeriesRepository .Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%")) .IsRestricted(QueryContext.Search) .AsSplitQuery() - .Take(maxRecords) .OrderBy(l => l.Name.ToLower()) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -383,8 +383,8 @@ public class SeriesRepository : ISeriesRepository .Include(s => s.Library) .AsNoTracking() .AsSplitQuery() - .Take(maxRecords) .OrderBy(s => s.SortName!.ToLower()) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); @@ -420,8 +420,8 @@ public class SeriesRepository : ISeriesRepository .Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%")) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .Take(maxRecords) .OrderBy(r => r.NormalizedTitle) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -431,7 +431,6 @@ public class SeriesRepository : ISeriesRepository .Where(c => c.Promoted || isAdmin) .RestrictAgainstAgeRestriction(userRating) .OrderBy(s => s.NormalizedTitle) - .AsNoTracking() .AsSplitQuery() .Take(maxRecords) .OrderBy(c => c.NormalizedTitle) @@ -443,8 +442,8 @@ public class SeriesRepository : ISeriesRepository .SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%"))) .AsSplitQuery() .Distinct() - .Take(maxRecords) .OrderBy(p => p.NormalizedName) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -453,8 +452,8 @@ public class SeriesRepository : ISeriesRepository .SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) .AsSplitQuery() .Distinct() - .Take(maxRecords) .OrderBy(t => t.NormalizedTitle) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -463,8 +462,8 @@ public class SeriesRepository : ISeriesRepository .SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) .AsSplitQuery() .Distinct() - .Take(maxRecords) .OrderBy(t => t.NormalizedTitle) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -482,8 +481,8 @@ public class SeriesRepository : ISeriesRepository result.Files = await _context.MangaFile .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) .AsSplitQuery() - .Take(maxRecords) .OrderBy(f => f.FilePath) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } @@ -499,8 +498,8 @@ public class SeriesRepository : ISeriesRepository ) .Where(c => c.Files.All(f => fileIds.Contains(f.Id))) .AsSplitQuery() - .Take(maxRecords) .OrderBy(c => c.TitleName) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -991,6 +990,8 @@ public class SeriesRepository : ISeriesRepository SortField.TimeToRead => query.DoOrderBy(s => s.AvgHoursToRead, filter.SortOptions), SortField.ReleaseYear => query.DoOrderBy(s => s.Metadata.ReleaseYear, filter.SortOptions), SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max(), filter.SortOptions), + SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings + .Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), filter.SortOptions), _ => query }; @@ -1043,7 +1044,9 @@ public class SeriesRepository : ISeriesRepository var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead); if (wantToReadStmt == null) return query; - var seriesIds = _context.AppUser.Where(u => u.Id == userId).SelectMany(u => u.WantToRead).Select(s => s.Id); + var seriesIds = _context.AppUser.Where(u => u.Id == userId) + .SelectMany(u => u.WantToRead) + .Select(s => s.SeriesId); if (bool.Parse(wantToReadStmt.Value)) { query = query.Where(s => seriesIds.Contains(s.Id)); @@ -1869,7 +1872,8 @@ public class SeriesRepository : ISeriesRepository var query = _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.Series.LibraryId)) + .Select(w => w.Series) .AsSplitQuery() .AsNoTracking(); @@ -1884,7 +1888,8 @@ public class SeriesRepository : ISeriesRepository var query = _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.Series.LibraryId)) + .Select(w => w.Series) .AsSplitQuery() .AsNoTracking(); @@ -1899,7 +1904,8 @@ public class SeriesRepository : ISeriesRepository return await _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.Series.LibraryId)) + .Select(w => w.Series) .AsSplitQuery() .AsNoTracking() .ToListAsync(); @@ -1994,7 +2000,7 @@ public class SeriesRepository : ISeriesRepository var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); return await _context.AppUser .Where(user => user.Id == userId) - .SelectMany(u => u.WantToRead.Where(s => s.Id == seriesId && libraryIds.Contains(s.LibraryId))) + .SelectMany(u => u.WantToRead.Where(s => s.SeriesId == seriesId && libraryIds.Contains(s.Series.LibraryId))) .AsSplitQuery() .AsNoTracking() .AnyAsync(); diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 62c8cc81a..f87531e8a 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -31,7 +31,7 @@ public class AppUser : IdentityUser, IHasConcurrencyToken /// /// A list of Series the user want's to read /// - public ICollection WantToRead { get; set; } = null!; + public ICollection WantToRead { get; set; } = null!; /// /// A list of Devices which allows the user to send files to /// diff --git a/API/Entities/AppUserWantToRead.cs b/API/Entities/AppUserWantToRead.cs new file mode 100644 index 000000000..d41e44962 --- /dev/null +++ b/API/Entities/AppUserWantToRead.cs @@ -0,0 +1,20 @@ +namespace API.Entities; + +public class AppUserWantToRead +{ + public int Id { get; set; } + + public required int SeriesId { get; set; } + public virtual Series Series { get; set; } + + + // Relationships + /// + /// Navigational Property for EF. Links to a unique AppUser + /// + public AppUser AppUser { get; set; } = null!; + /// + /// User this table of content belongs to + /// + public int AppUserId { get; set; } +} diff --git a/API/Entities/ManualMigrationHistory.cs b/API/Entities/ManualMigrationHistory.cs new file mode 100644 index 000000000..e65e07b2c --- /dev/null +++ b/API/Entities/ManualMigrationHistory.cs @@ -0,0 +1,14 @@ +using System; + +namespace API.Entities; + +/// +/// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed +/// +public class ManualMigrationHistory +{ + public int Id { get; set; } + public string ProductVersion { get; set; } + public required string Name { get; set; } + public DateTime RanAt { get; set; } +} diff --git a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs index 1ef2d5dd8..f3dbfef14 100644 --- a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs @@ -37,6 +37,8 @@ public static class BookmarkSort SortField.TimeToRead => query.DoOrderBy(s => s.Series.AvgHoursToRead, sortOptions), SortField.ReleaseYear => query.DoOrderBy(s => s.Series.Metadata.ReleaseYear, sortOptions), SortField.ReadProgress => query.DoOrderBy(s => s.Series.Progress.Where(p => p.SeriesId == s.Series.Id).Select(p => p.LastModified).Max(), sortOptions), + SortField.AverageRating => query.DoOrderBy(s => s.Series.ExternalSeriesMetadata.ExternalRatings + .Where(p => p.SeriesId == s.Series.Id).Average(p => p.AverageScore), sortOptions), _ => query }; diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs index e59e9e922..4913c4059 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs @@ -33,6 +33,8 @@ public static class SeriesSort SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id && p.AppUserId == userId) .Select(p => p.LastModified) .Max(), sortOptions), + SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings + .Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), sortOptions), _ => query }; diff --git a/API/Logging/LogLevelOptions.cs b/API/Logging/LogLevelOptions.cs index 6f7cef02f..958792c84 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/API/Logging/LogLevelOptions.cs @@ -73,8 +73,10 @@ public static class LogLevelOptions if (isRequestLoggingMiddleware) { - if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/api/health") return false; - if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/hubs/messages") return false; + var path = e.Properties["Path"].ToString().Replace("\"", string.Empty); + if (e.Properties.ContainsKey("Path") && path == "/api/health") return false; + if (e.Properties.ContainsKey("Path") && path == "/hubs/messages") return false; + if (e.Properties.ContainsKey("Path") && path.StartsWith("/api/image")) return false; } return true; diff --git a/API/Program.cs b/API/Program.cs index c6666075b..17423c845 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -87,6 +87,30 @@ public class Program } } + // Apply Before manual migrations that need to run before actual migrations + try + { + Task.Run(async () => + { + // Apply all migrations on startup + var dataContext = services.GetRequiredService(); + var directoryService = services.GetRequiredService(); + + logger.LogInformation("Running Migrations"); + + // v0.7.14 + await MigrateWantToReadExport.Migrate(dataContext, directoryService, logger); + + await unitOfWork.CommitAsync(); + logger.LogInformation("Running Migrations - complete"); + }).GetAwaiter() + .GetResult(); + } + catch (Exception ex) + { + logger.LogCritical(ex, "An error occurred during migration"); + } + await context.Database.MigrateAsync(); diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index 3d657a929..3aaa2c837 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -288,8 +288,8 @@ public class CleanupService : ICleanupService var seriesIds = series.Select(s => s.Id).ToList(); if (seriesIds.Count == 0) continue; - user.WantToRead ??= new List(); - user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.Id)).ToList(); + user.WantToRead ??= new List(); + user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.SeriesId)).ToList(); _unitOfWork.UserRepository.Update(user); } diff --git a/API/Startup.cs b/API/Startup.cs index 76bf66209..f7b37f0c3 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -248,6 +248,8 @@ public class Startup // v0.7.14 await MigrateEmailTemplates.Migrate(directoryService, logger); await MigrateVolumeNumber.Migrate(unitOfWork, dataContext, logger); + await MigrateWantToReadImport.Migrate(unitOfWork, directoryService, logger); + await MigrateManualHistory.Migrate(dataContext, logger); // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); diff --git a/UI/Web/hash-localization.js b/UI/Web/hash-localization.js index 0d1621d90..b4f086ab4 100644 --- a/UI/Web/hash-localization.js +++ b/UI/Web/hash-localization.js @@ -14,6 +14,12 @@ function generateChecksum(str, algorithm, encoding) { const result = {}; +// Remove file if it exists +const cacheBustingFilePath = './i18n-cache-busting.json'; +if (fs.existsSync(cacheBustingFilePath)) { + fs.unlinkSync(cacheBustingFilePath); +} + glob.sync(`${jsonFilesDir}**/*.json`).forEach(path => { let tokens = path.split('dist\\browser\\assets\\langs\\'); if (tokens.length === 1) { diff --git a/UI/Web/package.json b/UI/Web/package.json index 6e575de39..011146c69 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -3,12 +3,12 @@ "version": "0.7.12.1", "scripts": { "ng": "ng", - "start": "npm run cache-langs && ng serve", - "build": "npm run cache-langs && ng build", + "start": "npm run cache-locale && ng serve", + "build": "npm run cache-locale && ng build", "minify-langs": "node minify-json.js", - "cache-langs": "node hash-localization.js", - "cache-langs-prime": "node hash-localization-prime.js", - "prod": "npm run cache-langs-prime && ng build --configuration production && npm run minify-langs && npm run cache-langs", + "cache-locale": "node hash-localization.js", + "cache-locale-prime": "node hash-localization-prime.js", + "prod": "npm run cache-locale-prime && ng build --configuration production && npm run minify-langs && npm run cache-locale", "explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json", "lint": "ng lint", "e2e": "ng e2e" diff --git a/UI/Web/src/app/_models/metadata/series-filter.ts b/UI/Web/src/app/_models/metadata/series-filter.ts index 843844416..663fc2380 100644 --- a/UI/Web/src/app/_models/metadata/series-filter.ts +++ b/UI/Web/src/app/_models/metadata/series-filter.ts @@ -21,6 +21,10 @@ export enum SortField { TimeToRead = 5, ReleaseYear = 6, ReadProgress = 7, + /** + * Kavita+ only + */ + AverageRating = 8 } export const allSortFields = Object.keys(SortField) diff --git a/UI/Web/src/app/_pipes/sort-field.pipe.ts b/UI/Web/src/app/_pipes/sort-field.pipe.ts index 1fe878860..ea54d124d 100644 --- a/UI/Web/src/app/_pipes/sort-field.pipe.ts +++ b/UI/Web/src/app/_pipes/sort-field.pipe.ts @@ -27,6 +27,8 @@ export class SortFieldPipe implements PipeTransform { return this.translocoService.translate('sort-field-pipe.release-year'); case SortField.ReadProgress: return this.translocoService.translate('sort-field-pipe.read-progress'); + case SortField.AverageRating: + return this.translocoService.translate('sort-field-pipe.average-rating'); } } diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 4fb37c283..a70ef5a07 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -39,11 +39,16 @@ export class ServerService { } checkForUpdate() { - return this.http.get(this.baseUrl + 'server/check-update', {}); + return this.http.get(this.baseUrl + 'server/check-update'); + } + + checkHowOutOfDate() { + return this.http.get(this.baseUrl + 'server/checkHowOutOfDate', TextResonse) + .pipe(map(r => parseInt(r, 10))); } checkForUpdates() { - return this.http.get(this.baseUrl + 'server/check-for-updates', {}); + return this.http.get(this.baseUrl + 'server/check-for-updates', {}); } getChangelog() { diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index d19598d3c..d892d4c4d 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -40,7 +40,7 @@ export class ManageEmailSettingsComponent implements OnInit { ngOnInit(): void { this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; - this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [])); + this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)])); this.settingsForm.addControl('host', new FormControl(this.serverSettings.smtpConfig.host, [])); this.settingsForm.addControl('port', new FormControl(this.serverSettings.smtpConfig.port, [])); diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index 58d89cb7d..06af00124 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -5,6 +5,21 @@ {{t('notice')}} {{t('restart-required')}} +
+ + {{t('host-name-tooltip')}} + + + + +
+
+ {{t('host-name-validation')}} +
+
+
+
{{t('base-url-tooltip')}} diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index 358d68796..118766e3a 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -104,7 +104,6 @@ export class ManageSettingsComponent implements OnInit { modelSettings.smtpConfig = this.serverSettings.smtpConfig; - this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.resetForm(); diff --git a/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts b/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts index f2f1fdcd1..0ae467ebb 100644 --- a/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts +++ b/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts @@ -21,7 +21,6 @@ export class ChangelogComponent implements OnInit { constructor(private serverService: ServerService) { } ngOnInit(): void { - this.serverService.getChangelog().subscribe(updates => { this.updates = updates; this.isLoading = false; diff --git a/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.html b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.html new file mode 100644 index 000000000..ddbba7338 --- /dev/null +++ b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.scss b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.ts b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.ts new file mode 100644 index 000000000..41e6f36d9 --- /dev/null +++ b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.ts @@ -0,0 +1,38 @@ +import {Component, DestroyRef, inject, Input} from '@angular/core'; +import {FormsModule} from "@angular/forms"; +import {AsyncPipe, NgForOf, NgIf} from "@angular/common"; +import {NgbActiveModal, NgbHighlight, NgbModal, NgbTypeahead} from "@ng-bootstrap/ng-bootstrap"; +import {TranslocoDirective} from "@ngneat/transloco"; +import {ServerService} from "../../../_services/server.service"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {map} from "rxjs/operators"; +import {ChangelogComponent} from "../changelog/changelog.component"; +import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; + +@Component({ + selector: 'app-out-of-date-modal', + standalone: true, + imports: [ + FormsModule, + NgForOf, + NgIf, + NgbHighlight, + NgbTypeahead, + TranslocoDirective, + AsyncPipe, + ChangelogComponent, + SafeHtmlPipe + ], + templateUrl: './out-of-date-modal.component.html', + styleUrl: './out-of-date-modal.component.scss' +}) +export class OutOfDateModalComponent { + + private readonly ngbModal = inject(NgbActiveModal); + + @Input({required: true}) versionsOutOfDate: number = 0; + + close() { + this.ngbModal.close(); + } +} diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index 6442addde..941153732 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -13,6 +13,8 @@ import { SideNavComponent } from './sidenav/_components/side-nav/side-nav.compon import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ServerService} from "./_services/server.service"; +import {ImportCblModalComponent} from "./reading-list/_modals/import-cbl-modal/import-cbl-modal.component"; +import {OutOfDateModalComponent} from "./announcements/_components/out-of-date-modal/out-of-date-modal.component"; @Component({ selector: 'app-root', @@ -67,15 +69,6 @@ export class AppComponent implements OnInit { }); - // Every hour, have the UI check for an update. People seriously stay out of date - // interval(60 * 60 * 1000) // 60 minutes in milliseconds - // .pipe( - // switchMap(() => this.accountService.currentUser$), - // filter(u => u !== undefined && this.accountService.hasAdminRole(u)), - // switchMap(_ => this.serverService.checkForUpdates()) - // ) - // .subscribe(); - this.transitionState$ = this.accountService.currentUser$.pipe( tap(user => { @@ -111,11 +104,21 @@ export class AppComponent implements OnInit { // On load, make an initial call for valid license this.accountService.hasValidLicense().subscribe(); - interval(4 * 60 * 60 * 1000) // 4 hours in milliseconds + // Every hour, have the UI check for an update. People seriously stay out of date + interval(2* 60 * 60 * 1000) // 2 hours in milliseconds .pipe( switchMap(() => this.accountService.currentUser$), - filter(u => this.accountService.hasAdminRole(u!)), - switchMap(_ => this.serverService.checkForUpdates()) + filter(u => u !== undefined && this.accountService.hasAdminRole(u)), + switchMap(_ => this.serverService.checkHowOutOfDate()), + filter(versionOutOfDate => { + return !isNaN(versionOutOfDate) && versionOutOfDate > 2; + }), + tap(versionOutOfDate => { + if (!this.ngbModal.hasOpenModals()) { + const ref = this.ngbModal.open(OutOfDateModalComponent, {size: 'xl', fullscreen: 'md'}); + ref.componentInstance.versionsOutOfDate = 3; + } + }) ) .subscribe(); } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index b740928e3..b6d7573a9 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -288,7 +288,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { if (this.activeTabId === TabID.Chapters) chapterArray = this.chapters; // We must augment chapter indices as Bulk Selection assumes all on one page, but Storyline has mixed - const chapterIndexModifier = this.activeTabId === TabID.Storyline ? this.volumes.length + 1 : 0; + const chapterIndexModifier = this.activeTabId === TabID.Storyline ? this.volumes.length : 0; const selectedChapterIds = chapterArray.filter((_chapter, index: number) => { const mappedIndex = index + chapterIndexModifier; return selectedChapterIndexes.includes(mappedIndex + ''); diff --git a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts index f365eb886..f495d5a54 100644 --- a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts +++ b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts @@ -12,8 +12,9 @@ import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe'; import { NgIf, NgFor } from '@angular/common'; import { EditDeviceComponent } from '../edit-device/edit-device.component'; import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; -import {TranslocoDirective} from "@ngneat/transloco"; +import {translate, TranslocoDirective} from "@ngneat/transloco"; import {SettingsService} from "../../admin/settings.service"; +import {ConfirmService} from "../../shared/confirm.service"; @Component({ selector: 'app-manage-devices', @@ -28,6 +29,7 @@ export class ManageDevicesComponent implements OnInit { private readonly cdRef = inject(ChangeDetectorRef); private readonly deviceService = inject(DeviceService); private readonly settingsService = inject(SettingsService); + private readonly confirmService = inject(ConfirmService); devices: Array = []; addDeviceIsCollapsed: boolean = true; @@ -53,7 +55,8 @@ export class ManageDevicesComponent implements OnInit { }); } - deleteDevice(device: Device) { + async deleteDevice(device: Device) { + if (!await this.confirmService.confirm(translate('toasts.delete-device'))) return; this.deviceService.deleteDevice(device.id).subscribe(() => { const index = this.devices.indexOf(device); this.devices.splice(index, 1); diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index a306d7de8..a57fef24c 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -156,7 +156,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.accountService.hasValidLicense$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => { if (res) { - this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling}); + if (this.tabs.filter(t => t.fragment == FragmentID.Scrobbling).length === 0) { + this.tabs.push({title: 'scrobbling-tab', fragment: FragmentID.Scrobbling}); + } + this.hasActiveLicense = true; this.cdRef.markForCheck(); } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 59d4daec1..660ff2191 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -546,6 +546,15 @@ "title": "Announcements" }, + "out-of-date-modal": { + "title": "Don't fall behind!", + "close": "{{common.close}}", + "subtitle": "It seems your install is more than {{count}} versions behind!", + "description-1": "Please consider upgrading so that you're running the latest version of Kavita.", + "description-2": "Take a look at our wiki for instructions on how to update.", + "description-3": "If there is a specific reason you haven't updated yet we would love to know what is keeping you on an outdated version! Stop by our discord and let us know what is blocking your upgrade path." + }, + "changelog": { "installed": "Installed", "download": "Download", @@ -1190,6 +1199,9 @@ "folder-watching-label": "Folder Watching", "folder-watching-tooltip": "Allows Kavita to monitor Library Folders to detect changes and invoke scanning on those changes. This allows content to be updated without manually invoking scans or waiting for nightly scans. Will always wait 10 minutes before triggering scan.", "enable-folder-watching": "Enable Folder Watching", + "host-name-label": "{{manage-email-settings.host-name-label}}", + "host-name-tooltip": "{{manage-email-settings.host-name-tooltip}}", + "host-name-validation": "{{manage-email-settings.host-name-validation}}", "reset-to-default": "{{common.reset-to-default}}", @@ -1643,7 +1655,8 @@ "last-chapter-added": "Item Added", "time-to-read": "Time to Read", "release-year": "Release Year", - "read-progress": "Last Read" + "read-progress": "Last Read", + "average-rating": "Average Rating" }, "edit-series-modal": { @@ -2029,6 +2042,7 @@ "change-email-no-email": "Email has been updated", "device-updated": "Device updated", "device-created": "Device created", + "delete-device": "Are you sure you want to delete this device?", "confirm-regen-covers": "Refresh covers will force all cover images to be recalculated. This is a heavy operation. Are you sure you don't want to perform a Scan instead?", "alert-long-running": "This is a long running process. Please give it the time to complete before invoking again.", "confirm-delete-multiple-series": "Are you sure you want to delete {{count}} series? It will not modify files on disk.", diff --git a/openapi.json b/openapi.json index 5e743d735..94c6dab79 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.13.10" + "version": "0.7.13.11" }, "servers": [ { @@ -9458,6 +9458,20 @@ } } }, + "/api/Server/check-for-updates": { + "get": { + "tags": [ + "Server" + ], + "summary": "Checks for updates and pushes an event to the UI", + "description": "Some users have websocket issues so this is not always reliable to alert the user", + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/api/Server/check-update": { "get": { "tags": [ @@ -9664,19 +9678,6 @@ } } }, - "/api/Server/check-for-updates": { - "get": { - "tags": [ - "Server" - ], - "summary": "Checks for updates and pushes an event to the UI", - "responses": { - "200": { - "description": "Success" - } - } - } - }, "/api/Settings/base-url": { "get": { "tags": [ @@ -12545,7 +12546,7 @@ "wantToRead": { "type": "array", "items": { - "$ref": "#/components/schemas/Series" + "$ref": "#/components/schemas/AppUserWantToRead" }, "description": "A list of Series the user want's to read", "nullable": true @@ -13264,6 +13265,31 @@ "additionalProperties": false, "description": "A personal table of contents for a given user linked with a given book" }, + "AppUserWantToRead": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "seriesId": { + "type": "integer", + "format": "int32" + }, + "series": { + "$ref": "#/components/schemas/Series" + }, + "appUser": { + "$ref": "#/components/schemas/AppUser" + }, + "appUserId": { + "type": "integer", + "description": "User this table of content belongs to", + "format": "int32" + } + }, + "additionalProperties": false + }, "BookChapterItem": { "type": "object", "properties": { @@ -19034,7 +19060,8 @@ 4, 5, 6, - 7 + 7, + 8 ], "type": "integer", "format": "int32" @@ -20339,7 +20366,7 @@ }, "number": { "type": "number", - "description": "This will map to MinNumber. Number was removed in v0.7.13.8", + "description": "This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14", "format": "float", "deprecated": true },