From 15e09a0cf1f7813329bda7756c52efec6553fb69 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Tue, 15 Nov 2022 08:45:02 -0600 Subject: [PATCH] Fixed Series Relations Schema (#1654) * Bump loader-utils from 2.0.2 to 2.0.3 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.2 to 2.0.3. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.3/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.2...v2.0.3) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] * Fixed is want to read coming back as a string and not working correctly. * Changed from to Continue to be more explicit * Added the first migration which exports data as a csv in temp/. This is the backup in case data is lost in the migration. * Note for later * Fixed the migration for the series relation so when deleting any series on any edge of the relationship, the SeriesRelation row deletes. * Change buttons back to titles on series detail page * Wrote the code to import relations from the backup. * Added an additional version check to avoid file io on migration. * Code cleanup Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- API.Tests/Services/SeriesServiceTests.cs | 107 ++ API/API.csproj | 1 + API/Controllers/WantToReadController.cs | 2 +- API/Data/DataContext.cs | 6 +- API/Data/MigrateSeriesRelationsExport.cs | 104 + API/Data/MigrateSeriesRelationsImport.cs | 64 + ...115021908_SeriesRelationChange.Designer.cs | 1673 +++++++++++++++++ .../20221115021908_SeriesRelationChange.cs | 61 + .../Migrations/DataContextModelSnapshot.cs | 64 +- API/Data/Repositories/SeriesRepository.cs | 42 +- API/Entities/Metadata/SeriesRelation.cs | 4 +- API/Program.cs | 5 + API/Startup.cs | 5 + API/config/relations-imported.csv | 10 + UI/Web/package-lock.json | 24 +- UI/Web/src/app/_services/series.service.ts | 5 +- .../series-detail.component.html | 8 +- .../series-detail.component.scss | 9 - 18 files changed, 2111 insertions(+), 83 deletions(-) create mode 100644 API/Data/MigrateSeriesRelationsExport.cs create mode 100644 API/Data/MigrateSeriesRelationsImport.cs create mode 100644 API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs create mode 100644 API/Data/Migrations/20221115021908_SeriesRelationChange.cs create mode 100644 API/config/relations-imported.csv diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 8307136b7..33f3595fe 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -1240,6 +1240,113 @@ public class SeriesServiceTests Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 2)); } + + [Fact] + public async Task UpdateRelatedSeries_DeleteTargetSeries_ShouldSucceed() + { + await ResetDb(); + _context.Library.Add(new Library() + { + AppUsers = new List() + { + new AppUser() + { + UserName = "majora2007" + } + }, + Name = "Test LIb", + Type = LibraryType.Book, + Series = new List() + { + new Series() + { + Name = "Series A", + Volumes = new List(){} + }, + new Series() + { + Name = "Series B", + Volumes = new List(){} + }, + } + }); + + await _context.SaveChangesAsync(); + + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + // Add relations + var addRelationDto = CreateRelationsDto(series1); + addRelationDto.Adaptations.Add(2); + await _seriesService.UpdateRelatedSeries(addRelationDto); + Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); + + _context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); + try + { + await _context.SaveChangesAsync(); + } + catch (Exception) + { + Assert.Fail("Delete of Target Series Failed"); + } + + // Remove relations + Assert.Empty((await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related)).Relations); + } + + [Fact] + public async Task UpdateRelatedSeries_DeleteSourceSeries_ShouldSucceed() + { + await ResetDb(); + _context.Library.Add(new Library() + { + AppUsers = new List() + { + new AppUser() + { + UserName = "majora2007" + } + }, + Name = "Test LIb", + Type = LibraryType.Book, + Series = new List() + { + new Series() + { + Name = "Series A", + Volumes = new List(){} + }, + new Series() + { + Name = "Series B", + Volumes = new List(){} + }, + } + }); + + await _context.SaveChangesAsync(); + + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + // Add relations + var addRelationDto = CreateRelationsDto(series1); + addRelationDto.Adaptations.Add(2); + await _seriesService.UpdateRelatedSeries(addRelationDto); + Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); + + _context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1)); + try + { + await _context.SaveChangesAsync(); + } + catch (Exception) + { + Assert.Fail("Delete of Target Series Failed"); + } + + // Remove relations + Assert.Empty((await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related)).Relations); + } + [Fact] public async Task UpdateRelatedSeries_ShouldNotAllowDuplicates() { diff --git a/API/API.csproj b/API/API.csproj index ba8759d03..1310f09a4 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -48,6 +48,7 @@ + diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index c0465a988..30b8cb05d 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -42,7 +42,7 @@ public class WantToReadController : BaseApiController } [HttpGet] - public async Task>> GetWantToRead([FromQuery] int seriesId) + public async Task> GetWantToRead([FromQuery] int seriesId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); return Ok(await _unitOfWork.SeriesRepository.IsSeriesInWantToRead(user.Id, seriesId)); diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index c00289227..2e681c62d 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -68,13 +68,15 @@ public sealed class DataContext : IdentityDbContext pt.Series) .WithMany(p => p.Relations) .HasForeignKey(pt => pt.SeriesId) - .OnDelete(DeleteBehavior.ClientCascade); + .OnDelete(DeleteBehavior.Cascade); + builder.Entity() .HasOne(pt => pt.TargetSeries) .WithMany(t => t.RelationOf) .HasForeignKey(pt => pt.TargetSeriesId) - .OnDelete(DeleteBehavior.ClientCascade); + .OnDelete(DeleteBehavior.Cascade); + builder.Entity() diff --git a/API/Data/MigrateSeriesRelationsExport.cs b/API/Data/MigrateSeriesRelationsExport.cs new file mode 100644 index 000000000..874769708 --- /dev/null +++ b/API/Data/MigrateSeriesRelationsExport.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using API.Entities.Enums; +using CsvHelper; +using Kavita.Common.EnvironmentInfo; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data; + +internal sealed class SeriesRelationMigrationOutput +{ + public string SeriesName { get; set; } + public int SeriesId { get; set; } + public string TargetSeriesName { get; set; } + public int TargetId { get; set; } + public RelationKind Relationship { get; set; } +} + +/// +/// Introduced in v0.6.1.2 and v0.7, this exports to a temp file the existing series relationships. It is a 3 part migration. +/// This will run first, to export the data, then the DB migration will change the way the DB is constructed, then the last migration +/// will import said file and re-construct the relationships. +/// +public static class MigrateSeriesRelationsExport +{ + private const string OutputFile = "config/relations.csv"; + private const string CompleteOutputFile = "config/relations-imported.csv"; + public static async Task Migrate(DataContext dataContext, ILogger logger) + { + logger.LogCritical("Running MigrateSeriesRelationsExport migration - Please be patient, this may take some time. This is not an error"); + if (BuildInfo.Version > new Version(0, 6, 1, 3) + || new FileInfo(OutputFile).Exists + || new FileInfo(CompleteOutputFile).Exists) + { + logger.LogCritical("Running MigrateSeriesRelationsExport migration - complete. Nothing to do"); + return; + } + + var seriesWithRelationships = await dataContext.Series + .Where(s => s.Relations.Any()) + .Include(s => s.Relations) + .ThenInclude(r => r.TargetSeries) + .ToListAsync(); + + var records = new List(); + var excludedRelationships = new List() + { + RelationKind.Parent, + }; + foreach (var series in seriesWithRelationships) + { + foreach (var relationship in series.Relations.Where(r => !excludedRelationships.Contains(r.RelationKind))) + { + records.Add(new SeriesRelationMigrationOutput() + { + SeriesId = series.Id, + SeriesName = series.Name, + Relationship = relationship.RelationKind, + TargetId = relationship.TargetSeriesId, + TargetSeriesName = relationship.TargetSeries.Name + }); + } + } + + await using var writer = new StreamWriter(OutputFile); + await using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) + { + await csv.WriteRecordsAsync(records); + } + + await writer.DisposeAsync(); + + logger.LogCritical("{OutputFile} has a backup of all data", OutputFile); + + logger.LogCritical("Deleting all relationships in the DB. This is not an error"); + var entities = await dataContext.SeriesRelation + .Include(s => s.Series) + .Include(s => s.TargetSeries) + .Select(s => s) + .ToListAsync(); + + foreach (var seriesWithRelationship in entities) + { + logger.LogCritical("Deleting {SeriesName} --{RelationshipKind}--> {TargetSeriesName}", + seriesWithRelationship.Series.Name, seriesWithRelationship.RelationKind, seriesWithRelationship.TargetSeries.Name); + dataContext.SeriesRelation.Remove(seriesWithRelationship); + + await dataContext.SaveChangesAsync(); + } + + // In case of corrupted entities (where series were deleted but their Id still existed, we delete the rest of the table) + dataContext.SeriesRelation.RemoveRange(dataContext.SeriesRelation); + await dataContext.SaveChangesAsync(); + + + logger.LogCritical("Running MigrateSeriesRelationsExport migration - Completed. This is not an error"); + } +} diff --git a/API/Data/MigrateSeriesRelationsImport.cs b/API/Data/MigrateSeriesRelationsImport.cs new file mode 100644 index 000000000..dc4938d8a --- /dev/null +++ b/API/Data/MigrateSeriesRelationsImport.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using API.Entities.Enums; +using API.Entities.Metadata; +using CsvHelper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data; + +/// +/// Introduced in v0.6.1.2 and v0.7, this imports to a temp file the existing series relationships. It is a 3 part migration. +/// This will run last, to import the data and re-construct the relationships. +/// +public static class MigrateSeriesRelationsImport +{ + private const string OutputFile = "config/relations.csv"; + private const string CompleteOutputFile = "config/relations-imported.csv"; + public static async Task Migrate(DataContext dataContext, ILogger logger) + { + logger.LogCritical("Running MigrateSeriesRelationsImport migration - Please be patient, this may take some time. This is not an error"); + if (!new FileInfo(OutputFile).Exists) + { + logger.LogCritical("Running MigrateSeriesRelationsImport migration - complete. Nothing to do"); + return; + } + + logger.LogCritical("Loading backed up relationships into the DB"); + List records; + using var reader = new StreamReader(OutputFile); + using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) + { + records = csv.GetRecords().ToList(); + } + + foreach (var relation in records) + { + logger.LogCritical("Importing {SeriesName} --{RelationshipKind}--> {TargetSeriesName}", + relation.SeriesName, relation.Relationship, relation.TargetSeriesName); + + // Filter out series that don't exist + if (!await dataContext.Series.AnyAsync(s => s.Id == relation.SeriesId) || + !await dataContext.Series.AnyAsync(s => s.Id == relation.TargetId)) + continue; + + await dataContext.SeriesRelation.AddAsync(new SeriesRelation() + { + SeriesId = relation.SeriesId, + TargetSeriesId = relation.TargetId, + RelationKind = relation.Relationship + }); + + } + await dataContext.SaveChangesAsync(); + + File.Move(OutputFile, CompleteOutputFile); + + logger.LogCritical("Running MigrateSeriesRelationsImport migration - Completed. This is not an error"); + } +} diff --git a/API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs b/API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs new file mode 100644 index 000000000..d9a964ad0 --- /dev/null +++ b/API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs @@ -0,0 +1,1673 @@ +// +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("20221115021908_SeriesRelationChange")] + partial class SeriesRelationChange + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + + 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("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .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("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .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.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("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + 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("ShowScreenHints") + .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("LastModified") + .HasColumnType("TEXT"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + 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("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + 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.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + 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("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .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("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("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("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .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("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + 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("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("LastModified") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .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.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .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("NameLocked") + .HasColumnType("INTEGER"); + + 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("AppUserId"); + + 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.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .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("ExternalTag") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle", "ExternalTag") + .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("LastModified") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + 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("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.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.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", null) + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + 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.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.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + 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.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + 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("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("Devices"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + 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/20221115021908_SeriesRelationChange.cs b/API/Data/Migrations/20221115021908_SeriesRelationChange.cs new file mode 100644 index 000000000..83c3fdc60 --- /dev/null +++ b/API/Data/Migrations/20221115021908_SeriesRelationChange.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + public partial class SeriesRelationChange : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_SeriesRelation_Series_SeriesId", + table: "SeriesRelation"); + + migrationBuilder.DropForeignKey( + name: "FK_SeriesRelation_Series_TargetSeriesId", + table: "SeriesRelation"); + + migrationBuilder.AddForeignKey( + name: "FK_SeriesRelation_Series_SeriesId", + table: "SeriesRelation", + column: "SeriesId", + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SeriesRelation_Series_TargetSeriesId", + table: "SeriesRelation", + column: "TargetSeriesId", + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_SeriesRelation_Series_SeriesId", + table: "SeriesRelation"); + + migrationBuilder.DropForeignKey( + name: "FK_SeriesRelation_Series_TargetSeriesId", + table: "SeriesRelation"); + + migrationBuilder.AddForeignKey( + name: "FK_SeriesRelation_Series_SeriesId", + table: "SeriesRelation", + column: "SeriesId", + principalTable: "Series", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_SeriesRelation_Series_TargetSeriesId", + table: "SeriesRelation", + column: "TargetSeriesId", + principalTable: "Series", + principalColumn: "Id"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index ca7164702..c3f5c8e53 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -165,7 +165,7 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); - b.ToTable("AppUserBookmark", (string)null); + b.ToTable("AppUserBookmark"); }); modelBuilder.Entity("API.Entities.AppUserPreferences", b => @@ -256,7 +256,7 @@ namespace API.Data.Migrations b.HasIndex("ThemeId"); - b.ToTable("AppUserPreferences", (string)null); + b.ToTable("AppUserPreferences"); }); modelBuilder.Entity("API.Entities.AppUserProgress", b => @@ -295,7 +295,7 @@ namespace API.Data.Migrations b.HasIndex("SeriesId"); - b.ToTable("AppUserProgresses", (string)null); + b.ToTable("AppUserProgresses"); }); modelBuilder.Entity("API.Entities.AppUserRating", b => @@ -322,7 +322,7 @@ namespace API.Data.Migrations b.HasIndex("SeriesId"); - b.ToTable("AppUserRating", (string)null); + b.ToTable("AppUserRating"); }); modelBuilder.Entity("API.Entities.AppUserRole", b => @@ -413,7 +413,7 @@ namespace API.Data.Migrations b.HasIndex("VolumeId"); - b.ToTable("Chapter", (string)null); + b.ToTable("Chapter"); }); modelBuilder.Entity("API.Entities.CollectionTag", b => @@ -448,7 +448,7 @@ namespace API.Data.Migrations b.HasIndex("Id", "Promoted") .IsUnique(); - b.ToTable("CollectionTag", (string)null); + b.ToTable("CollectionTag"); }); modelBuilder.Entity("API.Entities.Device", b => @@ -485,7 +485,7 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); - b.ToTable("Device", (string)null); + b.ToTable("Device"); }); modelBuilder.Entity("API.Entities.FolderPath", b => @@ -507,7 +507,7 @@ namespace API.Data.Migrations b.HasIndex("LibraryId"); - b.ToTable("FolderPath", (string)null); + b.ToTable("FolderPath"); }); modelBuilder.Entity("API.Entities.Genre", b => @@ -530,7 +530,7 @@ namespace API.Data.Migrations b.HasIndex("NormalizedTitle", "ExternalTag") .IsUnique(); - b.ToTable("Genre", (string)null); + b.ToTable("Genre"); }); modelBuilder.Entity("API.Entities.Library", b => @@ -559,7 +559,7 @@ namespace API.Data.Migrations b.HasKey("Id"); - b.ToTable("Library", (string)null); + b.ToTable("Library"); }); modelBuilder.Entity("API.Entities.MangaFile", b => @@ -593,7 +593,7 @@ namespace API.Data.Migrations b.HasIndex("ChapterId"); - b.ToTable("MangaFile", (string)null); + b.ToTable("MangaFile"); }); modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => @@ -689,7 +689,7 @@ namespace API.Data.Migrations b.HasIndex("Id", "SeriesId") .IsUnique(); - b.ToTable("SeriesMetadata", (string)null); + b.ToTable("SeriesMetadata"); }); modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => @@ -713,7 +713,7 @@ namespace API.Data.Migrations b.HasIndex("TargetSeriesId"); - b.ToTable("SeriesRelation", (string)null); + b.ToTable("SeriesRelation"); }); modelBuilder.Entity("API.Entities.Person", b => @@ -733,7 +733,7 @@ namespace API.Data.Migrations b.HasKey("Id"); - b.ToTable("Person", (string)null); + b.ToTable("Person"); }); modelBuilder.Entity("API.Entities.ReadingList", b => @@ -776,7 +776,7 @@ namespace API.Data.Migrations b.HasIndex("AppUserId"); - b.ToTable("ReadingList", (string)null); + b.ToTable("ReadingList"); }); modelBuilder.Entity("API.Entities.ReadingListItem", b => @@ -810,7 +810,7 @@ namespace API.Data.Migrations b.HasIndex("VolumeId"); - b.ToTable("ReadingListItem", (string)null); + b.ToTable("ReadingListItem"); }); modelBuilder.Entity("API.Entities.Series", b => @@ -897,7 +897,7 @@ namespace API.Data.Migrations b.HasIndex("LibraryId"); - b.ToTable("Series", (string)null); + b.ToTable("Series"); }); modelBuilder.Entity("API.Entities.ServerSetting", b => @@ -914,7 +914,7 @@ namespace API.Data.Migrations b.HasKey("Key"); - b.ToTable("ServerSetting", (string)null); + b.ToTable("ServerSetting"); }); modelBuilder.Entity("API.Entities.SiteTheme", b => @@ -946,7 +946,7 @@ namespace API.Data.Migrations b.HasKey("Id"); - b.ToTable("SiteTheme", (string)null); + b.ToTable("SiteTheme"); }); modelBuilder.Entity("API.Entities.Tag", b => @@ -969,7 +969,7 @@ namespace API.Data.Migrations b.HasIndex("NormalizedTitle", "ExternalTag") .IsUnique(); - b.ToTable("Tag", (string)null); + b.ToTable("Tag"); }); modelBuilder.Entity("API.Entities.Volume", b => @@ -1015,7 +1015,7 @@ namespace API.Data.Migrations b.HasIndex("SeriesId"); - b.ToTable("Volume", (string)null); + b.ToTable("Volume"); }); modelBuilder.Entity("AppUserLibrary", b => @@ -1030,7 +1030,7 @@ namespace API.Data.Migrations b.HasIndex("LibrariesId"); - b.ToTable("AppUserLibrary", (string)null); + b.ToTable("AppUserLibrary"); }); modelBuilder.Entity("ChapterGenre", b => @@ -1045,7 +1045,7 @@ namespace API.Data.Migrations b.HasIndex("GenresId"); - b.ToTable("ChapterGenre", (string)null); + b.ToTable("ChapterGenre"); }); modelBuilder.Entity("ChapterPerson", b => @@ -1060,7 +1060,7 @@ namespace API.Data.Migrations b.HasIndex("PeopleId"); - b.ToTable("ChapterPerson", (string)null); + b.ToTable("ChapterPerson"); }); modelBuilder.Entity("ChapterTag", b => @@ -1075,7 +1075,7 @@ namespace API.Data.Migrations b.HasIndex("TagsId"); - b.ToTable("ChapterTag", (string)null); + b.ToTable("ChapterTag"); }); modelBuilder.Entity("CollectionTagSeriesMetadata", b => @@ -1090,7 +1090,7 @@ namespace API.Data.Migrations b.HasIndex("SeriesMetadatasId"); - b.ToTable("CollectionTagSeriesMetadata", (string)null); + b.ToTable("CollectionTagSeriesMetadata"); }); modelBuilder.Entity("GenreSeriesMetadata", b => @@ -1105,7 +1105,7 @@ namespace API.Data.Migrations b.HasIndex("SeriesMetadatasId"); - b.ToTable("GenreSeriesMetadata", (string)null); + b.ToTable("GenreSeriesMetadata"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => @@ -1204,7 +1204,7 @@ namespace API.Data.Migrations b.HasIndex("SeriesMetadatasId"); - b.ToTable("PersonSeriesMetadata", (string)null); + b.ToTable("PersonSeriesMetadata"); }); modelBuilder.Entity("SeriesMetadataTag", b => @@ -1219,7 +1219,7 @@ namespace API.Data.Migrations b.HasIndex("TagsId"); - b.ToTable("SeriesMetadataTag", (string)null); + b.ToTable("SeriesMetadataTag"); }); modelBuilder.Entity("API.Entities.AppUserBookmark", b => @@ -1363,13 +1363,13 @@ namespace API.Data.Migrations b.HasOne("API.Entities.Series", "Series") .WithMany("Relations") .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.ClientCascade) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("API.Entities.Series", "TargetSeries") .WithMany("RelationOf") .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.ClientCascade) + .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Series"); diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 9151e6861..97f1eebba 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -1257,15 +1257,6 @@ public class SeriesRepository : ISeriesRepository .Where(s => !ids.Contains(s.Id)) .ToListAsync(); - // If the series to remove has Relation (related series), we must manually unlink due to the DB not being - // setup correctly (if this is not done, a foreign key constraint will be thrown) - - foreach (var sr in seriesToRemove) - { - sr.Relations = new List(); - Update(sr); - } - _context.Series.RemoveRange(seriesToRemove); return seriesToRemove; @@ -1387,14 +1378,26 @@ public class SeriesRepository : ISeriesRepository AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating), AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating), Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating), - Parent = await _context.Series - .SelectMany(s => - s.RelationOf.Where(r => r.TargetSeriesId == seriesId - && usersSeriesIds.Contains(r.TargetSeriesId) - && r.RelationKind != RelationKind.Prequel - && r.RelationKind != RelationKind.Sequel - && r.RelationKind != RelationKind.Edition) - .Select(sr => sr.Series)) + // Parent = await _context.Series + // .SelectMany(s => + // s.TargetSeries.Where(r => r.TargetSeriesId == seriesId + // && usersSeriesIds.Contains(r.TargetSeriesId) + // && r.RelationKind != RelationKind.Prequel + // && r.RelationKind != RelationKind.Sequel + // && r.RelationKind != RelationKind.Edition) + // .Select(sr => sr.Series)) + // .RestrictAgainstAgeRestriction(userRating) + // .AsSplitQuery() + // .AsNoTracking() + // .ProjectTo(_mapper.ConfigurationProvider) + // .ToListAsync(), + Parent = await _context.SeriesRelation + .Where(r => r.TargetSeriesId == seriesId + && usersSeriesIds.Contains(r.TargetSeriesId) + && r.RelationKind != RelationKind.Prequel + && r.RelationKind != RelationKind.Sequel + && r.RelationKind != RelationKind.Edition) + .Select(sr => sr.Series) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AsNoTracking() @@ -1477,13 +1480,14 @@ public class SeriesRepository : ISeriesRepository public async Task IsSeriesInWantToRead(int userId, int seriesId) { + // BUG: This is always returning true for any series var libraryIds = GetLibraryIdsForUser(userId); return await _context.AppUser .Where(user => user.Id == userId) - .SelectMany(u => u.WantToRead) + .SelectMany(u => u.WantToRead.Where(s => s.Id == seriesId && libraryIds.Contains(s.LibraryId))) .AsSplitQuery() .AsNoTracking() - .AnyAsync(s => libraryIds.Contains(s.LibraryId) && s.Id == seriesId); + .AnyAsync(); } public async Task>> GetFolderPathMap(int libraryId) diff --git a/API/Entities/Metadata/SeriesRelation.cs b/API/Entities/Metadata/SeriesRelation.cs index bb152264a..f5263f11d 100644 --- a/API/Entities/Metadata/SeriesRelation.cs +++ b/API/Entities/Metadata/SeriesRelation.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using API.Entities.Enums; +using API.Entities.Enums; namespace API.Entities.Metadata; diff --git a/API/Program.cs b/API/Program.cs index 6e1d3f365..9f69cdf3c 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -39,6 +39,8 @@ public class Program Console.OutputEncoding = System.Text.Encoding.UTF8; Log.Logger = new LoggerConfiguration() .WriteTo.Console() + .MinimumLevel + .Information() .CreateBootstrapLogger(); var directoryService = new DirectoryService(null, new FileSystem()); @@ -79,6 +81,9 @@ public class Program } } + // This must run before the migration + await MigrateSeriesRelationsExport.Migrate(context, logger); + await context.Database.MigrateAsync(); await Seed.SeedRoles(services.GetRequiredService>()); diff --git a/API/Startup.cs b/API/Startup.cs index a9fef97b8..a22422d6a 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -209,6 +209,7 @@ public class Startup var readingListService = serviceProvider.GetRequiredService(); + logger.LogInformation("Running Migrations"); // Only run this if we are upgrading await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager); await MigrateRemoveExtraThemes.Migrate(unitOfWork, themeService); @@ -220,12 +221,16 @@ public class Startup await MigrateChangeRestrictionRoles.Migrate(unitOfWork, userManager, logger); await MigrateReadingListAgeRating.Migrate(unitOfWork, dataContext, readingListService, logger); + // v0.6.2 or v0.7 + await MigrateSeriesRelationsImport.Migrate(dataContext, logger); + // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); installVersion.Value = BuildInfo.Version.ToString(); unitOfWork.SettingsRepository.Update(installVersion); await unitOfWork.CommitAsync(); + logger.LogInformation("Running Migrations - done"); }).GetAwaiter() .GetResult(); } diff --git a/API/config/relations-imported.csv b/API/config/relations-imported.csv new file mode 100644 index 000000000..608421b32 --- /dev/null +++ b/API/config/relations-imported.csv @@ -0,0 +1,10 @@ +SeriesName,SeriesId,TargetSeriesName,TargetId,Relationship +Kaguya-sama - Love Is War,308,Kaguya-sama - Love Is War - Digital Colored Comics,307,AlternativeVersion +Kaguya-sama - Love Is War,308,Kaguya Wants To Be Confessed To Official Doujin,306,SpinOff +Konosuba,341,Konosuba - An Explosion on This Wonderful World!,342,SideStory +Konosuba,341,Kono Subarashii Sekai ni Nichijou wo!,337,SideStory +Accel World,1739,Accel World,887,Edition +24 Hours in Ancient Athens,1748,Kono Subarashii Sekai ni Nichijou wo!,337,Adaptation +24 Hours in Ancient Athens,1748,Accel World,887,Adaptation +Subete no Jidai o Tsuujite no Satsujinjutsu,1877,Accel World,887,Adaptation +KonoSuba,2032,Konosuba,341,Adaptation diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index aba7c9f7b..b5a4c61b7 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -5089,9 +5089,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -6746,9 +6746,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -7115,9 +7115,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -14660,9 +14660,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", + "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index f60767e85..ef6cbcea0 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -131,7 +131,10 @@ export class SeriesService { } isWantToRead(seriesId: number) { - return this.httpClient.get(this.baseUrl + 'want-to-read?seriesId=' + seriesId, {responseType: 'text' as 'json'}); + return this.httpClient.get(this.baseUrl + 'want-to-read?seriesId=' + seriesId, {responseType: 'text' as 'json'}) + .pipe(map(val => { + return val === 'true'; + })); } getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) { diff --git a/UI/Web/src/app/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/series-detail.component.html index e322e6f8c..5cd75e4ca 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/series-detail.component.html @@ -64,7 +64,7 @@
- From {{ContinuePointTitle}} + Continue {{ContinuePointTitle}}
@@ -78,14 +78,14 @@
-
-