diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index 794b6b1ec..9e0135612 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -7,10 +7,10 @@
-
+
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/API.Tests/Comparers/NaturalSortComparerTest.cs b/API.Tests/Comparers/NaturalSortComparerTest.cs
index 5b06c0a2d..477ba867f 100644
--- a/API.Tests/Comparers/NaturalSortComparerTest.cs
+++ b/API.Tests/Comparers/NaturalSortComparerTest.cs
@@ -54,6 +54,18 @@ namespace API.Tests.Comparers
new[] {"!001", "001", "002"},
new[] {"!001", "001", "002"}
)]
+ [InlineData(
+ new[] {"001", "", null},
+ new[] {"", "001", null}
+ )]
+ [InlineData(
+ new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.020/001.jpg"},
+ new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.020/001.jpg"}
+ )]
+ [InlineData(
+ new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"},
+ new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"}
+ )]
public void TestNaturalSortComparer(string[] input, string[] expected)
{
Array.Sort(input, _nc);
@@ -94,6 +106,10 @@ namespace API.Tests.Comparers
new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"},
new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"}
)]
+ [InlineData(
+ new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg"},
+ new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg"}
+ )]
public void TestNaturalSortComparerLinq(string[] input, string[] expected)
{
var output = input.OrderBy(c => c, _nc);
diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs
index d088901e5..1eb2a32d6 100644
--- a/API.Tests/Parser/MangaParserTests.cs
+++ b/API.Tests/Parser/MangaParserTests.cs
@@ -244,6 +244,7 @@ namespace API.Tests.Parser
[InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")]
[InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", "0")]
[InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")]
+ [InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")]
public void ParseChaptersTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
diff --git a/API/API.csproj b/API/API.csproj
index 88b786b79..05c3fa130 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -47,29 +47,29 @@
-
-
-
+
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/API/Comparators/NaturalSortComparer.cs b/API/Comparators/NaturalSortComparer.cs
index 24b0c20d2..650edb7ce 100644
--- a/API/Comparators/NaturalSortComparer.cs
+++ b/API/Comparators/NaturalSortComparer.cs
@@ -27,6 +27,10 @@ namespace API.Comparators
{
if (x == y) return 0;
+ if (x != null && y == null) return -1;
+ if (x == null) return 1;
+
+
if (!_table.TryGetValue(x ?? Empty, out var x1))
{
x1 = Regex.Split(x ?? Empty, "([0-9]+)");
diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs
new file mode 100644
index 000000000..b0fcd9930
--- /dev/null
+++ b/API/Controllers/MetadataController.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using API.Data;
+using API.DTOs;
+using API.DTOs.Metadata;
+using Microsoft.AspNetCore.Mvc;
+
+namespace API.Controllers;
+
+
+public class MetadataController : BaseApiController
+{
+ private readonly IUnitOfWork _unitOfWork;
+
+ public MetadataController(IUnitOfWork unitOfWork)
+ {
+ _unitOfWork = unitOfWork;
+ }
+
+ [HttpGet("genres")]
+ public async Task>> GetAllGenres()
+ {
+ return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync());
+ }
+
+ [HttpGet("people")]
+ public async Task>> GetAllPeople()
+ {
+ return Ok(await _unitOfWork.PersonRepository.GetAllPeople());
+ }
+}
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index d4abe7ba3..bf7de16e7 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -334,7 +334,7 @@ namespace API.Controllers
var existingTag = allTags.SingleOrDefault(t => t.Title == tag.Title);
if (existingTag != null)
{
- if (!series.Metadata.CollectionTags.Any(t => t.Title == tag.Title))
+ if (series.Metadata.CollectionTags.All(t => t.Title != tag.Title))
{
newTags.Add(existingTag);
}
diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs
index 6e52ef45c..a745b9f0e 100644
--- a/API/DTOs/Filtering/FilterDto.cs
+++ b/API/DTOs/Filtering/FilterDto.cs
@@ -1,5 +1,7 @@
using System.Collections;
using System.Collections.Generic;
+using API.Data.Migrations;
+using API.Entities;
using API.Entities.Enums;
namespace API.DTOs.Filtering
@@ -11,5 +13,64 @@ namespace API.DTOs.Filtering
///
public IList Formats { get; init; } = new List();
+ ///
+ /// The progress you want to be returned. This can be bitwise manipulated. Defaults to all applicable states.
+ ///
+ public ReadStatus ReadStatus { get; init; } = new ReadStatus();
+
+ ///
+ /// A list of library ids to restrict search to. Defaults to all libraries by passing empty list
+ ///
+ public IList Libraries { get; init; } = new List();
+ ///
+ /// A list of Genre ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Genres { get; init; } = new List();
+ ///
+ /// A list of Writers to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Writers { get; init; } = new List();
+ ///
+ /// A list of Penciller ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Penciller { get; init; } = new List();
+ ///
+ /// A list of Inker ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Inker { get; init; } = new List();
+ ///
+ /// A list of Colorist ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Colorist { get; init; } = new List();
+ ///
+ /// A list of Letterer ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Letterer { get; init; } = new List();
+ ///
+ /// A list of CoverArtist ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList CoverArtist { get; init; } = new List();
+ ///
+ /// A list of Editor ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Editor { get; init; } = new List();
+ ///
+ /// A list of Publisher ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Publisher { get; init; } = new List();
+ ///
+ /// A list of Character ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList Character { get; init; } = new List();
+ ///
+ /// A list of Collection Tag ids to restrict search to. Defaults to all genres by passing an empty list
+ ///
+ public IList CollectionTags { get; init; } = new List();
+ ///
+ /// Will return back everything with the rating and above
+ ///
+ ///
+ public int Rating { get; init; }
+
}
}
diff --git a/API/DTOs/Filtering/ReadStatus.cs b/API/DTOs/Filtering/ReadStatus.cs
new file mode 100644
index 000000000..af7bb2475
--- /dev/null
+++ b/API/DTOs/Filtering/ReadStatus.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace API.DTOs.Filtering;
+
+///
+/// Represents the Reading Status. This is a flag and allows multiple statues
+///
+public class ReadStatus
+{
+ public bool NotRead { get; set; } = false;
+ public bool InProgress { get; set; } = false;
+ public bool Read { get; set; } = false;
+}
diff --git a/API/DTOs/PersonDto.cs b/API/DTOs/PersonDto.cs
index 646817c1d..0ab7a4076 100644
--- a/API/DTOs/PersonDto.cs
+++ b/API/DTOs/PersonDto.cs
@@ -4,7 +4,8 @@ namespace API.DTOs
{
public class PersonDto
{
+ public int Id { get; set; }
public string Name { get; set; }
public PersonRole Role { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs b/API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs
new file mode 100644
index 000000000..64a21a956
--- /dev/null
+++ b/API/Data/Migrations/20211214000230_SeriesIncludes.Designer.cs
@@ -0,0 +1,1228 @@
+//
+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("20211214000230_SeriesIncludes")]
+ partial class SeriesIncludes
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "6.0.0");
+
+ 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("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .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("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("BookReaderDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("SiteDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ 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("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("GenreId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TitleName")
+ .HasColumnType("TEXT");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GenreId");
+
+ 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.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("FilePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ 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("ReleaseYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId")
+ .IsUnique();
+
+ b.HasIndex("Id", "SeriesId")
+ .IsUnique();
+
+ b.ToTable("SeriesMetadata");
+ });
+
+ 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("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .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("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LocalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalName")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
+ .IsUnique();
+
+ 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.Volume", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("INTEGER");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .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("ChapterPerson", b =>
+ {
+ b.Property("ChapterMetadatasId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PeopleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ChapterMetadatasId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.ToTable("ChapterPerson");
+ });
+
+ 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("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.Navigation("AppUser");
+ });
+
+ 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.Genre", null)
+ .WithMany("Chapters")
+ .HasForeignKey("GenreId");
+
+ b.HasOne("API.Entities.Volume", "Volume")
+ .WithMany("Chapters")
+ .HasForeignKey("VolumeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Volume");
+ });
+
+ 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.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.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("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("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("API.Entities.AppRole", b =>
+ {
+ b.Navigation("UserRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Navigation("Bookmarks");
+
+ b.Navigation("Progresses");
+
+ b.Navigation("Ratings");
+
+ b.Navigation("ReadingLists");
+
+ b.Navigation("UserPreferences");
+
+ b.Navigation("UserRoles");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Navigation("Files");
+ });
+
+ modelBuilder.Entity("API.Entities.Genre", b =>
+ {
+ b.Navigation("Chapters");
+ });
+
+ 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("Volumes");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Navigation("Chapters");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/API/Data/Migrations/20211214000230_SeriesIncludes.cs b/API/Data/Migrations/20211214000230_SeriesIncludes.cs
new file mode 100644
index 000000000..092d9001d
--- /dev/null
+++ b/API/Data/Migrations/20211214000230_SeriesIncludes.cs
@@ -0,0 +1,57 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace API.Data.Migrations
+{
+ public partial class SeriesIncludes : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_AppUserRating_SeriesId",
+ table: "AppUserRating",
+ column: "SeriesId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AppUserProgresses_SeriesId",
+ table: "AppUserProgresses",
+ column: "SeriesId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_AppUserProgresses_Series_SeriesId",
+ table: "AppUserProgresses",
+ column: "SeriesId",
+ principalTable: "Series",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_AppUserRating_Series_SeriesId",
+ table: "AppUserRating",
+ column: "SeriesId",
+ principalTable: "Series",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_AppUserProgresses_Series_SeriesId",
+ table: "AppUserProgresses");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_AppUserRating_Series_SeriesId",
+ table: "AppUserRating");
+
+ migrationBuilder.DropIndex(
+ name: "IX_AppUserRating_SeriesId",
+ table: "AppUserRating");
+
+ migrationBuilder.DropIndex(
+ name: "IX_AppUserProgresses_SeriesId",
+ table: "AppUserProgresses");
+ }
+ }
+}
diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs
index 6abe85571..b20f13f6d 100644
--- a/API/Data/Migrations/DataContextModelSnapshot.cs
+++ b/API/Data/Migrations/DataContextModelSnapshot.cs
@@ -240,6 +240,8 @@ namespace API.Data.Migrations
b.HasIndex("AppUserId");
+ b.HasIndex("SeriesId");
+
b.ToTable("AppUserProgresses");
});
@@ -265,6 +267,8 @@ namespace API.Data.Migrations
b.HasIndex("AppUserId");
+ b.HasIndex("SeriesId");
+
b.ToTable("AppUserRating");
});
@@ -887,6 +891,12 @@ namespace API.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
+ b.HasOne("API.Entities.Series", null)
+ .WithMany("Progress")
+ .HasForeignKey("SeriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
b.Navigation("AppUser");
});
@@ -898,6 +908,12 @@ namespace API.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
+ b.HasOne("API.Entities.Series", null)
+ .WithMany("Ratings")
+ .HasForeignKey("SeriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
b.Navigation("AppUser");
});
@@ -1193,6 +1209,10 @@ namespace API.Data.Migrations
{
b.Navigation("Metadata");
+ b.Navigation("Progress");
+
+ b.Navigation("Ratings");
+
b.Navigation("Volumes");
});
diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs
index 271524994..e1d53934c 100644
--- a/API/Data/Repositories/GenreRepository.cs
+++ b/API/Data/Repositories/GenreRepository.cs
@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using API.DTOs.Metadata;
using API.Entities;
using AutoMapper;
+using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
@@ -12,7 +14,8 @@ public interface IGenreRepository
void Attach(Genre genre);
void Remove(Genre genre);
Task FindByNameAsync(string genreName);
- Task> GetAllGenres();
+ Task> GetAllGenresAsync();
+ Task> GetAllGenreDtosAsync();
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
}
@@ -57,8 +60,15 @@ public class GenreRepository : IGenreRepository
await _context.SaveChangesAsync();
}
- public async Task> GetAllGenres()
+ public async Task> GetAllGenresAsync()
{
return await _context.Genre.ToListAsync();
}
+
+ public async Task> GetAllGenreDtosAsync()
+ {
+ return await _context.Genre
+ .ProjectTo(_mapper.ConfigurationProvider)
+ .ToListAsync();
+ }
}
diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs
index 8955635b0..0c55a7b49 100644
--- a/API/Data/Repositories/SeriesRepository.cs
+++ b/API/Data/Repositories/SeriesRepository.cs
@@ -177,17 +177,15 @@ public class SeriesRepository : ISeriesRepository
public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
{
- var formats = filter.GetSqlFilter();
+ var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
- var userLibraries = await GetUserLibraries(libraryId, userId);
-
- var query = _context.Series
- .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format))
- .OrderBy(s => s.SortName)
+ var retSeries = query
+ .OrderByDescending(s => s.SortName)
.ProjectTo(_mapper.ConfigurationProvider)
+ .AsSplitQuery()
.AsNoTracking();
- return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
+ return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
}
private async Task> GetUserLibraries(int libraryId, int userId)
@@ -247,7 +245,7 @@ public class SeriesRepository : ISeriesRepository
public async Task DeleteSeriesAsync(int seriesId)
{
var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync();
- _context.Series.Remove(series);
+ if (series != null) _context.Series.Remove(series);
return await _context.SaveChangesAsync() > 0;
}
@@ -376,18 +374,84 @@ public class SeriesRepository : ISeriesRepository
///
public async Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter)
{
- var formats = filter.GetSqlFilter();
+ var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
- var userLibraries = await GetUserLibraries(libraryId, userId);
-
- var query = _context.Series
- .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format))
+ var retSeries = query
.OrderByDescending(s => s.Created)
.ProjectTo(_mapper.ConfigurationProvider)
.AsSplitQuery()
.AsNoTracking();
- return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
+ return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize);
+ }
+
+ private IList ExtractFilters(int libraryId, int userId, FilterDto filter, ref List userLibraries,
+ out List allPeopleIds, out bool hasPeopleFilter, out bool hasGenresFilter, out bool hasCollectionTagFilter,
+ out bool hasRatingFilter, out bool hasProgressFilter, out IList seriesIds)
+ {
+ var formats = filter.GetSqlFilter();
+
+ if (filter.Libraries.Count > 0)
+ {
+ userLibraries = userLibraries.Where(l => filter.Libraries.Contains(l)).ToList();
+ }
+
+ allPeopleIds = new List();
+ allPeopleIds.AddRange(filter.Writers);
+ allPeopleIds.AddRange(filter.Character);
+ allPeopleIds.AddRange(filter.Colorist);
+ allPeopleIds.AddRange(filter.Editor);
+ allPeopleIds.AddRange(filter.Inker);
+ allPeopleIds.AddRange(filter.Letterer);
+ allPeopleIds.AddRange(filter.Penciller);
+ allPeopleIds.AddRange(filter.Publisher);
+ allPeopleIds.AddRange(filter.CoverArtist);
+
+ hasPeopleFilter = allPeopleIds.Count > 0;
+ hasGenresFilter = filter.Genres.Count > 0;
+ hasCollectionTagFilter = filter.CollectionTags.Count > 0;
+ hasRatingFilter = filter.Rating > 0;
+ hasProgressFilter = !filter.ReadStatus.Read || !filter.ReadStatus.InProgress || !filter.ReadStatus.NotRead;
+
+
+ bool ProgressComparison(int pagesRead, int totalPages)
+ {
+ var result = false;
+ if (filter.ReadStatus.NotRead)
+ {
+ result = (pagesRead == 0);
+ }
+
+ if (filter.ReadStatus.Read)
+ {
+ result = result || (pagesRead == totalPages);
+ }
+
+ if (filter.ReadStatus.InProgress)
+ {
+ result = result || (pagesRead > 0 && pagesRead < totalPages);
+ }
+
+ return result;
+ }
+
+ seriesIds = new List();
+ if (hasProgressFilter)
+ {
+ seriesIds = _context.Series
+ .Include(s => s.Progress)
+ .Select(s => new
+ {
+ Series = s,
+ PagesRead = s.Progress.Where(p => p.AppUserId == userId).Sum(p => p.PagesRead),
+ })
+ .ToList()
+ .Where(s => ProgressComparison(s.PagesRead, s.Series.Pages))
+ .Select(s => s.Series.Id)
+ .ToList();
+ }
+
+ return formats;
}
///
@@ -401,24 +465,23 @@ public class SeriesRepository : ISeriesRepository
///
public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter)
{
- var formats = filter.GetSqlFilter();
+ var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
+ .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
+ new
+ {
+ Series = s,
+ PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId)
+ .Sum(s1 => s1.PagesRead),
+ progress.AppUserId,
+ LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId)
+ .Max(p => p.LastModified)
+ });
- var userLibraries = await GetUserLibraries(libraryId, userId);
- var series = _context.Series
- .Where(s => formats.Contains(s.Format) && userLibraries.Contains(s.LibraryId))
- .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) => new
- {
- Series = s,
- PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId).Sum(s1 => s1.PagesRead),
- progress.AppUserId,
- LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId).Max(p => p.LastModified)
- })
- .AsNoTracking();
- var retSeries = series.Where(s => s.AppUserId == userId
- && s.PagesRead > 0
- && s.PagesRead < s.Series.Pages)
+ var retSeries = query.Where(s => s.AppUserId == userId
+ && s.PagesRead > 0
+ && s.PagesRead < s.Series.Pages)
.OrderByDescending(s => s.LastModified)
.ThenByDescending(s => s.Series.LastModified)
.Select(s => s.Series)
@@ -430,6 +493,63 @@ public class SeriesRepository : ISeriesRepository
return await retSeries.ToListAsync();
}
+ private async Task> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter)
+ {
+ var userLibraries = await GetUserLibraries(libraryId, userId);
+ var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries,
+ out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter,
+ out var hasCollectionTagFilter, out var hasRatingFilter, out var hasProgressFilter,
+ out var seriesIds);
+
+ var query = _context.Series
+ .Where(s => userLibraries.Contains(s.LibraryId)
+ && formats.Contains(s.Format)
+ && (!hasGenresFilter || s.Metadata.Genres.Any(g => filter.Genres.Contains(g.Id)))
+ && (!hasPeopleFilter || s.Metadata.People.Any(p => allPeopleIds.Contains(p.Id)))
+ && (!hasCollectionTagFilter ||
+ s.Metadata.CollectionTags.Any(t => filter.CollectionTags.Contains(t.Id)))
+ && (!hasRatingFilter || s.Ratings.Any(r => r.Rating >= filter.Rating))
+ && (!hasProgressFilter || seriesIds.Contains(s.Id))
+ )
+ .AsNoTracking();
+ // IQueryable newFilter = null;
+ // if (hasProgressFilter)
+ // {
+ // newFilter = query
+ // .Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
+ // new
+ // {
+ // Series = s,
+ // PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId)
+ // .Sum(s1 => s1.PagesRead),
+ // progress.AppUserId,
+ // LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId)
+ // .Max(p => p.LastModified)
+ // })
+ // .Select(d => new FilterableQuery()
+ // {
+ // Series = d.Series,
+ // AppUserId = d.AppUserId,
+ // LastModified = d.LastModified,
+ // PagesRead = d.PagesRead
+ // })
+ // .Where(d => seriesIds.Contains(d.Series.Id));
+ // }
+ // else
+ // {
+ // newFilter = query.Select(s => new FilterableQuery()
+ // {
+ // Series = s,
+ // LastModified = DateTime.Now, // TODO: Figure this out
+ // AppUserId = userId,
+ // PagesRead = 0
+ // });
+ // }
+
+
+ return query;
+ }
+
public async Task GetSeriesMetadata(int seriesId)
{
var metadataDto = await _context.SeriesMetadata
diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs
index ed2fd64e2..a532028bb 100644
--- a/API/Entities/Series.cs
+++ b/API/Entities/Series.cs
@@ -53,6 +53,8 @@ namespace API.Entities
public MangaFormat Format { get; set; } = MangaFormat.Unknown;
public SeriesMetadata Metadata { get; set; }
+ public ICollection Ratings { get; set; } = new List();
+ public ICollection Progress { get; set; } = new List();
// Relationships
public List Volumes { get; set; }
diff --git a/API/Helpers/SQLHelper.cs b/API/Helpers/SQLHelper.cs
index fcd44e7da..d06d246ef 100644
--- a/API/Helpers/SQLHelper.cs
+++ b/API/Helpers/SQLHelper.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
+using API.DTOs;
using Microsoft.EntityFrameworkCore;
namespace API.Helpers
diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs
index 5ebac08cf..fb50cb880 100644
--- a/API/Parser/Parser.cs
+++ b/API/Parser/Parser.cs
@@ -412,7 +412,7 @@ namespace API.Parser
MatchOptions, RegexTimeout),
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz
new Regex(
- @"^(?!Vol)(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
+ @"^(?!Vol)(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
MatchOptions, RegexTimeout),
// Tower Of God S01 014 (CBT) (digital).cbz
new Regex(
diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs
index eff48bb72..87e90bd36 100644
--- a/API/Services/MetadataService.cs
+++ b/API/Services/MetadataService.cs
@@ -369,7 +369,7 @@ public class MetadataService : IMetadataService
_logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count);
var allPeople = await _unitOfWork.PersonRepository.GetAllPeople();
- var allGenres = await _unitOfWork.GenreRepository.GetAllGenres();
+ var allGenres = await _unitOfWork.GenreRepository.GetAllGenresAsync();
var seriesIndex = 0;
@@ -489,7 +489,7 @@ public class MetadataService : IMetadataService
MessageFactory.RefreshMetadataProgressEvent(libraryId, 0F));
var allPeople = await _unitOfWork.PersonRepository.GetAllPeople();
- var allGenres = await _unitOfWork.GenreRepository.GetAllGenres();
+ var allGenres = await _unitOfWork.GenreRepository.GetAllGenresAsync();
ProcessSeriesMetadataUpdate(series, allPeople, allGenres, forceUpdate);
diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs
index 50f966db6..c4f4659e8 100644
--- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs
+++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs
@@ -128,8 +128,6 @@ namespace API.Services.Tasks.Scanner
{
info.Chapters = info.ComicInfo.Number;
}
-
- _logger.LogDebug("ComicInfo read added {Time} ms to processing", sw.ElapsedMilliseconds);
}
TrackSeries(info);
diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj
index 7e20e9d3d..abf131604 100644
--- a/Kavita.Common/Kavita.Common.csproj
+++ b/Kavita.Common/Kavita.Common.csproj
@@ -11,7 +11,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/UI/Web/src/app/_models/person.ts b/UI/Web/src/app/_models/person.ts
index c0ecc7c4e..88b0848fa 100644
--- a/UI/Web/src/app/_models/person.ts
+++ b/UI/Web/src/app/_models/person.ts
@@ -13,6 +13,7 @@ export enum PersonRole {
}
export interface Person {
+ id: number;
name: string;
role: PersonRole;
}
\ No newline at end of file
diff --git a/UI/Web/src/app/_models/series-filter.ts b/UI/Web/src/app/_models/series-filter.ts
index f4eef7b1c..39cd48ab0 100644
--- a/UI/Web/src/app/_models/series-filter.ts
+++ b/UI/Web/src/app/_models/series-filter.ts
@@ -1,38 +1,53 @@
import { MangaFormat } from "./manga-format";
-export interface FilterItem {
+export interface FilterItem {
title: string;
- value: any;
+ value: T;
selected: boolean;
}
export interface SeriesFilter {
formats: Array;
+ libraries: Array,
+ readStatus: ReadStatus;
+ genres: Array