From 348bc062ee2af538ba7c08200a8c3fa62eae131e Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sat, 15 Jul 2023 09:27:21 -0500 Subject: [PATCH] Remove From On Deck (#2131) * Allow admins to customize the amount of progress time or last item added time for on deck calculation * Implemented the ability to remove series from on deck. They will be removed until the user reads a new chapter. Quite a few db lookup reduction calls for reading based stuff, like continue point, bookmarks, etc. --- API/Controllers/ReaderController.cs | 83 +- API/Controllers/SeriesController.cs | 12 + API/Controllers/SettingsController.cs | 18 + API/DTOs/Settings/ServerSettingDTO.cs | 8 + API/Data/DataContext.cs | 1 + .../20230715125951_OnDeckRemoval.Designer.cs | 2184 +++++++++++++++++ .../20230715125951_OnDeckRemoval.cs | 93 + .../Migrations/DataContextModelSnapshot.cs | 46 +- API/Data/Repositories/SeriesRepository.cs | 48 +- API/Data/Seed.cs | 2 + API/Entities/AppUser.cs | 4 + API/Entities/AppUserOnDeckRemoval.cs | 11 + API/Entities/Enums/ServerSettingKey.cs | 10 + .../Converters/ServerSettingConverter.cs | 6 + API/Services/ReaderService.cs | 1 + .../app/_services/action-factory.service.ts | 11 +- UI/Web/src/app/_services/series.service.ts | 4 + .../src/app/admin/_models/server-settings.ts | 2 + .../manage-settings.component.html | 33 +- .../manage-settings.component.ts | 8 +- .../series-card/series-card.component.ts | 40 +- .../_components/dashboard.component.html | 3 +- .../_components/dashboard.component.ts | 20 +- openapi.json | 36 +- 24 files changed, 2597 insertions(+), 87 deletions(-) create mode 100644 API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs create mode 100644 API/Data/Migrations/20230715125951_OnDeckRemoval.cs create mode 100644 API/Entities/AppUserOnDeckRemoval.cs diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index ffa650a73..356592dfd 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -314,6 +314,7 @@ public class ReaderController : BaseApiController if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress"); BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); + BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id)); return Ok(); } @@ -376,13 +377,11 @@ public class ReaderController : BaseApiController MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId, markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages))); - if (await _unitOfWork.CommitAsync()) - { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); - return Ok(); - } + if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress"); - return BadRequest("Could not save progress"); + BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); + BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id)); + return Ok(); } @@ -406,14 +405,12 @@ public class ReaderController : BaseApiController var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList()); - if (await _unitOfWork.CommitAsync()) - { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); - return Ok(); - } + if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress"); + BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); + BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id)); + return Ok(); - return BadRequest("Could not save progress"); } /// @@ -463,16 +460,14 @@ public class ReaderController : BaseApiController await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } - if (await _unitOfWork.CommitAsync()) - { - foreach (var sId in dto.SeriesIds) - { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); - } - return Ok(); - } + if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress"); - return BadRequest("Could not save progress"); + foreach (var sId in dto.SeriesIds) + { + BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); + BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(sId, user.Id)); + } + return Ok(); } /// @@ -530,11 +525,14 @@ public class ReaderController : BaseApiController /// /// [HttpPost("progress")] - public async Task BookmarkProgress(ProgressDto progressDto) + public async Task SaveProgress(ProgressDto progressDto) { - if (await _readerService.SaveReadingProgress(progressDto, User.GetUserId())) return Ok(true); + var userId = User.GetUserId(); + if (!await _readerService.SaveReadingProgress(progressDto, userId)) + return BadRequest("Could not save progress"); - return BadRequest("Could not save progress"); + + return Ok(true); } /// @@ -545,9 +543,7 @@ public class ReaderController : BaseApiController [HttpGet("continue-point")] public async Task> GetContinuePoint(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - - return Ok(await _readerService.GetContinuePoint(seriesId, userId)); + return Ok(await _readerService.GetContinuePoint(seriesId, User.GetUserId())); } /// @@ -558,8 +554,7 @@ public class ReaderController : BaseApiController [HttpGet("has-progress")] public async Task> HasProgress(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId)); + return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, User.GetUserId())); } /// @@ -570,10 +565,7 @@ public class ReaderController : BaseApiController [HttpGet("chapter-bookmarks")] public async Task>> GetBookmarks(int chapterId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user == null) return Unauthorized(); - if (user.Bookmarks == null) return Ok(Array.Empty()); - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId)); + return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(User.GetUserId(), chapterId)); } /// @@ -584,11 +576,7 @@ public class ReaderController : BaseApiController [HttpPost("all-bookmarks")] public async Task>> GetAllBookmarks(FilterDto filterDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user == null) return Unauthorized(); - if (user.Bookmarks == null) return Ok(Array.Empty()); - - return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto)); + return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(User.GetUserId(), filterDto)); } /// @@ -676,10 +664,7 @@ public class ReaderController : BaseApiController [HttpGet("volume-bookmarks")] public async Task>> GetBookmarksForVolume(int volumeId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user == null) return Unauthorized(); - if (user.Bookmarks == null) return Ok(Array.Empty()); - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId)); + return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(User.GetUserId(), volumeId)); } /// @@ -690,11 +675,7 @@ public class ReaderController : BaseApiController [HttpGet("series-bookmarks")] public async Task>> GetBookmarksForSeries(int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user == null) return Unauthorized(); - if (user.Bookmarks == null) return Ok(Array.Empty()); - - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId)); + return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(User.GetUserId(), seriesId)); } /// @@ -760,8 +741,7 @@ public class ReaderController : BaseApiController [HttpGet("next-chapter")] public async Task> GetNextChapter(int seriesId, int volumeId, int currentChapterId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, userId); + return await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, User.GetUserId()); } @@ -779,8 +759,7 @@ public class ReaderController : BaseApiController [HttpGet("prev-chapter")] public async Task> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); - return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId); + return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, User.GetUserId()); } /// @@ -793,7 +772,7 @@ public class ReaderController : BaseApiController [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId"})] public async Task> GetEstimateToCompletion(int seriesId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + var userId = User.GetUserId(); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); // Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 25336ec61..6c3b7ced8 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -284,6 +284,18 @@ public class SeriesController : BaseApiController return Ok(pagedList); } + /// + /// Removes a series from displaying on deck until the next read event on that series + /// + /// + /// + [HttpPost("remove-from-on-deck")] + public async Task RemoveFromOnDeck([FromQuery] int seriesId) + { + await _unitOfWork.SeriesRepository.RemoveFromOnDeck(seriesId, User.GetUserId()); + return Ok(); + } + /// /// Runs a Cover Image Generation task /// diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 33556e6ad..9466fe6dd 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -182,6 +182,24 @@ public class SettingsController : BaseApiController _unitOfWork.SettingsRepository.Update(setting); } + if (setting.Key == ServerSettingKey.OnDeckProgressDays && updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.OnDeckUpdateDays && updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) + { + setting.Value = updateSettingsDto.TaskScan; + _unitOfWork.SettingsRepository.Update(setting); + } + if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value) { if (OsInfo.IsDocker) continue; diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 1a33d7175..15dd9177b 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -76,4 +76,12 @@ public class ServerSettingDto /// The size in MB for Caching API data /// public long CacheSize { get; set; } + /// + /// How many Days since today in the past for reading progress, should content be considered for On Deck, before it gets removed automatically + /// + public int OnDeckProgressDays { get; set; } + /// + /// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically + /// + public int OnDeckUpdateDays { get; set; } } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 416ec5d05..e4dc6b746 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -52,6 +52,7 @@ public sealed class DataContext : IdentityDbContext ScrobbleEvent { get; set; } = null!; public DbSet ScrobbleError { get; set; } = null!; public DbSet ScrobbleHold { get; set; } = null!; + public DbSet AppUserOnDeckRemoval { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) diff --git a/API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs b/API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs new file mode 100644 index 000000000..90035e9f0 --- /dev/null +++ b/API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs @@ -0,0 +1,2184 @@ +// +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("20230715125951_OnDeckRemoval")] + partial class OnDeckRemoval + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.8"); + + 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.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("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("Rating") + .HasColumnType("INTEGER"); + + 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.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.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.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.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("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("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("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.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("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.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.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.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.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("ScrobbleHolds"); + + 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("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/20230715125951_OnDeckRemoval.cs b/API/Data/Migrations/20230715125951_OnDeckRemoval.cs new file mode 100644 index 000000000..3cc27196f --- /dev/null +++ b/API/Data/Migrations/20230715125951_OnDeckRemoval.cs @@ -0,0 +1,93 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class OnDeckRemoval : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Title", + table: "ReadingList", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "NormalizedTitle", + table: "ReadingList", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "AppUserOnDeckRemoval", + 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_AppUserOnDeckRemoval", x => x.Id); + table.ForeignKey( + name: "FK_AppUserOnDeckRemoval_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserOnDeckRemoval_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserOnDeckRemoval_AppUserId", + table: "AppUserOnDeckRemoval", + column: "AppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserOnDeckRemoval_SeriesId", + table: "AppUserOnDeckRemoval", + column: "SeriesId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUserOnDeckRemoval"); + + migrationBuilder.AlterColumn( + name: "Title", + table: "ReadingList", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "NormalizedTitle", + table: "ReadingList", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index e4fd9dd2c..cc8d2660f 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", "7.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.8"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -183,6 +183,27 @@ namespace API.Data.Migrations b.ToTable("AppUserBookmark"); }); + 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") @@ -943,6 +964,7 @@ namespace API.Data.Migrations .HasColumnType("TEXT"); b.Property("NormalizedTitle") + .IsRequired() .HasColumnType("TEXT"); b.Property("Promoted") @@ -958,6 +980,7 @@ namespace API.Data.Migrations .HasColumnType("TEXT"); b.Property("Title") + .IsRequired() .HasColumnType("TEXT"); b.HasKey("Id"); @@ -1082,7 +1105,7 @@ namespace API.Data.Migrations b.Property("LibraryId") .HasColumnType("INTEGER"); - b.Property("MalId") + b.Property("MalId") .HasColumnType("INTEGER"); b.Property("ProcessDateUtc") @@ -1626,6 +1649,25 @@ namespace API.Data.Migrations 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") diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index dfa85dcad..cf47e8688 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -14,6 +14,7 @@ using API.DTOs.Metadata; using API.DTOs.ReadingLists; using API.DTOs.Search; using API.DTOs.SeriesDetail; +using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; @@ -137,6 +138,8 @@ public interface ISeriesRepository Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); Task GetAverageUserRating(int seriesId); + Task RemoveFromOnDeck(int seriesId, int userId); + Task ClearOnDeckRemoval(int seriesId, int userId); } public class SeriesRepository : ISeriesRepository @@ -757,16 +760,33 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter) { - var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30); - var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7); + var settings = await _context.ServerSetting + .Select(x => x) + .AsNoTracking() + .ToListAsync(); + var serverSettings = _mapper.Map(settings); + + var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckProgressDays); + var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckUpdateDays); var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); + // Don't allow any series the user has explicitly removed + var onDeckRemovals = _context.AppUserOnDeckRemoval + .Where(d => d.AppUserId == userId) + .Select(d => d.SeriesId) + .AsEnumerable(); + + // var onDeckRemovals = _context.AppUser.Where(u => u.Id == userId) + // .SelectMany(u => u.OnDeckRemovals.Select(d => d.Id)) + // .AsEnumerable(); + var query = _context.Series .Where(s => usersSeriesIds.Contains(s.Id)) + .Where(s => !onDeckRemovals.Contains(s.Id)) .Select(s => new { Series = s, @@ -1670,6 +1690,30 @@ public class SeriesRepository : ISeriesRepository return avg.HasValue ? (int) avg.Value : 0; } + public async Task RemoveFromOnDeck(int seriesId, int userId) + { + var existingEntry = await _context.AppUserOnDeckRemoval + .Where(u => u.Id == userId && u.SeriesId == seriesId) + .AnyAsync(); + if (existingEntry) return; + _context.AppUserOnDeckRemoval.Add(new AppUserOnDeckRemoval() + { + SeriesId = seriesId, + AppUserId = userId + }); + await _context.SaveChangesAsync(); + } + + public async Task ClearOnDeckRemoval(int seriesId, int userId) + { + var existingEntry = await _context.AppUserOnDeckRemoval + .Where(u => u.Id == userId && u.SeriesId == seriesId) + .FirstOrDefaultAsync(); + if (existingEntry == null) return; + _context.AppUserOnDeckRemoval.Remove(existingEntry); + await _context.SaveChangesAsync(); + } + public async Task IsSeriesInWantToRead(int userId, int seriesId) { var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 93c01d6ed..076f086cd 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -108,6 +108,8 @@ public static class Seed new() {Key = ServerSettingKey.HostName, Value = string.Empty}, new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()}, new() {Key = ServerSettingKey.LicenseKey, Value = string.Empty}, + new() {Key = ServerSettingKey.OnDeckProgressDays, Value = $"{30}"}, + new() {Key = ServerSettingKey.OnDeckUpdateDays, Value = $"{7}"}, new() { Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty }, // Not used from DB, but DB is sync with appSettings.json diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index bddf553ab..58f7d7033 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -37,6 +37,10 @@ public class AppUser : IdentityUser, IHasConcurrencyToken /// public ICollection Devices { get; set; } = null!; /// + /// A list of Series the user doesn't want on deck + /// + //public ICollection OnDeckRemovals { get; set; } = null!; + /// /// An API Key to interact with external services, like OPDS /// public string? ApiKey { get; set; } diff --git a/API/Entities/AppUserOnDeckRemoval.cs b/API/Entities/AppUserOnDeckRemoval.cs new file mode 100644 index 000000000..3b7b16f80 --- /dev/null +++ b/API/Entities/AppUserOnDeckRemoval.cs @@ -0,0 +1,11 @@ +namespace API.Entities; + +public class AppUserOnDeckRemoval +{ + public int Id { get; set; } + public int SeriesId { get; set; } + public Series Series { get; set; } + public int AppUserId { get; set; } + public AppUser AppUser { get; set; } + +} diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index 382367186..c8d9c12be 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -133,5 +133,15 @@ public enum ServerSettingKey /// [Description("Cache")] CacheSize = 24, + /// + /// How many Days since today in the past for reading progress, should content be considered for On Deck, before it gets removed automatically + /// + [Description("OnDeckProgressDays")] + OnDeckProgressDays = 25, + /// + /// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically + /// + [Description("OnDeckUpdateDays")] + OnDeckUpdateDays = 26, } diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 02bc70d06..9163a027f 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -73,6 +73,12 @@ public class ServerSettingConverter : ITypeConverter, case ServerSettingKey.CacheSize: destination.CacheSize = long.Parse(row.Value); break; + case ServerSettingKey.OnDeckProgressDays: + destination.OnDeckProgressDays = int.Parse(row.Value); + break; + case ServerSettingKey.OnDeckUpdateDays: + destination.OnDeckUpdateDays = int.Parse(row.Value); + break; } } diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index b320519fc..1f92f642b 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -262,6 +262,7 @@ public class ReaderService : IReaderService BookScrollId = progressDto.BookScrollId }); _unitOfWork.UserRepository.Update(userWithProgress); + BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(progressDto.SeriesId, userId)); } else { diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index e61f2b481..309bf43b8 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -88,6 +88,10 @@ export enum Action { * Import some data into Kavita */ Import = 18, + /** + * Removes the Series from On Deck inclusion + */ + RemoveFromOnDeck = 19, } export interface ActionItem { @@ -563,9 +567,7 @@ export class ActionFactoryService { // Checks the whole tree for the action and returns true if it exists public hasAction(actions: Array>, action: Action) { - var actionFound = false; - - if (actions.length === 0) return actionFound; + if (actions.length === 0) return false; for (let i = 0; i < actions.length; i++) { @@ -573,8 +575,7 @@ export class ActionFactoryService { if (this.hasAction(actions[i].children, action)) return true; } - - return actionFound; + return false; } } diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index b7359a8f6..edc83e520 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -226,4 +226,8 @@ export class SeriesService { getOverallRating(seriesId: number) { return this.httpClient.get(this.baseUrl + 'rating/overall?seriesId=' + seriesId); } + + removeFromOnDeck(seriesId: number) { + return this.httpClient.post(this.baseUrl + 'series/remove-from-on-deck?seriesId=' + seriesId, {}); + } } diff --git a/UI/Web/src/app/admin/_models/server-settings.ts b/UI/Web/src/app/admin/_models/server-settings.ts index 60147c966..b72396a8c 100644 --- a/UI/Web/src/app/admin/_models/server-settings.ts +++ b/UI/Web/src/app/admin/_models/server-settings.ts @@ -18,4 +18,6 @@ export interface ServerSettings { enableFolderWatching: boolean; hostName: string; cacheSize: number; + onDeckProgressDays: number; + onDeckUpdateDays: number; } 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 36bdd6dd2..925a806a1 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 @@ -114,6 +114,38 @@

+
+   + The number of days since last progress before kicking something off On Deck. + The number of days since last progress before kicking something off On Deck. + + +

+ Must be at least 1 day +

+

+ This field is required +

+
+
+
+   + The number of days since last chapter was added to include something On Deck. + The number of days since last chapter was added to include something On Deck. + + +

+ Must be at least 1 day +

+

+ This field is required +

+
+
@@ -125,7 +157,6 @@
-

OPDS support will allow all users to use OPDS to read and download content from the server.

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 47a889ced..72bddde99 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 @@ -25,10 +25,6 @@ export class ManageSettingsComponent implements OnInit { taskFrequencies: Array = []; logLevels: Array = []; - get TagBadgeCursor() { - return TagBadgeCursor; - } - constructor(private settingsService: SettingsService, private toastr: ToastrService, private serverService: ServerService) { } @@ -57,6 +53,8 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required])); this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, [])); this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)])); + this.settingsForm.addControl('onDeckProgressDays', new FormControl(this.serverSettings.onDeckProgressDays, [Validators.required])); + this.settingsForm.addControl('onDeckUpdateDays', new FormControl(this.serverSettings.onDeckUpdateDays, [Validators.required])); this.serverService.getServerInfo().subscribe(info => { if (info.isDocker) { @@ -84,6 +82,8 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs); this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName); this.settingsForm.get('cacheSize')?.setValue(this.serverSettings.cacheSize); + this.settingsForm.get('onDeckProgressDays')?.setValue(this.serverSettings.onDeckProgressDays); + this.settingsForm.get('onDeckUpdateDays')?.setValue(this.serverSettings.onDeckUpdateDays); this.settingsForm.markAsPristine(); } diff --git a/UI/Web/src/app/cards/series-card/series-card.component.ts b/UI/Web/src/app/cards/series-card/series-card.component.ts index 41b9102a7..f7dac176d 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.ts +++ b/UI/Web/src/app/cards/series-card/series-card.component.ts @@ -8,16 +8,16 @@ import { OnInit, Output } from '@angular/core'; -import { Router } from '@angular/router'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ToastrService } from 'ngx-toastr'; -import { Series } from 'src/app/_models/series'; -import { ImageService } from 'src/app/_services/image.service'; -import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service'; -import { SeriesService } from 'src/app/_services/series.service'; -import { ActionService } from 'src/app/_services/action.service'; -import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-series-modal.component'; -import { RelationKind } from 'src/app/_models/series-detail/relation-kind'; +import {Router} from '@angular/router'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {ToastrService} from 'ngx-toastr'; +import {Series} from 'src/app/_models/series'; +import {ImageService} from 'src/app/_services/image.service'; +import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service'; +import {SeriesService} from 'src/app/_services/series.service'; +import {ActionService} from 'src/app/_services/action.service'; +import {EditSeriesModalComponent} from '../_modals/edit-series-modal/edit-series-modal.component'; +import {RelationKind} from 'src/app/_models/series-detail/relation-kind'; import {CommonModule} from "@angular/common"; import {CardItemComponent} from "../card-item/card-item.component"; import {RelationshipPipe} from "../../pipe/relationship.pipe"; @@ -47,6 +47,10 @@ export class SeriesCardComponent implements OnInit, OnChanges { * If the Series has a relationship to display */ @Input() relation: RelationKind | undefined = undefined; + /** + * When a series card is shown on deck, a special actionable is added to the list + */ + @Input() isOnDeck: boolean = false; @Output() clicked = new EventEmitter(); /** @@ -79,6 +83,19 @@ export class SeriesCardComponent implements OnInit, OnChanges { ngOnChanges(changes: any) { if (this.data) { this.actions = this.actionFactoryService.getSeriesActions((action: ActionItem, series: Series) => this.handleSeriesActionCallback(action, series)); + if (this.isOnDeck) { + const othersIndex = this.actions.findIndex(obj => obj.title === 'Others'); + if (this.actions[othersIndex].children.findIndex(o => o.action === Action.RemoveFromOnDeck) < 0) { + this.actions[othersIndex].children.push({ + action: Action.RemoveFromOnDeck, + title: 'Remove From On Deck', + callback: (action: ActionItem, series: Series) => this.handleSeriesActionCallback(action, series), + class: 'danger', + requiresAdmin: false, + children: [], + }); + } + } this.cdRef.markForCheck(); } } @@ -125,6 +142,9 @@ export class SeriesCardComponent implements OnInit, OnChanges { const device = (action._extra!.data as Device); this.actionService.sendSeriesToDevice(series.id, device); break; + case Action.RemoveFromOnDeck: + this.seriesService.removeFromOnDeck(series.id).subscribe(() => this.reload.emit(series.id)); + break; default: break; } diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.html b/UI/Web/src/app/dashboard/_components/dashboard.component.html index def199198..cfb2c2da5 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.html +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.html @@ -15,7 +15,8 @@ - + diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.ts b/UI/Web/src/app/dashboard/_components/dashboard.component.ts index 4bd6b58c0..e2802089f 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.ts @@ -120,15 +120,17 @@ export class DashboardComponent implements OnInit { this.loadRecentlyAddedSeries(); } - reloadInProgress(series: Series | boolean) { - if (series === true || series === false) { - if (!series) {return;} - } - // If the update to Series doesn't affect the requirement to be in this stream, then ignore update request - const seriesObj = (series as Series); - if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) { - return; - } + reloadInProgress(series: Series | number) { + // if (typeof series === 'number') { + // this.loadOnDeck(); + // return; + // } + // + // // If the update to Series doesn't affect the requirement to be in this stream, then ignore update request + // const seriesObj = (series as Series); + // if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) { + // return; + // } this.loadOnDeck(); } diff --git a/openapi.json b/openapi.json index d5a266433..8d5b221b0 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.4.2" + "version": "0.7.4.3" }, "servers": [ { @@ -7749,6 +7749,30 @@ } } }, + "/api/Series/remove-from-on-deck": { + "post": { + "tags": [ + "Series" + ], + "summary": "Removes a series from displaying on deck until the next read event on that series", + "parameters": [ + { + "name": "seriesId", + "in": "query", + "description": "", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/api/Series/refresh-metadata": { "post": { "tags": [ @@ -15877,6 +15901,16 @@ "type": "integer", "description": "The size in MB for Caching API data", "format": "int64" + }, + "onDeckProgressDays": { + "type": "integer", + "description": "How many Days since today in the past for reading progress, should content be considered for On Deck, before it gets removed automatically", + "format": "int32" + }, + "onDeckUpdateDays": { + "type": "integer", + "description": "How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically", + "format": "int32" } }, "additionalProperties": false