diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 805c3b61d..cdd72de1c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -28,7 +28,7 @@ body: label: Kavita Version Number - If you don't see your version number listed, please update Kavita and see if your issue still persists. multiple: false options: - - 0.8.7 - Stable + - 0.8.6.2 - Stable - Nightly Testing Branch validations: required: true diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index ec9c1884f..38ec425fe 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -10,8 +10,8 @@ - - + + @@ -26,10 +26,5 @@ Always - - - PreserveNewest - - diff --git a/API.Benchmark/Data/AesopsFables.epub b/API.Benchmark/Data/AesopsFables.epub deleted file mode 100644 index d2ab9a8b2..000000000 Binary files a/API.Benchmark/Data/AesopsFables.epub and /dev/null differ diff --git a/API.Benchmark/KoreaderHashBenchmark.cs b/API.Benchmark/KoreaderHashBenchmark.cs deleted file mode 100644 index c0abfd2ad..000000000 --- a/API.Benchmark/KoreaderHashBenchmark.cs +++ /dev/null @@ -1,41 +0,0 @@ -using API.Helpers.Builders; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Order; -using System; -using API.Entities.Enums; - -namespace API.Benchmark -{ - [StopOnFirstError] - [MemoryDiagnoser] - [RankColumn] - [Orderer(SummaryOrderPolicy.FastestToSlowest)] - [SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)] - public class KoreaderHashBenchmark - { - private const string sourceEpub = "./Data/AesopsFables.epub"; - - [Benchmark(Baseline = true)] - public void TestBuildManga_baseline() - { - var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub) - .Build(); - if (file == null) - { - throw new Exception("Failed to build manga file"); - } - } - - [Benchmark] - public void TestBuildManga_withHash() - { - var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub) - .WithHash() - .Build(); - if (file == null) - { - throw new Exception("Failed to build manga file"); - } - } - } -} diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index a571a6e72..20e10e548 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -6,13 +6,13 @@ - - + + - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -36,10 +36,4 @@ - - - PreserveNewest - - - diff --git a/API.Tests/Data/AesopsFables.epub b/API.Tests/Data/AesopsFables.epub deleted file mode 100644 index d2ab9a8b2..000000000 Binary files a/API.Tests/Data/AesopsFables.epub and /dev/null differ diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index 96d74b46d..866e0202c 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -67,7 +67,7 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 2)] + [InlineData(false, 1)] public void RestrictAgainstAgeRestriction_Genre_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { var items = new List() @@ -94,7 +94,7 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 2)] + [InlineData(false, 1)] public void RestrictAgainstAgeRestriction_Tag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { var items = new List() diff --git a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs b/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs deleted file mode 100644 index e1f585806..000000000 --- a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using API.Helpers; -using Xunit; - -namespace API.Tests.Helpers; - -public class BookSortTitlePrefixHelperTests -{ - [Theory] - [InlineData("The Avengers", "Avengers")] - [InlineData("A Game of Thrones", "Game of Thrones")] - [InlineData("An American Tragedy", "American Tragedy")] - public void TestEnglishPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("El Quijote", "Quijote")] - [InlineData("La Casa de Papel", "Casa de Papel")] - [InlineData("Los Miserables", "Miserables")] - [InlineData("Las Vegas", "Vegas")] - [InlineData("Un Mundo Feliz", "Mundo Feliz")] - [InlineData("Una Historia", "Historia")] - public void TestSpanishPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Le Petit Prince", "Petit Prince")] - [InlineData("La Belle et la Bête", "Belle et la Bête")] - [InlineData("Les Misérables", "Misérables")] - [InlineData("Un Amour de Swann", "Amour de Swann")] - [InlineData("Une Vie", "Vie")] - [InlineData("Des Souris et des Hommes", "Souris et des Hommes")] - public void TestFrenchPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Der Herr der Ringe", "Herr der Ringe")] - [InlineData("Die Verwandlung", "Verwandlung")] - [InlineData("Das Kapital", "Kapital")] - [InlineData("Ein Sommernachtstraum", "Sommernachtstraum")] - [InlineData("Eine Geschichte", "Geschichte")] - public void TestGermanPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Il Nome della Rosa", "Nome della Rosa")] - [InlineData("La Divina Commedia", "Divina Commedia")] - [InlineData("Lo Hobbit", "Hobbit")] - [InlineData("Gli Ultimi", "Ultimi")] - [InlineData("Le Città Invisibili", "Città Invisibili")] - [InlineData("Un Giorno", "Giorno")] - [InlineData("Una Notte", "Notte")] - public void TestItalianPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("O Alquimista", "Alquimista")] - [InlineData("A Moreninha", "Moreninha")] - [InlineData("Os Lusíadas", "Lusíadas")] - [InlineData("As Meninas", "Meninas")] - [InlineData("Um Defeito de Cor", "Defeito de Cor")] - [InlineData("Uma História", "História")] - public void TestPortuguesePrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("", "")] // Empty string returns empty - [InlineData("Book", "Book")] // Single word, no change - [InlineData("Avengers", "Avengers")] // No prefix, no change - public void TestNoPrefixCases(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The", "The")] // Just a prefix word alone - [InlineData("A", "A")] // Just single letter prefix alone - [InlineData("Le", "Le")] // French prefix alone - public void TestPrefixWordAlone(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("THE AVENGERS", "AVENGERS")] // All caps - [InlineData("the avengers", "avengers")] // All lowercase - [InlineData("The AVENGERS", "AVENGERS")] // Mixed case - [InlineData("tHe AvEnGeRs", "AvEnGeRs")] // Random case - public void TestCaseInsensitivity(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("Then Came You", "Then Came You")] // "The" + "n" = not a prefix - [InlineData("And Then There Were None", "And Then There Were None")] // "An" + "d" = not a prefix - [InlineData("Elsewhere", "Elsewhere")] // "El" + "sewhere" = not a prefix (no space) - [InlineData("Lesson Plans", "Lesson Plans")] // "Les" + "son" = not a prefix (no space) - [InlineData("Theory of Everything", "Theory of Everything")] // "The" + "ory" = not a prefix - public void TestFalsePositivePrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The ", "The ")] // Prefix with only space after - returns original - [InlineData("La ", "La ")] // Same for other languages - [InlineData("El ", "El ")] // Same for Spanish - public void TestPrefixWithOnlySpaceAfter(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The Multiple Spaces", " Multiple Spaces")] // Doesn't trim extra spaces from remainder - [InlineData("Le Petit Prince", " Petit Prince")] // Leading space preserved in remainder - public void TestSpaceHandling(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("The The Matrix", "The Matrix")] // Removes first "The", leaves second - [InlineData("A A Clockwork Orange", "A Clockwork Orange")] // Removes first "A", leaves second - [InlineData("El El Cid", "El Cid")] // Spanish version - public void TestRepeatedPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("L'Étranger", "L'Étranger")] // French contraction - no space, no change - [InlineData("D'Artagnan", "D'Artagnan")] // Contraction - no space, no change - [InlineData("The-Matrix", "The-Matrix")] // Hyphen instead of space - no change - [InlineData("The.Avengers", "The.Avengers")] // Period instead of space - no change - public void TestNonSpaceSeparators(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("三国演义", "三国演义")] // Chinese - no processing due to CJK detection - [InlineData("한국어", "한국어")] // Korean - not in CJK range, would be processed normally - public void TestCjkLanguages(string inputString, string expected) - { - // NOTE: These don't do anything, I am waiting for user input on if these are needed - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("नमस्ते दुनिया", "नमस्ते दुनिया")] // Hindi - not CJK, processed normally - [InlineData("مرحبا بالعالم", "مرحبا بالعالم")] // Arabic - not CJK, processed normally - [InlineData("שלום עולם", "שלום עולם")] // Hebrew - not CJK, processed normally - public void TestNonLatinNonCjkScripts(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } - - [Theory] - [InlineData("в мире", "мире")] // Russian "в" (in) - should be removed - [InlineData("на столе", "столе")] // Russian "на" (on) - should be removed - [InlineData("с друзьями", "друзьями")] // Russian "с" (with) - should be removed - public void TestRussianPrefixes(string inputString, string expected) - { - Assert.Equal(expected, BookSortTitlePrefixHelper.GetSortTitle(inputString)); - } -} diff --git a/API.Tests/Helpers/KoreaderHelperTests.cs b/API.Tests/Helpers/KoreaderHelperTests.cs deleted file mode 100644 index 66d287a5d..000000000 --- a/API.Tests/Helpers/KoreaderHelperTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using API.DTOs.Koreader; -using API.DTOs.Progress; -using API.Helpers; -using System.Runtime.CompilerServices; -using Xunit; - -namespace API.Tests.Helpers; - - -public class KoreaderHelperTests -{ - - [Theory] - [InlineData("/body/DocFragment[11]/body/div/a", 10, null)] - [InlineData("/body/DocFragment[1]/body/div/p[40]", 0, 40)] - [InlineData("/body/DocFragment[8]/body/div/p[28]/text().264", 7, 28)] - public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber) - { - var expected = EmptyProgressDto(); - expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[{pNumber}]" : null; - expected.PageNum = page; - var actual = EmptyProgressDto(); - - KoreaderHelper.UpdateProgressDto(actual, koreaderPosition); - Assert.Equal(expected.BookScrollId, actual.BookScrollId); - Assert.Equal(expected.PageNum, actual.PageNum); - } - - - [Theory] - [InlineData("//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[20]", 5, "/body/DocFragment[6]/body/div/p[20]")] - [InlineData(null, 10, "/body/DocFragment[11]/body/div/a")] - public void GetKoreaderPosition(string scrollId, int page, string koreaderPosition) - { - var given = EmptyProgressDto(); - given.BookScrollId = scrollId; - given.PageNum = page; - - Assert.Equal(koreaderPosition, KoreaderHelper.GetKoreaderPosition(given)); - } - - [Theory] - [InlineData("./Data/AesopsFables.epub", "8795ACA4BF264B57C1EEDF06A0CEE688")] - public void GetKoreaderHash(string filePath, string hash) - { - Assert.Equal(KoreaderHelper.HashContents(filePath), hash); - } - - private ProgressDto EmptyProgressDto() - { - return new ProgressDto - { - ChapterId = 0, - PageNum = 0, - VolumeId = 0, - SeriesId = 0, - LibraryId = 0 - }; - } -} diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/API.Tests/Helpers/PersonHelperTests.cs index 47dab48da..66713e17c 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/API.Tests/Helpers/PersonHelperTests.cs @@ -1,10 +1,5 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; namespace API.Tests.Helpers; @@ -12,215 +7,127 @@ public class PersonHelperTests : AbstractDbTest { protected override async Task ResetDb() { - Context.Series.RemoveRange(Context.Series.ToList()); - Context.Person.RemoveRange(Context.Person.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); Context.Series.RemoveRange(Context.Series.ToList()); await Context.SaveChangesAsync(); } - - // 1. Test adding new people and keeping existing ones - [Fact] - public async Task UpdateChapterPeopleAsync_AddNewPeople_ExistingPersonRetained() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var existingPerson = new PersonBuilder("Joe Shmo").Build(); - var chapter = new ChapterBuilder("1").Build(); - - // Create an existing person and assign them to the series with a role - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithFormat(MangaFormat.Archive) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(existingPerson, PersonRole.Editor) - .Build()) - .WithVolume(new VolumeBuilder("1").WithChapter(chapter).Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Call UpdateChapterPeopleAsync with one existing and one new person - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo", "New Person" }, PersonRole.Editor, UnitOfWork); - - // Assert existing person retained and new person added - var people = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Contains(people, p => p.Name == "Joe Shmo"); - Assert.Contains(people, p => p.Name == "New Person"); - - var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - Assert.Contains("Joe Shmo", chapterPeople); - Assert.Contains("New Person", chapterPeople); - } - - // 2. Test removing a person no longer in the list - [Fact] - public async Task UpdateChapterPeopleAsync_RemovePeople() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var existingPerson1 = new PersonBuilder("Joe Shmo").Build(); - var existingPerson2 = new PersonBuilder("Jane Doe").Build(); - var chapter = new ChapterBuilder("1") - .WithPerson(existingPerson1, PersonRole.Editor) - .WithPerson(existingPerson2, PersonRole.Editor) - .Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Call UpdateChapterPeopleAsync with only one person - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); - - // PersonHelper does not remove the Person from the global DbSet itself - await UnitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); - - var people = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.DoesNotContain(people, p => p.Name == "Jane Doe"); - - var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - Assert.Contains("Joe Shmo", chapterPeople); - Assert.DoesNotContain("Jane Doe", chapterPeople); - } - - // 3. Test no changes when the list of people is the same - [Fact] - public async Task UpdateChapterPeopleAsync_NoChanges() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var existingPerson = new PersonBuilder("Joe Shmo").Build(); - var chapter = new ChapterBuilder("1").WithPerson(existingPerson, PersonRole.Editor).Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Call UpdateChapterPeopleAsync with the same list - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); - - var people = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Contains(people, p => p.Name == "Joe Shmo"); - - var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - Assert.Contains("Joe Shmo", chapterPeople); - Assert.Single(chapter.People); // No duplicate entries - } - - // 4. Test multiple roles for a person - [Fact] - public async Task UpdateChapterPeopleAsync_MultipleRoles() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var person = new PersonBuilder("Joe Shmo").Build(); - var chapter = new ChapterBuilder("1").WithPerson(person, PersonRole.Writer).Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Add same person as Editor - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); - - // Ensure that the same person is assigned with two roles - var chapterPeople = chapter - .People - .Where(cp => - cp.Person.Name == "Joe Shmo") - .ToList(); - Assert.Equal(2, chapterPeople.Count); // One for each role - Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Writer); - Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Editor); - } - - [Fact] - public async Task UpdateChapterPeopleAsync_MatchOnAlias_NoChanges() - { - await ResetDb(); - - var library = new LibraryBuilder("My Library") - .Build(); - - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var person = new PersonBuilder("Joe Doe") - .WithAlias("Jonny Doe") - .Build(); - - var chapter = new ChapterBuilder("1") - .WithPerson(person, PersonRole.Editor) - .Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - await UnitOfWork.CommitAsync(); - - // Add on Name - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Doe" }, PersonRole.Editor, UnitOfWork); - await UnitOfWork.CommitAsync(); - - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - - // Add on alias - await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Jonny Doe" }, PersonRole.Editor, UnitOfWork); - await UnitOfWork.CommitAsync(); - - allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - } - - // TODO: Unit tests for series + // + // // 1. Test adding new people and keeping existing ones + // [Fact] + // public async Task UpdateChapterPeopleAsync_AddNewPeople_ExistingPersonRetained() + // { + // var existingPerson = new PersonBuilder("Joe Shmo").Build(); + // var chapter = new ChapterBuilder("1").Build(); + // + // // Create an existing person and assign them to the series with a role + // var series = new SeriesBuilder("Test 1") + // .WithFormat(MangaFormat.Archive) + // .WithMetadata(new SeriesMetadataBuilder() + // .WithPerson(existingPerson, PersonRole.Editor) + // .Build()) + // .WithVolume(new VolumeBuilder("1").WithChapter(chapter).Build()) + // .Build(); + // + // _unitOfWork.SeriesRepository.Add(series); + // await _unitOfWork.CommitAsync(); + // + // // Call UpdateChapterPeopleAsync with one existing and one new person + // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo", "New Person" }, PersonRole.Editor, _unitOfWork); + // + // // Assert existing person retained and new person added + // var people = await _unitOfWork.PersonRepository.GetAllPeople(); + // Assert.Contains(people, p => p.Name == "Joe Shmo"); + // Assert.Contains(people, p => p.Name == "New Person"); + // + // var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); + // Assert.Contains("Joe Shmo", chapterPeople); + // Assert.Contains("New Person", chapterPeople); + // } + // + // // 2. Test removing a person no longer in the list + // [Fact] + // public async Task UpdateChapterPeopleAsync_RemovePeople() + // { + // var existingPerson1 = new PersonBuilder("Joe Shmo").Build(); + // var existingPerson2 = new PersonBuilder("Jane Doe").Build(); + // var chapter = new ChapterBuilder("1").Build(); + // + // var series = new SeriesBuilder("Test 1") + // .WithVolume(new VolumeBuilder("1") + // .WithChapter(new ChapterBuilder("1") + // .WithPerson(existingPerson1, PersonRole.Editor) + // .WithPerson(existingPerson2, PersonRole.Editor) + // .Build()) + // .Build()) + // .Build(); + // + // _unitOfWork.SeriesRepository.Add(series); + // await _unitOfWork.CommitAsync(); + // + // // Call UpdateChapterPeopleAsync with only one person + // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, _unitOfWork); + // + // var people = await _unitOfWork.PersonRepository.GetAllPeople(); + // Assert.DoesNotContain(people, p => p.Name == "Jane Doe"); + // + // var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); + // Assert.Contains("Joe Shmo", chapterPeople); + // Assert.DoesNotContain("Jane Doe", chapterPeople); + // } + // + // // 3. Test no changes when the list of people is the same + // [Fact] + // public async Task UpdateChapterPeopleAsync_NoChanges() + // { + // var existingPerson = new PersonBuilder("Joe Shmo").Build(); + // var chapter = new ChapterBuilder("1").Build(); + // + // var series = new SeriesBuilder("Test 1") + // .WithVolume(new VolumeBuilder("1") + // .WithChapter(new ChapterBuilder("1") + // .WithPerson(existingPerson, PersonRole.Editor) + // .Build()) + // .Build()) + // .Build(); + // + // _unitOfWork.SeriesRepository.Add(series); + // await _unitOfWork.CommitAsync(); + // + // // Call UpdateChapterPeopleAsync with the same list + // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, _unitOfWork); + // + // var people = await _unitOfWork.PersonRepository.GetAllPeople(); + // Assert.Contains(people, p => p.Name == "Joe Shmo"); + // + // var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); + // Assert.Contains("Joe Shmo", chapterPeople); + // Assert.Single(chapter.People); // No duplicate entries + // } + // + // // 4. Test multiple roles for a person + // [Fact] + // public async Task UpdateChapterPeopleAsync_MultipleRoles() + // { + // var person = new PersonBuilder("Joe Shmo").Build(); + // var chapter = new ChapterBuilder("1").Build(); + // + // var series = new SeriesBuilder("Test 1") + // .WithVolume(new VolumeBuilder("1") + // .WithChapter(new ChapterBuilder("1") + // .WithPerson(person, PersonRole.Writer) // Assign person as Writer + // .Build()) + // .Build()) + // .Build(); + // + // _unitOfWork.SeriesRepository.Add(series); + // await _unitOfWork.CommitAsync(); + // + // // Add same person as Editor + // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, _unitOfWork); + // + // // Ensure that the same person is assigned with two roles + // var chapterPeople = chapter.People.Where(cp => cp.Person.Name == "Joe Shmo").ToList(); + // Assert.Equal(2, chapterPeople.Count); // One for each role + // Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Writer); + // Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Editor); + // } } diff --git a/API.Tests/Helpers/RandfHelper.cs b/API.Tests/Helpers/RandfHelper.cs deleted file mode 100644 index d8c007df7..000000000 --- a/API.Tests/Helpers/RandfHelper.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace API.Tests.Helpers; - -public class RandfHelper -{ - private static readonly Random Random = new (); - - /// - /// Returns true if all simple fields are equal - /// - /// - /// - /// fields to ignore, note that the names are very weird sometimes - /// - /// - /// - public static bool AreSimpleFieldsEqual(object obj1, object obj2, IList ignoreFields) - { - if (obj1 == null || obj2 == null) - throw new ArgumentNullException("Neither object can be null."); - - Type type1 = obj1.GetType(); - Type type2 = obj2.GetType(); - - if (type1 != type2) - throw new ArgumentException("Objects must be of the same type."); - - FieldInfo[] fields = type1.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); - - foreach (var field in fields) - { - if (field.IsInitOnly) continue; - if (ignoreFields.Contains(field.Name)) continue; - - Type fieldType = field.FieldType; - - if (IsRelevantType(fieldType)) - { - object value1 = field.GetValue(obj1); - object value2 = field.GetValue(obj2); - - if (!Equals(value1, value2)) - { - throw new ArgumentException("Fields must be of the same type: " + field.Name + " was " + value1 + " and " + value2); - } - } - } - - return true; - } - - private static bool IsRelevantType(Type type) - { - return type.IsPrimitive - || type == typeof(string) - || type.IsEnum; - } - - /// - /// Sets all simple fields of the given object to a random value - /// - /// - /// Simple is, primitive, string, or enum - /// - public static void SetRandomValues(object obj) - { - if (obj == null) throw new ArgumentNullException(nameof(obj)); - - Type type = obj.GetType(); - FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - - foreach (var field in fields) - { - if (field.IsInitOnly) continue; // Skip readonly fields - - object value = GenerateRandomValue(field.FieldType); - if (value != null) - { - field.SetValue(obj, value); - } - } - } - - private static object GenerateRandomValue(Type type) - { - if (type == typeof(int)) - return Random.Next(); - if (type == typeof(float)) - return (float)Random.NextDouble() * 100; - if (type == typeof(double)) - return Random.NextDouble() * 100; - if (type == typeof(bool)) - return Random.Next(2) == 1; - if (type == typeof(char)) - return (char)Random.Next('A', 'Z' + 1); - if (type == typeof(byte)) - return (byte)Random.Next(0, 256); - if (type == typeof(short)) - return (short)Random.Next(short.MinValue, short.MaxValue); - if (type == typeof(long)) - return (long)(Random.NextDouble() * long.MaxValue); - if (type == typeof(string)) - return GenerateRandomString(10); - if (type.IsEnum) - { - var values = Enum.GetValues(type); - return values.GetValue(Random.Next(values.Length)); - } - - // Unsupported type - return null; - } - - private static string GenerateRandomString(int length) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.Next(s.Length)]).ToArray()); - } -} diff --git a/API.Tests/Parsers/ComicVineParserTests.cs b/API.Tests/Parsers/ComicVineParserTests.cs index 2f4fd568e..f01e98afd 100644 --- a/API.Tests/Parsers/ComicVineParserTests.cs +++ b/API.Tests/Parsers/ComicVineParserTests.cs @@ -36,7 +36,7 @@ public class ComicVineParserTests public void Parse_SeriesWithComicInfo() { var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/", - RootDirectory, LibraryType.ComicVine, true, new ComicInfo() + RootDirectory, LibraryType.ComicVine, new ComicInfo() { Series = "Birds of Prey", Volume = "2002" @@ -54,7 +54,7 @@ public class ComicVineParserTests public void Parse_SeriesWithDirectoryNameAsSeriesYear() { var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/", - RootDirectory, LibraryType.ComicVine, true, null); + RootDirectory, LibraryType.ComicVine, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey (2002)", actual.Series); @@ -69,7 +69,7 @@ public class ComicVineParserTests public void Parse_SeriesWithADirectoryNameAsSeriesYear() { var actual = _parser.Parse("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", "C:/Comics/DC Comics/", - RootDirectory, LibraryType.ComicVine, true, null); + RootDirectory, LibraryType.ComicVine, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey (1999)", actual.Series); @@ -84,7 +84,7 @@ public class ComicVineParserTests public void Parse_FallbackToDirectoryNameOnly() { var actual = _parser.Parse("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", "C:/Comics/DC Comics/", - RootDirectory, LibraryType.ComicVine, true, null); + RootDirectory, LibraryType.ComicVine, null); Assert.NotNull(actual); Assert.Equal("Blood Syndicate", actual.Series); diff --git a/API.Tests/Parsers/DefaultParserTests.cs b/API.Tests/Parsers/DefaultParserTests.cs index 244c08b97..733b55d62 100644 --- a/API.Tests/Parsers/DefaultParserTests.cs +++ b/API.Tests/Parsers/DefaultParserTests.cs @@ -33,7 +33,7 @@ public class DefaultParserTests [InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")] public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries) { - var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, true, null); + var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, null); if (actual == null) { Assert.NotNull(actual); @@ -74,7 +74,7 @@ public class DefaultParserTests fs.AddFile(inputFile, new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fs); var parser = new BasicParser(ds, new ImageParser(ds)); - var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null); + var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); Assert.Equal(expectedParseInfo, actual.Series); } @@ -90,7 +90,7 @@ public class DefaultParserTests fs.AddFile(inputFile, new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fs); var parser = new BasicParser(ds, new ImageParser(ds)); - var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null); + var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); Assert.Equal(expectedParseInfo, actual.Series); } @@ -251,7 +251,7 @@ public class DefaultParserTests foreach (var file in expected.Keys) { var expectedInfo = expected[file]; - var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, true, null); + var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, null); if (expectedInfo == null) { Assert.Null(actual); @@ -289,7 +289,7 @@ public class DefaultParserTests Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, FullFilePath = filepath, IsSpecial = false }; - var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, true, null); + var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -315,7 +315,7 @@ public class DefaultParserTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, true, null); + actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -341,7 +341,7 @@ public class DefaultParserTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, true, null); + actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -383,7 +383,7 @@ public class DefaultParserTests FullFilePath = filepath }; - var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null); + var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null); Assert.NotNull(actual); _testOutputHelper.WriteLine($"Validating {filepath}"); @@ -412,7 +412,7 @@ public class DefaultParserTests FullFilePath = filepath }; - actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null); + actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null); Assert.NotNull(actual); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expected.Format, actual.Format); @@ -475,7 +475,7 @@ public class DefaultParserTests foreach (var file in expected.Keys) { var expectedInfo = expected[file]; - var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, true, null); + var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, null); if (expectedInfo == null) { Assert.Null(actual); diff --git a/API.Tests/Parsers/ImageParserTests.cs b/API.Tests/Parsers/ImageParserTests.cs index 63df1926e..f95c98ddf 100644 --- a/API.Tests/Parsers/ImageParserTests.cs +++ b/API.Tests/Parsers/ImageParserTests.cs @@ -34,7 +34,7 @@ public class ImageParserTests public void Parse_SeriesWithDirectoryName() { var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01/01.jpg", "C:/Comics/Birds of Prey/", - RootDirectory, LibraryType.Image, true, null); + RootDirectory, LibraryType.Image, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey", actual.Series); @@ -48,7 +48,7 @@ public class ImageParserTests public void Parse_SeriesWithNoNestedChapter() { var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01 page 01.jpg", "C:/Comics/", - RootDirectory, LibraryType.Image, true, null); + RootDirectory, LibraryType.Image, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey", actual.Series); @@ -62,7 +62,7 @@ public class ImageParserTests public void Parse_SeriesWithLooseImages() { var actual = _parser.Parse("C:/Comics/Birds of Prey/page 01.jpg", "C:/Comics/", - RootDirectory, LibraryType.Image, true, null); + RootDirectory, LibraryType.Image, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey", actual.Series); diff --git a/API.Tests/Parsers/PdfParserTests.cs b/API.Tests/Parsers/PdfParserTests.cs index 08bf9f25d..72088526d 100644 --- a/API.Tests/Parsers/PdfParserTests.cs +++ b/API.Tests/Parsers/PdfParserTests.cs @@ -35,7 +35,7 @@ public class PdfParserTests { var actual = _parser.Parse("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf", "C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/", - RootDirectory, LibraryType.Book, true, null); + RootDirectory, LibraryType.Book, null); Assert.NotNull(actual); Assert.Equal("A Dictionary of Japanese Food - Ingredients and Culture", actual.Series); diff --git a/API.Tests/Parsing/ImageParsingTests.cs b/API.Tests/Parsing/ImageParsingTests.cs index 362b4b08c..3d78d9372 100644 --- a/API.Tests/Parsing/ImageParsingTests.cs +++ b/API.Tests/Parsing/ImageParsingTests.cs @@ -34,7 +34,7 @@ public class ImageParsingTests Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, FullFilePath = filepath, IsSpecial = false }; - var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, true, null); + var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -60,7 +60,7 @@ public class ImageParsingTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null); + actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -86,7 +86,7 @@ public class ImageParsingTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null); + actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); diff --git a/API.Tests/Parsing/MagazineParserTests.cs b/API.Tests/Parsing/MagazineParserTests.cs new file mode 100644 index 000000000..f6e71d9e0 --- /dev/null +++ b/API.Tests/Parsing/MagazineParserTests.cs @@ -0,0 +1,43 @@ +using Xunit; + +namespace API.Tests.Parser; + +public class MagazineParserTests +{ + [Theory] + [InlineData("3D World - 2018 UK", "3D World")] + [InlineData("3D World - 2018", "3D World")] + [InlineData("UK World - 022012 [Digital]", "UK World")] + [InlineData("Computer Weekly - September 2023", "Computer Weekly")] + public void ParseSeriesTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineSeries(filename)); + } + + [Theory] + [InlineData("UK World - 022012 [Digital]", "2012")] + [InlineData("Computer Weekly - September 2023", "2023")] + [InlineData("Computer Weekly - September 2023 #2", "2023")] + [InlineData("PC Games - 2001 #01", "2001")] + public void ParseVolumeTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineVolume(filename)); + } + + [Theory] + [InlineData("UK World - 022012 [Digital]", "0")] + [InlineData("Computer Weekly - September 2023", "9")] + [InlineData("Computer Weekly - September 2023 #2", "2")] + [InlineData("PC Games - 2001 #01", "1")] + public void ParseChapterTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineChapter(filename)); + } + + [Theory] + [InlineData("AIR International Vol. 14 No. 3 (ISSN 1011-3250)", "1011-3250")] + public void ParseGTINTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseGTIN(filename)); + } +} diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index 53f2bc4c9..8b93c5f90 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -68,8 +68,10 @@ public class MangaParsingTests [InlineData("Манга Тома 1-4", "1-4")] [InlineData("Манга Том 1-4", "1-4")] [InlineData("조선왕조실톡 106화", "106")] + [InlineData("죽음 13회", "13")] [InlineData("동의보감 13장", "13")] [InlineData("몰?루 아카이브 7.5권", "7.5")] + [InlineData("주술회전 1.5권", "1.5")] [InlineData("63권#200", "63")] [InlineData("시즌34삽화2", "34")] [InlineData("Accel World Chapter 001 Volume 002", "2")] diff --git a/API.Tests/Repository/GenreRepositoryTests.cs b/API.Tests/Repository/GenreRepositoryTests.cs deleted file mode 100644 index d197a91ba..000000000 --- a/API.Tests/Repository/GenreRepositoryTests.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; - -namespace API.Tests.Repository; - -public class GenreRepositoryTests : AbstractDbTest -{ - private AppUser _fullAccess; - private AppUser _restrictedAccess; - private AppUser _restrictedAgeAccess; - - protected override async Task ResetDb() - { - Context.Genre.RemoveRange(Context.Genre); - Context.Library.RemoveRange(Context.Library); - await Context.SaveChangesAsync(); - } - - private TestGenreSet CreateTestGenres() - { - return new TestGenreSet - { - SharedSeriesChaptersGenre = new GenreBuilder("Shared Series Chapter Genre").Build(), - SharedSeriesGenre = new GenreBuilder("Shared Series Genre").Build(), - SharedChaptersGenre = new GenreBuilder("Shared Chapters Genre").Build(), - Lib0SeriesChaptersGenre = new GenreBuilder("Lib0 Series Chapter Genre").Build(), - Lib0SeriesGenre = new GenreBuilder("Lib0 Series Genre").Build(), - Lib0ChaptersGenre = new GenreBuilder("Lib0 Chapters Genre").Build(), - Lib1SeriesChaptersGenre = new GenreBuilder("Lib1 Series Chapter Genre").Build(), - Lib1SeriesGenre = new GenreBuilder("Lib1 Series Genre").Build(), - Lib1ChaptersGenre = new GenreBuilder("Lib1 Chapters Genre").Build(), - Lib1ChapterAgeGenre = new GenreBuilder("Lib1 Chapter Age Genre").Build() - }; - } - - private async Task SeedDbWithGenres(TestGenreSet genres) - { - await CreateTestUsers(); - await AddGenresToContext(genres); - await CreateLibrariesWithGenres(genres); - await AssignLibrariesToUsers(); - } - - private async Task CreateTestUsers() - { - _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); - _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); - _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); - _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; - _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; - - Context.Users.Add(_fullAccess); - Context.Users.Add(_restrictedAccess); - Context.Users.Add(_restrictedAgeAccess); - await Context.SaveChangesAsync(); - } - - private async Task AddGenresToContext(TestGenreSet genres) - { - var allGenres = genres.GetAllGenres(); - Context.Genre.AddRange(allGenres); - await Context.SaveChangesAsync(); - } - - private async Task CreateLibrariesWithGenres(TestGenreSet genres) - { - var lib0 = new LibraryBuilder("lib0") - .WithSeries(new SeriesBuilder("lib0-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0SeriesGenre]) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0ChaptersGenre]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .Build()) - .Build()) - .Build(); - - var lib1 = new LibraryBuilder("lib1") - .WithSeries(new SeriesBuilder("lib1-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre, genres.Lib1ChapterAgeGenre]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("lib1-s1") - .WithMetadata(new SeriesMetadataBuilder() - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre]) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) - .Build()) - .Build()) - .Build()) - .Build(); - - Context.Library.Add(lib0); - Context.Library.Add(lib1); - await Context.SaveChangesAsync(); - } - - private async Task AssignLibrariesToUsers() - { - var lib0 = Context.Library.First(l => l.Name == "lib0"); - var lib1 = Context.Library.First(l => l.Name == "lib1"); - - _fullAccess.Libraries.Add(lib0); - _fullAccess.Libraries.Add(lib1); - _restrictedAccess.Libraries.Add(lib1); - _restrictedAgeAccess.Libraries.Add(lib1); - - await Context.SaveChangesAsync(); - } - - private static Predicate ContainsGenreCheck(Genre genre) - { - return g => g.Id == genre.Id; - } - - private static void AssertGenrePresent(IEnumerable genres, Genre expectedGenre) - { - Assert.Contains(genres, ContainsGenreCheck(expectedGenre)); - } - - private static void AssertGenreNotPresent(IEnumerable genres, Genre expectedGenre) - { - Assert.DoesNotContain(genres, ContainsGenreCheck(expectedGenre)); - } - - private static BrowseGenreDto GetGenreDto(IEnumerable genres, Genre genre) - { - return genres.First(dto => dto.Id == genre.Id); - } - - [Fact] - public async Task GetBrowseableGenre_FullAccess_ReturnsAllGenresWithCorrectCounts() - { - // Arrange - await ResetDb(); - var genres = CreateTestGenres(); - await SeedDbWithGenres(genres); - - // Act - var fullAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_fullAccess.Id, new UserParams()); - - // Assert - Assert.Equal(genres.GetAllGenres().Count, fullAccessGenres.TotalCount); - - foreach (var genre in genres.GetAllGenres()) - { - AssertGenrePresent(fullAccessGenres, genre); - } - - // Verify counts - 1 lib0 series, 2 lib1 series = 3 total series - Assert.Equal(3, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); - Assert.Equal(6, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); - Assert.Equal(1, GetGenreDto(fullAccessGenres, genres.Lib0SeriesGenre).SeriesCount); - } - - [Fact] - public async Task GetBrowseableGenre_RestrictedAccess_ReturnsOnlyAccessibleGenres() - { - // Arrange - await ResetDb(); - var genres = CreateTestGenres(); - await SeedDbWithGenres(genres); - - // Act - var restrictedAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 4 library 1 specific = 7 genres - Assert.Equal(7, restrictedAccessGenres.TotalCount); - - // Verify shared and Library 1 genres are present - AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesGenre); - AssertGenrePresent(restrictedAccessGenres, genres.SharedChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChaptersGenre); - AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChapterAgeGenre); - - // Verify Library 0 specific genres are not present - AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesChaptersGenre); - AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesGenre); - AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0ChaptersGenre); - - // Verify counts - 2 lib1 series - Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); - Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); - Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.Lib1SeriesGenre).SeriesCount); - Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.Lib1ChaptersGenre).ChapterCount); - Assert.Equal(1, GetGenreDto(restrictedAccessGenres, genres.Lib1ChapterAgeGenre).ChapterCount); - } - - [Fact] - public async Task GetBrowseableGenre_RestrictedAgeAccess_FiltersAgeRestrictedContent() - { - // Arrange - await ResetDb(); - var genres = CreateTestGenres(); - await SeedDbWithGenres(genres); - - // Act - var restrictedAgeAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAgeAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 3 lib1 specific = 6 genres (age-restricted genre filtered out) - Assert.Equal(6, restrictedAgeAccessGenres.TotalCount); - - // Verify accessible genres are present - AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedChaptersGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesChaptersGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesGenre); - AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre); - - // Verify age-restricted genre is filtered out - AssertGenreNotPresent(restrictedAgeAccessGenres, genres.Lib1ChapterAgeGenre); - - // Verify counts - 1 series lib1 (age-restricted series filtered out) - Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); - Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1SeriesGenre).SeriesCount); - - // These values represent a bug - chapters are not properly filtered when their series is age-restricted - // Should be 2, but currently returns 3 due to the filtering issue - Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); - Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre).ChapterCount); - } - - private class TestGenreSet - { - public Genre SharedSeriesChaptersGenre { get; set; } - public Genre SharedSeriesGenre { get; set; } - public Genre SharedChaptersGenre { get; set; } - public Genre Lib0SeriesChaptersGenre { get; set; } - public Genre Lib0SeriesGenre { get; set; } - public Genre Lib0ChaptersGenre { get; set; } - public Genre Lib1SeriesChaptersGenre { get; set; } - public Genre Lib1SeriesGenre { get; set; } - public Genre Lib1ChaptersGenre { get; set; } - public Genre Lib1ChapterAgeGenre { get; set; } - - public List GetAllGenres() - { - return - [ - SharedSeriesChaptersGenre, SharedSeriesGenre, SharedChaptersGenre, - Lib0SeriesChaptersGenre, Lib0SeriesGenre, Lib0ChaptersGenre, - Lib1SeriesChaptersGenre, Lib1SeriesGenre, Lib1ChaptersGenre, Lib1ChapterAgeGenre - ]; - } - } -} diff --git a/API.Tests/Repository/PersonRepositoryTests.cs b/API.Tests/Repository/PersonRepositoryTests.cs deleted file mode 100644 index a2b19cc0c..000000000 --- a/API.Tests/Repository/PersonRepositoryTests.cs +++ /dev/null @@ -1,342 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; - -namespace API.Tests.Repository; - -public class PersonRepositoryTests : AbstractDbTest -{ - private AppUser _fullAccess; - private AppUser _restrictedAccess; - private AppUser _restrictedAgeAccess; - - protected override async Task ResetDb() - { - Context.Person.RemoveRange(Context.Person.ToList()); - Context.Library.RemoveRange(Context.Library.ToList()); - Context.AppUser.RemoveRange(Context.AppUser.ToList()); - await UnitOfWork.CommitAsync(); - } - - private async Task SeedDb() - { - _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); - _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); - _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); - _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; - _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; - - Context.AppUser.Add(_fullAccess); - Context.AppUser.Add(_restrictedAccess); - Context.AppUser.Add(_restrictedAgeAccess); - await Context.SaveChangesAsync(); - - var people = CreateTestPeople(); - Context.Person.AddRange(people); - await Context.SaveChangesAsync(); - - var libraries = CreateTestLibraries(people); - Context.Library.AddRange(libraries); - await Context.SaveChangesAsync(); - - _fullAccess.Libraries.Add(libraries[0]); // lib0 - _fullAccess.Libraries.Add(libraries[1]); // lib1 - _restrictedAccess.Libraries.Add(libraries[1]); // lib1 only - _restrictedAgeAccess.Libraries.Add(libraries[1]); // lib1 only - - await Context.SaveChangesAsync(); - } - - private static List CreateTestPeople() - { - return new List - { - new PersonBuilder("Shared Series Chapter Person").Build(), - new PersonBuilder("Shared Series Person").Build(), - new PersonBuilder("Shared Chapters Person").Build(), - new PersonBuilder("Lib0 Series Chapter Person").Build(), - new PersonBuilder("Lib0 Series Person").Build(), - new PersonBuilder("Lib0 Chapters Person").Build(), - new PersonBuilder("Lib1 Series Chapter Person").Build(), - new PersonBuilder("Lib1 Series Person").Build(), - new PersonBuilder("Lib1 Chapters Person").Build(), - new PersonBuilder("Lib1 Chapter Age Person").Build() - }; - } - - private static List CreateTestLibraries(List people) - { - var lib0 = new LibraryBuilder("lib0") - .WithSeries(new SeriesBuilder("lib0-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Writer) - .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Writer) - .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Writer) - .WithPerson(GetPersonByName(people, "Lib0 Series Person"), PersonRole.Writer) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Colorist) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Colorist) - .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Colorist) - .WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Colorist) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Editor) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Editor) - .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Editor) - .WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Editor) - .Build()) - .Build()) - .Build()) - .Build(); - - var lib1 = new LibraryBuilder("lib1") - .WithSeries(new SeriesBuilder("lib1-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Letterer) - .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Letterer) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Letterer) - .WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Letterer) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Imprint) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Imprint) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Imprint) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Imprint) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.CoverArtist) - .WithPerson(GetPersonByName(people, "Lib1 Chapter Age Person"), PersonRole.CoverArtist) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("lib1-s1") - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Inker) - .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Inker) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Inker) - .WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Inker) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Team) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Team) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Team) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Team) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Translator) - .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Translator) - .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Translator) - .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Translator) - .Build()) - .Build()) - .Build()) - .Build(); - - return new List { lib0, lib1 }; - } - - private static Person GetPersonByName(List people, string name) - { - return people.First(p => p.Name == name); - } - - private Person GetPersonByName(string name) - { - return Context.Person.First(p => p.Name == name); - } - - private static Predicate ContainsPersonCheck(Person person) - { - return p => p.Id == person.Id; - } - - [Fact] - public async Task GetBrowsePersonDtos() - { - await ResetDb(); - await SeedDb(); - - // Get people from database for assertions - var sharedSeriesChaptersPerson = GetPersonByName("Shared Series Chapter Person"); - var lib0SeriesPerson = GetPersonByName("Lib0 Series Person"); - var lib1SeriesPerson = GetPersonByName("Lib1 Series Person"); - var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person"); - var allPeople = Context.Person.ToList(); - - var fullAccessPeople = - await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_fullAccess.Id, new BrowsePersonFilterDto(), - new UserParams()); - Assert.Equal(allPeople.Count, fullAccessPeople.TotalCount); - - foreach (var person in allPeople) - Assert.Contains(fullAccessPeople, ContainsPersonCheck(person)); - - // 1 series in lib0, 2 series in lib1 - Assert.Equal(3, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount); - // 3 series with each 2 chapters - Assert.Equal(6, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount); - // 1 series in lib0 - Assert.Equal(1, fullAccessPeople.First(dto => dto.Id == lib0SeriesPerson.Id).SeriesCount); - // 2 series in lib1 - Assert.Equal(2, fullAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount); - - var restrictedAccessPeople = - await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAccess.Id, new BrowsePersonFilterDto(), - new UserParams()); - - Assert.Equal(7, restrictedAccessPeople.TotalCount); - - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Chapter Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Chapters Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Chapter Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapters Person"))); - Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapter Age Person"))); - - // 2 series in lib1, no series in lib0 - Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount); - // 2 series with each 2 chapters - Assert.Equal(4, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount); - // 2 series in lib1 - Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount); - - var restrictedAgeAccessPeople = await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAgeAccess.Id, - new BrowsePersonFilterDto(), new UserParams()); - - // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up - Assert.Equal(6, restrictedAgeAccessPeople.TotalCount); - - // No access to the age restricted chapter - Assert.DoesNotContain(restrictedAgeAccessPeople, ContainsPersonCheck(lib1ChapterAgePerson)); - } - - [Fact] - public async Task GetRolesForPersonByName() - { - await ResetDb(); - await SeedDb(); - - var sharedSeriesPerson = GetPersonByName("Shared Series Person"); - var sharedChaptersPerson = GetPersonByName("Shared Chapters Person"); - var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person"); - - var sharedSeriesRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _fullAccess.Id); - var chapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _fullAccess.Id); - var ageChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _fullAccess.Id); - Assert.Equal(3, sharedSeriesRoles.Count()); - Assert.Equal(6, chapterRoles.Count()); - Assert.Single(ageChapterRoles); - - var restrictedRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAccess.Id); - var restrictedChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAccess.Id); - var restrictedAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAccess.Id); - Assert.Equal(2, restrictedRoles.Count()); - Assert.Equal(4, restrictedChapterRoles.Count()); - Assert.Single(restrictedAgePersonChapterRoles); - - var restrictedAgeRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAgeAccess.Id); - var restrictedAgeChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAgeAccess.Id); - var restrictedAgeAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAgeAccess.Id); - Assert.Single(restrictedAgeRoles); - Assert.Equal(2, restrictedAgeChapterRoles.Count()); - // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up - Assert.Empty(restrictedAgeAgePersonChapterRoles); - } - - [Fact] - public async Task GetPersonDtoByName() - { - await ResetDb(); - await SeedDb(); - - var allPeople = Context.Person.ToList(); - - foreach (var person in allPeople) - { - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName(person.Name, _fullAccess.Id)); - } - - Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAccess.Id)); - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Shared Series Person", _restrictedAccess.Id)); - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAccess.Id)); - - Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAgeAccess.Id)); - Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAgeAccess.Id)); - // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up - Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Chapter Age Person", _restrictedAgeAccess.Id)); - } - - [Fact] - public async Task GetSeriesKnownFor() - { - await ResetDb(); - await SeedDb(); - - var sharedSeriesPerson = GetPersonByName("Shared Series Person"); - var lib1SeriesPerson = GetPersonByName("Lib1 Series Person"); - - var series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _fullAccess.Id); - Assert.Equal(3, series.Count()); - - series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAccess.Id); - Assert.Equal(2, series.Count()); - - series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAgeAccess.Id); - Assert.Single(series); - - series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(lib1SeriesPerson.Id, _restrictedAgeAccess.Id); - Assert.Single(series); - } - - [Fact] - public async Task GetChaptersForPersonByRole() - { - await ResetDb(); - await SeedDb(); - - var sharedChaptersPerson = GetPersonByName("Shared Chapters Person"); - - // Lib0 - var chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Colorist); - var restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Colorist); - var restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Colorist); - Assert.Single(chapters); - Assert.Empty(restrictedChapters); - Assert.Empty(restrictedAgeChapters); - - // Lib1 - age restricted series - chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Imprint); - restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Imprint); - restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Imprint); - Assert.Single(chapters); - Assert.Single(restrictedChapters); - Assert.Empty(restrictedAgeChapters); - - // Lib1 - not age restricted series - chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Team); - restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Team); - restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Team); - Assert.Single(chapters); - Assert.Single(restrictedChapters); - Assert.Single(restrictedAgeChapters); - } -} diff --git a/API.Tests/Repository/TagRepositoryTests.cs b/API.Tests/Repository/TagRepositoryTests.cs deleted file mode 100644 index 229082eb6..000000000 --- a/API.Tests/Repository/TagRepositoryTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; - -namespace API.Tests.Repository; - -public class TagRepositoryTests : AbstractDbTest -{ - private AppUser _fullAccess; - private AppUser _restrictedAccess; - private AppUser _restrictedAgeAccess; - - protected override async Task ResetDb() - { - Context.Tag.RemoveRange(Context.Tag); - Context.Library.RemoveRange(Context.Library); - await Context.SaveChangesAsync(); - } - - private TestTagSet CreateTestTags() - { - return new TestTagSet - { - SharedSeriesChaptersTag = new TagBuilder("Shared Series Chapter Tag").Build(), - SharedSeriesTag = new TagBuilder("Shared Series Tag").Build(), - SharedChaptersTag = new TagBuilder("Shared Chapters Tag").Build(), - Lib0SeriesChaptersTag = new TagBuilder("Lib0 Series Chapter Tag").Build(), - Lib0SeriesTag = new TagBuilder("Lib0 Series Tag").Build(), - Lib0ChaptersTag = new TagBuilder("Lib0 Chapters Tag").Build(), - Lib1SeriesChaptersTag = new TagBuilder("Lib1 Series Chapter Tag").Build(), - Lib1SeriesTag = new TagBuilder("Lib1 Series Tag").Build(), - Lib1ChaptersTag = new TagBuilder("Lib1 Chapters Tag").Build(), - Lib1ChapterAgeTag = new TagBuilder("Lib1 Chapter Age Tag").Build() - }; - } - - private async Task SeedDbWithTags(TestTagSet tags) - { - await CreateTestUsers(); - await AddTagsToContext(tags); - await CreateLibrariesWithTags(tags); - await AssignLibrariesToUsers(); - } - - private async Task CreateTestUsers() - { - _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); - _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); - _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); - _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; - _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; - - Context.Users.Add(_fullAccess); - Context.Users.Add(_restrictedAccess); - Context.Users.Add(_restrictedAgeAccess); - await Context.SaveChangesAsync(); - } - - private async Task AddTagsToContext(TestTagSet tags) - { - var allTags = tags.GetAllTags(); - Context.Tag.AddRange(allTags); - await Context.SaveChangesAsync(); - } - - private async Task CreateLibrariesWithTags(TestTagSet tags) - { - var lib0 = new LibraryBuilder("lib0") - .WithSeries(new SeriesBuilder("lib0-s0") - .WithMetadata(new SeriesMetadata - { - Tags = [tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib0SeriesChaptersTag, tags.Lib0SeriesTag] - }) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib0SeriesChaptersTag, tags.Lib0ChaptersTag]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .Build()) - .Build()) - .Build()) - .Build(); - - var lib1 = new LibraryBuilder("lib1") - .WithSeries(new SeriesBuilder("lib1-s0") - .WithMetadata(new SeriesMetadataBuilder() - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag, tags.Lib1ChapterAgeTag]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .WithSeries(new SeriesBuilder("lib1-s1") - .WithMetadata(new SeriesMetadataBuilder() - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag]) - .Build()) - .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder("1") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .Build()) - .WithChapter(new ChapterBuilder("2") - .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) - .WithAgeRating(AgeRating.Mature17Plus) - .Build()) - .Build()) - .Build()) - .Build(); - - Context.Library.Add(lib0); - Context.Library.Add(lib1); - await Context.SaveChangesAsync(); - } - - private async Task AssignLibrariesToUsers() - { - var lib0 = Context.Library.First(l => l.Name == "lib0"); - var lib1 = Context.Library.First(l => l.Name == "lib1"); - - _fullAccess.Libraries.Add(lib0); - _fullAccess.Libraries.Add(lib1); - _restrictedAccess.Libraries.Add(lib1); - _restrictedAgeAccess.Libraries.Add(lib1); - - await Context.SaveChangesAsync(); - } - - private static Predicate ContainsTagCheck(Tag tag) - { - return t => t.Id == tag.Id; - } - - private static void AssertTagPresent(IEnumerable tags, Tag expectedTag) - { - Assert.Contains(tags, ContainsTagCheck(expectedTag)); - } - - private static void AssertTagNotPresent(IEnumerable tags, Tag expectedTag) - { - Assert.DoesNotContain(tags, ContainsTagCheck(expectedTag)); - } - - private static BrowseTagDto GetTagDto(IEnumerable tags, Tag tag) - { - return tags.First(dto => dto.Id == tag.Id); - } - - [Fact] - public async Task GetBrowseableTag_FullAccess_ReturnsAllTagsWithCorrectCounts() - { - // Arrange - await ResetDb(); - var tags = CreateTestTags(); - await SeedDbWithTags(tags); - - // Act - var fullAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_fullAccess.Id, new UserParams()); - - // Assert - Assert.Equal(tags.GetAllTags().Count, fullAccessTags.TotalCount); - - foreach (var tag in tags.GetAllTags()) - { - AssertTagPresent(fullAccessTags, tag); - } - - // Verify counts - 1 series lib0, 2 series lib1 = 3 total series - Assert.Equal(3, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); - Assert.Equal(6, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); - Assert.Equal(1, GetTagDto(fullAccessTags, tags.Lib0SeriesTag).SeriesCount); - } - - [Fact] - public async Task GetBrowseableTag_RestrictedAccess_ReturnsOnlyAccessibleTags() - { - // Arrange - await ResetDb(); - var tags = CreateTestTags(); - await SeedDbWithTags(tags); - - // Act - var restrictedAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 4 library 1 specific = 7 tags - Assert.Equal(7, restrictedAccessTags.TotalCount); - - // Verify shared and Library 1 tags are present - AssertTagPresent(restrictedAccessTags, tags.SharedSeriesChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.SharedSeriesTag); - AssertTagPresent(restrictedAccessTags, tags.SharedChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1ChaptersTag); - AssertTagPresent(restrictedAccessTags, tags.Lib1ChapterAgeTag); - - // Verify Library 0 specific tags are not present - AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesChaptersTag); - AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesTag); - AssertTagNotPresent(restrictedAccessTags, tags.Lib0ChaptersTag); - - // Verify counts - 2 series lib1 - Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); - Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); - Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.Lib1SeriesTag).SeriesCount); - Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.Lib1ChaptersTag).ChapterCount); - } - - [Fact] - public async Task GetBrowseableTag_RestrictedAgeAccess_FiltersAgeRestrictedContent() - { - // Arrange - await ResetDb(); - var tags = CreateTestTags(); - await SeedDbWithTags(tags); - - // Act - var restrictedAgeAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAgeAccess.Id, new UserParams()); - - // Assert - Should see: 3 shared + 3 lib1 specific = 6 tags (age-restricted tag filtered out) - Assert.Equal(6, restrictedAgeAccessTags.TotalCount); - - // Verify accessible tags are present - AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag); - AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesTag); - AssertTagPresent(restrictedAgeAccessTags, tags.SharedChaptersTag); - AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesChaptersTag); - AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesTag); - AssertTagPresent(restrictedAgeAccessTags, tags.Lib1ChaptersTag); - - // Verify age-restricted tag is filtered out - AssertTagNotPresent(restrictedAgeAccessTags, tags.Lib1ChapterAgeTag); - - // Verify counts - 1 series lib1 (age-restricted series filtered out) - Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); - Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); - Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.Lib1SeriesTag).SeriesCount); - Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.Lib1ChaptersTag).ChapterCount); - } - - private class TestTagSet - { - public Tag SharedSeriesChaptersTag { get; set; } - public Tag SharedSeriesTag { get; set; } - public Tag SharedChaptersTag { get; set; } - public Tag Lib0SeriesChaptersTag { get; set; } - public Tag Lib0SeriesTag { get; set; } - public Tag Lib0ChaptersTag { get; set; } - public Tag Lib1SeriesChaptersTag { get; set; } - public Tag Lib1SeriesTag { get; set; } - public Tag Lib1ChaptersTag { get; set; } - public Tag Lib1ChapterAgeTag { get; set; } - - public List GetAllTags() - { - return - [ - SharedSeriesChaptersTag, SharedSeriesTag, SharedChaptersTag, - Lib0SeriesChaptersTag, Lib0SeriesTag, Lib0ChaptersTag, - Lib1SeriesChaptersTag, Lib1SeriesTag, Lib1ChaptersTag, Lib1ChapterAgeTag - ]; - } - } -} diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index 5848c74ba..a80c1ca01 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -137,7 +137,7 @@ public class BookServiceTests var comicInfo = _bookService.GetComicInfo(filePath); Assert.NotNull(comicInfo); - var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, true, comicInfo); + var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, comicInfo); Assert.NotNull(parserInfo); Assert.Equal(parserInfo.Title, comicInfo.Title); Assert.Equal(parserInfo.Series, comicInfo.Title); diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index caf1ae393..5c1752cd8 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -50,12 +50,12 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService throw new System.NotImplementedException(); } - public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) + public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type) { throw new System.NotImplementedException(); } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) + public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) { throw new System.NotImplementedException(); } diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 973b7c6df..c2c226538 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -15,7 +15,6 @@ using API.Entities.Person; using API.Helpers.Builders; using API.Services.Plus; using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Hangfire; using Microsoft.EntityFrameworkCore; @@ -43,7 +42,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest _externalMetadataService = new ExternalMetadataService(UnitOfWork, Substitute.For>(), Mapper, Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For(), Substitute.For()); + Substitute.For()); } #region Gloabl @@ -882,217 +881,6 @@ public class ExternalMetadataServiceTests : AbstractDbTest } - [Fact] - public void IsSeriesCompleted_ExactMatch() - { - const string seriesName = "Test - Exact Match"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(5) - .WithTotalCount(5) - .Build()) - .Build(); - - var chapters = new List(); - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 5, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.True(result); - } - - [Fact] - public void IsSeriesCompleted_Volumes_DecimalVolumes() - { - const string seriesName = "Test - Volume Complete"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(3) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .WithVolume(new VolumeBuilder("2.5").WithNumber(2.5f).Build()) - .Build(); - - var chapters = new List(); - // External metadata includes decimal volume 2.5 - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.True(result); - Assert.Equal(3, series.Metadata.MaxCount); - Assert.Equal(3, series.Metadata.TotalCount); - } - - /// - /// This is validating that we get a completed even though we have a special chapter and AL doesn't count it - /// - [Fact] - public void IsSeriesCompleted_Volumes_HasSpecialAndDecimal_ExternalNoSpecial() - { - const string seriesName = "Test - Volume Complete"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(3) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("1.5").WithNumber(1.5f).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .WithVolume(new VolumeBuilder(Parser.SpecialVolume).Build()) - .Build(); - - var chapters = new List(); - // External metadata includes volume 1.5, but not the special - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.True(result); - Assert.Equal(3, series.Metadata.MaxCount); - Assert.Equal(3, series.Metadata.TotalCount); - } - - /// - /// This unit test also illustrates the bug where you may get a false positive if you had Volumes 1,2, and 2.1. While - /// missing volume 3. With the external metadata expecting non-decimal volumes. - /// i.e. it would fail if we only had one decimal volume - /// - [Fact] - public void IsSeriesCompleted_Volumes_TooManyDecimalVolumes() - { - const string seriesName = "Test - Volume Complete"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(3) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .WithVolume(new VolumeBuilder("2.1").WithNumber(2.1f).Build()) - .WithVolume(new VolumeBuilder("2.2").WithNumber(2.2f).Build()) - .Build(); - - var chapters = new List(); - // External metadata includes no special or decimals. There are 3 volumes. And we're missing volume 3 - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.False(result); - } - - [Fact] - public void IsSeriesCompleted_NoVolumes_GEQChapterCheck() - { - // We own 11 chapters, the external metadata expects 10 - const string seriesName = "Test - Chapter MaxCount, no volumes"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(11) - .WithTotalCount(10) - .Build()) - .Build(); - - var chapters = new List(); - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.True(result); - Assert.Equal(11, series.Metadata.TotalCount); - Assert.Equal(11, series.Metadata.MaxCount); - } - - [Fact] - public void IsSeriesCompleted_NoVolumes_IncludeAllChaptersCheck() - { - const string seriesName = "Test - Chapter Count"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(7) - .WithTotalCount(10) - .Build()) - .Build(); - - var chapters = new List - { - new ChapterBuilder("0").Build(), - new ChapterBuilder("2").Build(), - new ChapterBuilder("3").Build(), - new ChapterBuilder("4").Build(), - new ChapterBuilder("5").Build(), - new ChapterBuilder("6").Build(), - new ChapterBuilder("7").Build(), - new ChapterBuilder("7.1").Build(), - new ChapterBuilder("7.2").Build(), - new ChapterBuilder("7.3").Build() - }; - // External metadata includes prologues (0) and extra's (7.X) - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.True(result); - Assert.Equal(10, series.Metadata.TotalCount); - Assert.Equal(10, series.Metadata.MaxCount); - } - - [Fact] - public void IsSeriesCompleted_NotEnoughVolumes() - { - const string seriesName = "Test - Incomplete Volume"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(2) - .WithTotalCount(5) - .Build()) - .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) - .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) - .Build(); - - var chapters = new List(); - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 5 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); - - Assert.False(result); - } - - [Fact] - public void IsSeriesCompleted_NoVolumes_NotEnoughChapters() - { - const string seriesName = "Test - Incomplete Chapter"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .WithMaxCount(5) - .WithTotalCount(8) - .Build()) - .Build(); - - var chapters = new List - { - new ChapterBuilder("1").Build(), - new ChapterBuilder("2").Build(), - new ChapterBuilder("3").Build() - }; - var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; - - var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); - - Assert.False(result); - } - #endregion @@ -1890,130 +1678,6 @@ public class ExternalMetadataServiceTests : AbstractDbTest #endregion - #region People Alias - - [Fact] - public async Task PeopleAliasing_AddAsAlias() - { - await ResetDb(); - - const string seriesName = "Test - People - Add as Alias"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - Context.Person.Add(new PersonBuilder("John Doe").Build()); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("Doe", "John", "Story")] - }, 1); - - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - - var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); - Assert.Single(allWriters); - - var johnDoe = allWriters[0].Person; - - Assert.Contains("Doe John", johnDoe.Aliases.Select(pa => pa.Alias)); - } - - [Fact] - public async Task PeopleAliasing_AddOnAlias() - { - await ResetDb(); - - const string seriesName = "Test - People - Add as Alias"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - - Context.Person.Add(new PersonBuilder("John Doe").WithAlias("Doe John").Build()); - - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("Doe", "John", "Story")] - }, 1); - - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - - var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); - Assert.Single(allWriters); - - var johnDoe = allWriters[0].Person; - - Assert.Contains("Doe John", johnDoe.Aliases.Select(pa => pa.Alias)); - } - - [Fact] - public async Task PeopleAliasing_DontAddAsAlias_SameButNotSwitched() - { - await ResetDb(); - - const string seriesName = "Test - People - Add as Alias"; - var series = new SeriesBuilder(seriesName) - .WithLibraryId(1) - .WithMetadata(new SeriesMetadataBuilder() - .Build()) - .Build(); - Context.Series.Attach(series); - await Context.SaveChangesAsync(); - - var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); - metadataSettings.Enabled = true; - metadataSettings.EnablePeople = true; - metadataSettings.FirstLastPeopleNaming = true; - metadataSettings.Overrides = [MetadataSettingField.People]; - metadataSettings.PersonRoles = [PersonRole.Writer]; - Context.MetadataSettings.Update(metadataSettings); - await Context.SaveChangesAsync(); - - await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() - { - Name = seriesName, - Staff = [CreateStaff("John", "Doe Doe", "Story"), CreateStaff("Doe", "John Doe", "Story")] - }, 1); - - var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); - Assert.NotNull(postSeries); - - var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); - Assert.Equal(2, allWriters.Count); - } - - #endregion - #region People - Characters [Fact] @@ -3147,8 +2811,6 @@ public class ExternalMetadataServiceTests : AbstractDbTest metadataSettings.EnableTags = false; metadataSettings.EnablePublicationStatus = false; metadataSettings.EnableStartDate = false; - metadataSettings.FieldMappings = []; - metadataSettings.AgeRatingMappings = new Dictionary(); Context.MetadataSettings.Update(metadataSettings); await Context.SaveChangesAsync(); diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index f2c87e1ad..a1073a55b 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -161,10 +161,10 @@ public class ImageServiceTests private static void GenerateColorImage(string hexColor, string outputPath) { - var (r, g, b) = ImageService.HexToRgb(hexColor); - using var blackImage = Image.Black(200, 100); - using var colorImage = blackImage.NewFromImage(r, g, b); - colorImage.WriteToFile(outputPath); + var color = ImageService.HexToRgb(hexColor); + using var colorImage = Image.Black(200, 100); + using var output = colorImage + new[] { color.R / 255.0, color.G / 255.0, color.B / 255.0 }; + output.WriteToFile(outputPath); } private void GenerateHtmlFileForColorScape() diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index a732b2526..f8714f69a 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -58,35 +58,35 @@ public class MockReadingItemService : IReadingItemService throw new NotImplementedException(); } - public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type) { if (_comicVineParser.IsApplicable(path, type)) { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_imageParser.IsApplicable(path, type)) { - return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_bookParser.IsApplicable(path, type)) { - return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_pdfParser.IsApplicable(path, type)) { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_basicParser.IsApplicable(path, type)) { - return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } return null; } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) { - return Parse(path, rootPath, libraryRoot, type, enableMetadata); + return Parse(path, rootPath, libraryRoot, type); } } diff --git a/API.Tests/Services/PersonServiceTests.cs b/API.Tests/Services/PersonServiceTests.cs deleted file mode 100644 index 5c1929b1c..000000000 --- a/API.Tests/Services/PersonServiceTests.cs +++ /dev/null @@ -1,286 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using Xunit; - -namespace API.Tests.Services; - -public class PersonServiceTests: AbstractDbTest -{ - - [Fact] - public async Task PersonMerge_KeepNonEmptyMetadata() - { - var ps = new PersonService(UnitOfWork); - - var person1 = new Person - { - Name = "Casey Delores", - NormalizedName = "Casey Delores".ToNormalized(), - HardcoverId = "ANonEmptyId", - MalId = 12, - }; - - var person2 = new Person - { - Name= "Delores Casey", - NormalizedName = "Delores Casey".ToNormalized(), - Description = "Hi, I'm Delores Casey!", - Aliases = [new PersonAliasBuilder("Casey, Delores").Build()], - AniListId = 27, - }; - - UnitOfWork.PersonRepository.Attach(person1); - UnitOfWork.PersonRepository.Attach(person2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person1); - - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - - var person = allPeople[0]; - Assert.Equal("Casey Delores", person.Name); - Assert.NotEmpty(person.Description); - Assert.Equal(27, person.AniListId); - Assert.NotNull(person.HardcoverId); - Assert.NotEmpty(person.HardcoverId); - Assert.Contains(person.Aliases, pa => pa.Alias == "Delores Casey"); - Assert.Contains(person.Aliases, pa => pa.Alias == "Casey, Delores"); - } - - [Fact] - public async Task PersonMerge_MergedPersonDestruction() - { - var ps = new PersonService(UnitOfWork); - - var person1 = new Person - { - Name = "Casey Delores", - NormalizedName = "Casey Delores".ToNormalized(), - }; - - var person2 = new Person - { - Name = "Delores Casey", - NormalizedName = "Delores Casey".ToNormalized(), - }; - - UnitOfWork.PersonRepository.Attach(person1); - UnitOfWork.PersonRepository.Attach(person2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person1); - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - } - - [Fact] - public async Task PersonMerge_RetentionChapters() - { - var ps = new PersonService(UnitOfWork); - - var library = new LibraryBuilder("My Library").Build(); - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var user = new AppUserBuilder("Amelia", "amelia@localhost") - .WithLibrary(library).Build(); - UnitOfWork.UserRepository.Add(user); - - var person = new PersonBuilder("Jillian Cowan").Build(); - - var person2 = new PersonBuilder("Cowan Jillian").Build(); - - var chapter = new ChapterBuilder("1") - .WithPerson(person, PersonRole.Editor) - .Build(); - - var chapter2 = new ChapterBuilder("2") - .WithPerson(person2, PersonRole.Editor) - .Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .Build(); - - var series2 = new SeriesBuilder("Test 2") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("2") - .WithChapter(chapter2) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - UnitOfWork.SeriesRepository.Add(series2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person); - - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - var mergedPerson = allPeople[0]; - - Assert.Equal("Jillian Cowan", mergedPerson.Name); - - var chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(1, 1, PersonRole.Editor); - Assert.Equal(2, chapters.Count()); - - chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(1, ChapterIncludes.People); - Assert.NotNull(chapter); - Assert.Single(chapter.People); - - chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(2, ChapterIncludes.People); - Assert.NotNull(chapter2); - Assert.Single(chapter2.People); - - Assert.Equal(chapter.People.First().PersonId, chapter2.People.First().PersonId); - } - - [Fact] - public async Task PersonMerge_NoDuplicateChaptersOrSeries() - { - await ResetDb(); - - var ps = new PersonService(UnitOfWork); - - var library = new LibraryBuilder("My Library").Build(); - UnitOfWork.LibraryRepository.Add(library); - await UnitOfWork.CommitAsync(); - - var user = new AppUserBuilder("Amelia", "amelia@localhost") - .WithLibrary(library).Build(); - UnitOfWork.UserRepository.Add(user); - - var person = new PersonBuilder("Jillian Cowan").Build(); - - var person2 = new PersonBuilder("Cowan Jillian").Build(); - - var chapter = new ChapterBuilder("1") - .WithPerson(person, PersonRole.Editor) - .WithPerson(person2, PersonRole.Colorist) - .Build(); - - var chapter2 = new ChapterBuilder("2") - .WithPerson(person2, PersonRole.Editor) - .WithPerson(person, PersonRole.Editor) - .Build(); - - var series = new SeriesBuilder("Test 1") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("1") - .WithChapter(chapter) - .Build()) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(person, PersonRole.Editor) - .WithPerson(person2, PersonRole.Editor) - .Build()) - .Build(); - - var series2 = new SeriesBuilder("Test 2") - .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder("2") - .WithChapter(chapter2) - .Build()) - .WithMetadata(new SeriesMetadataBuilder() - .WithPerson(person, PersonRole.Editor) - .WithPerson(person2, PersonRole.Colorist) - .Build()) - .Build(); - - UnitOfWork.SeriesRepository.Add(series); - UnitOfWork.SeriesRepository.Add(series2); - await UnitOfWork.CommitAsync(); - - await ps.MergePeopleAsync(person2, person); - var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); - Assert.Single(allPeople); - - var mergedPerson = await UnitOfWork.PersonRepository.GetPersonById(person.Id, PersonIncludes.All); - Assert.NotNull(mergedPerson); - Assert.Equal(3, mergedPerson.ChapterPeople.Count); - Assert.Equal(3, mergedPerson.SeriesMetadataPeople.Count); - - chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(chapter.Id, ChapterIncludes.People); - Assert.NotNull(chapter); - Assert.Equal(2, chapter.People.Count); - Assert.Single(chapter.People.Select(p => p.Person.Id).Distinct()); - Assert.Contains(chapter.People, p => p.Role == PersonRole.Editor); - Assert.Contains(chapter.People, p => p.Role == PersonRole.Colorist); - - chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(chapter2.Id, ChapterIncludes.People); - Assert.NotNull(chapter2); - Assert.Single(chapter2.People); - Assert.Contains(chapter2.People, p => p.Role == PersonRole.Editor); - Assert.DoesNotContain(chapter2.People, p => p.Role == PersonRole.Colorist); - - series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Metadata); - Assert.NotNull(series); - Assert.Single(series.Metadata.People); - Assert.Contains(series.Metadata.People, p => p.Role == PersonRole.Editor); - Assert.DoesNotContain(series.Metadata.People, p => p.Role == PersonRole.Colorist); - - series2 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series2.Id, SeriesIncludes.Metadata); - Assert.NotNull(series2); - Assert.Equal(2, series2.Metadata.People.Count); - Assert.Contains(series2.Metadata.People, p => p.Role == PersonRole.Editor); - Assert.Contains(series2.Metadata.People, p => p.Role == PersonRole.Colorist); - - - } - - [Fact] - public async Task PersonAddAlias_NoOverlap() - { - await ResetDb(); - - UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jillian Cowan").Build()); - UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jilly Cowan").WithAlias("Jolly Cowan").Build()); - await UnitOfWork.CommitAsync(); - - var ps = new PersonService(UnitOfWork); - - var person1 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jillian Cowan"); - var person2 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jilly Cowan"); - Assert.NotNull(person1); - Assert.NotNull(person2); - - // Overlap on Name - var success = await ps.UpdatePersonAliasesAsync(person1, ["Jilly Cowan"]); - Assert.False(success); - - // Overlap on alias - success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan"]); - Assert.False(success); - - // No overlap - success = await ps.UpdatePersonAliasesAsync(person2, ["Jilly Joy Cowan"]); - Assert.True(success); - - // Some overlap - success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]); - Assert.False(success); - - // Some overlap - success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]); - Assert.False(success); - - Assert.Single(person2.Aliases); - } - - protected override async Task ResetDb() - { - Context.Person.RemoveRange(Context.Person.ToList()); - - await Context.SaveChangesAsync(); - } -} diff --git a/API.Tests/Services/ReadingProfileServiceTest.cs b/API.Tests/Services/ReadingProfileServiceTest.cs deleted file mode 100644 index b3d81e5ac..000000000 --- a/API.Tests/Services/ReadingProfileServiceTest.cs +++ /dev/null @@ -1,561 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Tests.Helpers; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; -using NSubstitute; -using Xunit; - -namespace API.Tests.Services; - -public class ReadingProfileServiceTest: AbstractDbTest -{ - - /// - /// Does not add a default reading profile - /// - /// - public async Task<(ReadingProfileService, AppUser, Library, Series)> Setup() - { - var user = new AppUserBuilder("amelia", "amelia@localhost").Build(); - Context.AppUser.Add(user); - await UnitOfWork.CommitAsync(); - - var series = new SeriesBuilder("Spice and Wolf").Build(); - - var library = new LibraryBuilder("Manga") - .WithSeries(series) - .Build(); - - user.Libraries.Add(library); - await UnitOfWork.CommitAsync(); - - var rps = new ReadingProfileService(UnitOfWork, Substitute.For(), Mapper); - user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.UserPreferences); - - return (rps, user, library, series); - } - - [Fact] - public async Task ImplicitProfileFirst() - { - await ResetDb(); - var (rps, user, library, series) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Implicit) - .WithSeries(series) - .WithName("Implicit Profile") - .Build(); - - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Non-implicit Profile") - .Build(); - - user.ReadingProfiles.Add(profile); - user.ReadingProfiles.Add(profile2); - await UnitOfWork.CommitAsync(); - - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfile); - Assert.Equal("Implicit Profile", seriesProfile.Name); - - // Find parent - seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id, true); - Assert.NotNull(seriesProfile); - Assert.Equal("Non-implicit Profile", seriesProfile.Name); - } - - [Fact] - public async Task CantDeleteDefaultReadingProfile() - { - await ResetDb(); - var (rps, user, _, _) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Default) - .Build(); - Context.AppUserReadingProfiles.Add(profile); - await UnitOfWork.CommitAsync(); - - await Assert.ThrowsAsync(async () => - { - await rps.DeleteReadingProfile(user.Id, profile.Id); - }); - - var profile2 = new AppUserReadingProfileBuilder(user.Id).Build(); - Context.AppUserReadingProfiles.Add(profile2); - await UnitOfWork.CommitAsync(); - - await rps.DeleteReadingProfile(user.Id, profile2.Id); - await UnitOfWork.CommitAsync(); - - var allProfiles = await Context.AppUserReadingProfiles.ToListAsync(); - Assert.Single(allProfiles); - } - - [Fact] - public async Task CreateImplicitSeriesReadingProfile() - { - await ResetDb(); - var (rps, user, _, series) = await Setup(); - - var dto = new UserReadingProfileDto - { - ReaderMode = ReaderMode.Webtoon, - ScalingOption = ScalingOption.FitToHeight, - WidthOverride = 53, - }; - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - - var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Contains(profile.SeriesIds, s => s == series.Id); - Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); - } - - [Fact] - public async Task UpdateImplicitReadingProfile_DoesNotCreateNew() - { - await ResetDb(); - var (rps, user, _, series) = await Setup(); - - var dto = new UserReadingProfileDto - { - ReaderMode = ReaderMode.Webtoon, - ScalingOption = ScalingOption.FitToHeight, - WidthOverride = 53, - }; - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - - var profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Contains(profile.SeriesIds, s => s == series.Id); - Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); - - dto = new UserReadingProfileDto - { - ReaderMode = ReaderMode.LeftRight, - }; - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, dto); - profile = await rps.GetReadingProfileForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Contains(profile.SeriesIds, s => s == series.Id); - Assert.Equal(ReadingProfileKind.Implicit, profile.Kind); - Assert.Equal(ReaderMode.LeftRight, profile.ReaderMode); - - var implicitCount = await Context.AppUserReadingProfiles - .Where(p => p.Kind == ReadingProfileKind.Implicit) - .CountAsync(); - Assert.Equal(1, implicitCount); - } - - [Fact] - public async Task GetCorrectProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Series Specific") - .Build(); - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithLibrary(lib) - .WithName("Library Specific") - .Build(); - var profile3 = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Default) - .WithName("Global") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - Context.AppUserReadingProfiles.Add(profile2); - Context.AppUserReadingProfiles.Add(profile3); - - var series2 = new SeriesBuilder("Rainbows After Storms").Build(); - lib.Series.Add(series2); - - var lib2 = new LibraryBuilder("Manga2").Build(); - var series3 = new SeriesBuilder("A Tropical Fish Yearns for Snow").Build(); - lib2.Series.Add(series3); - - user.Libraries.Add(lib2); - await UnitOfWork.CommitAsync(); - - var p = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(p); - Assert.Equal("Series Specific", p.Name); - - p = await rps.GetReadingProfileDtoForSeries(user.Id, series2.Id); - Assert.NotNull(p); - Assert.Equal("Library Specific", p.Name); - - p = await rps.GetReadingProfileDtoForSeries(user.Id, series3.Id); - Assert.NotNull(p); - Assert.Equal("Global", p.Name); - } - - [Fact] - public async Task ReplaceReadingProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var profile1 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile 1") - .Build(); - - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithName("Profile 2") - .Build(); - - Context.AppUserReadingProfiles.Add(profile1); - Context.AppUserReadingProfiles.Add(profile2); - await UnitOfWork.CommitAsync(); - - var profile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Equal("Profile 1", profile.Name); - - await rps.AddProfileToSeries(user.Id, profile2.Id, series.Id); - profile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(profile); - Assert.Equal("Profile 2", profile.Name); - } - - [Fact] - public async Task DeleteReadingProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var profile1 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile 1") - .Build(); - - Context.AppUserReadingProfiles.Add(profile1); - await UnitOfWork.CommitAsync(); - - await rps.ClearSeriesProfile(user.Id, series.Id); - var profiles = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); - Assert.DoesNotContain(profiles, rp => rp.SeriesIds.Contains(series.Id)); - - } - - [Fact] - public async Task BulkAddReadingProfiles() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - for (var i = 0; i < 10; i++) - { - var generatedSeries = new SeriesBuilder($"Generated Series #{i}").Build(); - lib.Series.Add(generatedSeries); - } - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - - var profile2 = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Profile2") - .Build(); - Context.AppUserReadingProfiles.Add(profile2); - - await UnitOfWork.CommitAsync(); - - var someSeriesIds = lib.Series.Take(lib.Series.Count / 2).Select(s => s.Id).ToList(); - await rps.BulkAddProfileToSeries(user.Id, profile.Id, someSeriesIds); - - foreach (var id in someSeriesIds) - { - var foundProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(foundProfile); - Assert.Equal(profile.Id, foundProfile.Id); - } - - var allIds = lib.Series.Select(s => s.Id).ToList(); - await rps.BulkAddProfileToSeries(user.Id, profile2.Id, allIds); - - foreach (var id in allIds) - { - var foundProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(foundProfile); - Assert.Equal(profile2.Id, foundProfile.Id); - } - - - } - - [Fact] - public async Task BulkAssignDeletesImplicit() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var implicitProfile = Mapper.Map(new AppUserReadingProfileBuilder(user.Id) - .Build()); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Profile 1") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - - for (var i = 0; i < 10; i++) - { - var generatedSeries = new SeriesBuilder($"Generated Series #{i}").Build(); - lib.Series.Add(generatedSeries); - } - await UnitOfWork.CommitAsync(); - - var ids = lib.Series.Select(s => s.Id).ToList(); - - foreach (var id in ids) - { - await rps.UpdateImplicitReadingProfile(user.Id, id, implicitProfile); - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind); - } - - await rps.BulkAddProfileToSeries(user.Id, profile.Id, ids); - - foreach (var id in ids) - { - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind); - } - - var implicitCount = await Context.AppUserReadingProfiles - .Where(p => p.Kind == ReadingProfileKind.Implicit) - .CountAsync(); - Assert.Equal(0, implicitCount); - } - - [Fact] - public async Task AddDeletesImplicit() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var implicitProfile = Mapper.Map(new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Implicit) - .Build()); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Profile 1") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - await UnitOfWork.CommitAsync(); - - await rps.UpdateImplicitReadingProfile(user.Id, series.Id, implicitProfile); - - var seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.Implicit, seriesProfile.Kind); - - await rps.AddProfileToSeries(user.Id, profile.Id, series.Id); - - seriesProfile = await rps.GetReadingProfileDtoForSeries(user.Id, series.Id); - Assert.NotNull(seriesProfile); - Assert.Equal(ReadingProfileKind.User, seriesProfile.Kind); - - var implicitCount = await Context.AppUserReadingProfiles - .Where(p => p.Kind == ReadingProfileKind.Implicit) - .CountAsync(); - Assert.Equal(0, implicitCount); - } - - [Fact] - public async Task CreateReadingProfile() - { - await ResetDb(); - var (rps, user, lib, series) = await Setup(); - - var dto = new UserReadingProfileDto - { - Name = "Profile 1", - ReaderMode = ReaderMode.LeftRight, - EmulateBook = false, - }; - - await rps.CreateReadingProfile(user.Id, dto); - - var dto2 = new UserReadingProfileDto - { - Name = "Profile 2", - ReaderMode = ReaderMode.LeftRight, - EmulateBook = false, - }; - - await rps.CreateReadingProfile(user.Id, dto2); - - var dto3 = new UserReadingProfileDto - { - Name = "Profile 1", // Not unique name - ReaderMode = ReaderMode.LeftRight, - EmulateBook = false, - }; - - await Assert.ThrowsAsync(async () => - { - await rps.CreateReadingProfile(user.Id, dto3); - }); - - var allProfiles = Context.AppUserReadingProfiles.ToList(); - Assert.Equal(2, allProfiles.Count); - } - - [Fact] - public async Task ClearSeriesProfile_RemovesImplicitAndUnlinksExplicit() - { - await ResetDb(); - var (rps, user, _, series) = await Setup(); - - var implicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithKind(ReadingProfileKind.Implicit) - .WithName("Implicit Profile") - .Build(); - - var explicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithSeries(series) - .WithName("Explicit Profile") - .Build(); - - Context.AppUserReadingProfiles.Add(implicitProfile); - Context.AppUserReadingProfiles.Add(explicitProfile); - await UnitOfWork.CommitAsync(); - - var allBefore = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); - Assert.Equal(2, allBefore.Count(rp => rp.SeriesIds.Contains(series.Id))); - - await rps.ClearSeriesProfile(user.Id, series.Id); - - var remainingProfiles = await Context.AppUserReadingProfiles.ToListAsync(); - Assert.Single(remainingProfiles); - Assert.Equal("Explicit Profile", remainingProfiles[0].Name); - Assert.Empty(remainingProfiles[0].SeriesIds); - - var profilesForSeries = await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id); - Assert.DoesNotContain(profilesForSeries, rp => rp.SeriesIds.Contains(series.Id)); - } - - [Fact] - public async Task AddProfileToLibrary_AddsAndOverridesExisting() - { - await ResetDb(); - var (rps, user, lib, _) = await Setup(); - - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Library Profile") - .Build(); - Context.AppUserReadingProfiles.Add(profile); - await UnitOfWork.CommitAsync(); - - await rps.AddProfileToLibrary(user.Id, profile.Id, lib.Id); - await UnitOfWork.CommitAsync(); - - var linkedProfile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.NotNull(linkedProfile); - Assert.Equal(profile.Id, linkedProfile.Id); - - var newProfile = new AppUserReadingProfileBuilder(user.Id) - .WithName("New Profile") - .Build(); - Context.AppUserReadingProfiles.Add(newProfile); - await UnitOfWork.CommitAsync(); - - await rps.AddProfileToLibrary(user.Id, newProfile.Id, lib.Id); - await UnitOfWork.CommitAsync(); - - linkedProfile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.NotNull(linkedProfile); - Assert.Equal(newProfile.Id, linkedProfile.Id); - } - - [Fact] - public async Task ClearLibraryProfile_RemovesImplicitOrUnlinksExplicit() - { - await ResetDb(); - var (rps, user, lib, _) = await Setup(); - - var implicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithKind(ReadingProfileKind.Implicit) - .WithLibrary(lib) - .Build(); - Context.AppUserReadingProfiles.Add(implicitProfile); - await UnitOfWork.CommitAsync(); - - await rps.ClearLibraryProfile(user.Id, lib.Id); - var profile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.Null(profile); - - var explicitProfile = new AppUserReadingProfileBuilder(user.Id) - .WithLibrary(lib) - .Build(); - Context.AppUserReadingProfiles.Add(explicitProfile); - await UnitOfWork.CommitAsync(); - - await rps.ClearLibraryProfile(user.Id, lib.Id); - profile = (await UnitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(user.Id)) - .FirstOrDefault(rp => rp.LibraryIds.Contains(lib.Id)); - Assert.Null(profile); - - var stillExists = await Context.AppUserReadingProfiles.FindAsync(explicitProfile.Id); - Assert.NotNull(stillExists); - } - - /// - /// As response to #3793, I'm not sure if we want to keep this. It's not the most nice. But I think the idea of this test - /// is worth having. - /// - [Fact] - public void UpdateFields_UpdatesAll() - { - // Repeat to ensure booleans are flipped and actually tested - for (int i = 0; i < 10; i++) - { - var profile = new AppUserReadingProfile(); - var dto = new UserReadingProfileDto(); - - RandfHelper.SetRandomValues(profile); - RandfHelper.SetRandomValues(dto); - - ReadingProfileService.UpdateReaderProfileFields(profile, dto); - - var newDto = Mapper.Map(profile); - - Assert.True(RandfHelper.AreSimpleFieldsEqual(dto, newDto, - ["k__BackingField", "k__BackingField"])); - } - } - - - - protected override async Task ResetDb() - { - Context.AppUserReadingProfiles.RemoveRange(Context.AppUserReadingProfiles); - await UnitOfWork.CommitAsync(); - } -} diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index c337d2311..2e812647b 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -483,7 +483,7 @@ public class ScannerServiceTests : AbstractDbTest var infos = new Dictionary(); var library = await _scannerHelper.GenerateScannerData(testcase, infos); - library.LibraryExcludePatterns = [new LibraryExcludePattern() { Pattern = "**/Extra/*" }]; + library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**/Extra/*"}]; UnitOfWork.LibraryRepository.Update(library); await UnitOfWork.CommitAsync(); @@ -507,7 +507,7 @@ public class ScannerServiceTests : AbstractDbTest var infos = new Dictionary(); var library = await _scannerHelper.GenerateScannerData(testcase, infos); - library.LibraryExcludePatterns = [new LibraryExcludePattern() { Pattern = "**\\Extra\\*" }]; + library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**\\Extra\\*"}]; UnitOfWork.LibraryRepository.Update(library); await UnitOfWork.CommitAsync(); @@ -938,61 +938,4 @@ public class ScannerServiceTests : AbstractDbTest Assert.True(sortedChapters[1].SortOrder.Is(4f)); Assert.True(sortedChapters[2].SortOrder.Is(5f)); } - - - [Fact] - public async Task ScanLibrary_MetadataDisabled_NoOverrides() - { - const string testcase = "Series with Localized No Metadata - Manga.json"; - - // Get the first file and generate a ComicInfo - var infos = new Dictionary(); - infos.Add("Immoral Guild v01.cbz", new ComicInfo() - { - Series = "Immoral Guild", - LocalizedSeries = "Futoku no Guild" // Filename has a capital N and localizedSeries has lowercase - }); - - var library = await _scannerHelper.GenerateScannerData(testcase, infos); - - // Disable metadata - library.EnableMetadata = false; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - // Validate that there are 2 series - Assert.NotNull(postLib); - Assert.Equal(2, postLib.Series.Count); - - Assert.Contains(postLib.Series, x => x.Name == "Immoral Guild"); - Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild"); - } - - [Fact] - public async Task ScanLibrary_SortName_NoPrefix() - { - const string testcase = "Series with Prefix - Book.json"; - - var library = await _scannerHelper.GenerateScannerData(testcase); - - library.RemovePrefixForSortName = true; - UnitOfWork.LibraryRepository.Update(library); - await UnitOfWork.CommitAsync(); - - var scanner = _scannerHelper.CreateServices(); - await scanner.ScanLibrary(library.Id); - - var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); - - Assert.NotNull(postLib); - Assert.Equal(1, postLib.Series.Count); - - Assert.Equal("The Avengers", postLib.Series.First().Name); - Assert.Equal("Avengers", postLib.Series.First().SortName); - } } diff --git a/API.Tests/Services/ScrobblingServiceTests.cs b/API.Tests/Services/ScrobblingServiceTests.cs index 9245c8ecd..50398a146 100644 --- a/API.Tests/Services/ScrobblingServiceTests.cs +++ b/API.Tests/Services/ScrobblingServiceTests.cs @@ -1,17 +1,11 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; +using System.Linq; using System.Threading.Tasks; -using API.Data.Repositories; using API.DTOs.Scrobbling; -using API.Entities; using API.Entities.Enums; -using API.Entities.Scrobble; using API.Helpers.Builders; using API.Services; using API.Services.Plus; using API.SignalR; -using Kavita.Common; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -21,33 +15,11 @@ namespace API.Tests.Services; public class ScrobblingServiceTests : AbstractDbTest { - private const int ChapterPages = 100; - - /// - /// { - /// "Issuer": "Issuer", - /// "Issued At": "2025-06-15T21:01:57.615Z", - /// "Expiration": "2200-06-15T21:01:57.615Z" - /// } - /// - /// Our UnitTests will fail in 2200 :( - private const string ValidJwtToken = - "eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJleHAiOjcyNzI0NTAxMTcsImlhdCI6MTc1MDAyMTMxN30.zADmcGq_BfxbcV8vy4xw5Cbzn4COkmVINxgqpuL17Ng"; - private readonly ScrobblingService _service; private readonly ILicenseService _licenseService; private readonly ILocalizationService _localizationService; private readonly ILogger _logger; private readonly IEmailService _emailService; - private readonly IKavitaPlusApiService _kavitaPlusApiService; - /// - /// IReaderService, without the ScrobblingService injected - /// - private readonly IReaderService _readerService; - /// - /// IReaderService, with the _service injected - /// - private readonly IReaderService _hookedUpReaderService; public ScrobblingServiceTests() { @@ -55,24 +27,8 @@ public class ScrobblingServiceTests : AbstractDbTest _localizationService = Substitute.For(); _logger = Substitute.For>(); _emailService = Substitute.For(); - _kavitaPlusApiService = Substitute.For(); - _service = new ScrobblingService(UnitOfWork, Substitute.For(), _logger, _licenseService, - _localizationService, _emailService, _kavitaPlusApiService); - - _readerService = new ReaderService(UnitOfWork, - Substitute.For>(), - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); // Do not use the actual one - - _hookedUpReaderService = new ReaderService(UnitOfWork, - Substitute.For>(), - Substitute.For(), - Substitute.For(), - Substitute.For(), - _service); + _service = new ScrobblingService(UnitOfWork, Substitute.For(), _logger, _licenseService, _localizationService, _emailService); } protected override async Task ResetDb() @@ -90,30 +46,6 @@ public class ScrobblingServiceTests : AbstractDbTest var series = new SeriesBuilder("Test Series") .WithFormat(MangaFormat.Archive) .WithMetadata(new SeriesMetadataBuilder().Build()) - .WithVolume(new VolumeBuilder("Volume 1") - .WithChapters([ - new ChapterBuilder("1") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("2") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("3") - .WithPages(ChapterPages) - .Build()]) - .Build()) - .WithVolume(new VolumeBuilder("Volume 2") - .WithChapters([ - new ChapterBuilder("4") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("5") - .WithPages(ChapterPages) - .Build(), - new ChapterBuilder("6") - .WithPages(ChapterPages) - .Build()]) - .Build()) .Build(); var library = new LibraryBuilder("Test Library", LibraryType.Manga) @@ -135,296 +67,6 @@ public class ScrobblingServiceTests : AbstractDbTest await UnitOfWork.CommitAsync(); } - private async Task CreateScrobbleEvent(int? seriesId = null) - { - var evt = new ScrobbleEvent - { - ScrobbleEventType = ScrobbleEventType.ChapterRead, - Format = PlusMediaFormat.Manga, - SeriesId = seriesId ?? 0, - LibraryId = 0, - AppUserId = 0, - }; - - if (seriesId != null) - { - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value); - if (series != null) evt.Series = series; - } - - return evt; - } - - - #region K+ API Request Tests - - [Fact] - public async Task PostScrobbleUpdate_AuthErrors() - { - _kavitaPlusApiService.PostScrobbleUpdate(null!, "") - .ReturnsForAnyArgs(new ScrobbleResponseDto() - { - ErrorMessage = "Unauthorized" - }); - - var evt = await CreateScrobbleEvent(); - await Assert.ThrowsAsync(async () => - { - await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); - }); - Assert.True(evt.IsErrored); - Assert.Equal("Kavita+ subscription no longer active", evt.ErrorDetails); - } - - [Fact] - public async Task PostScrobbleUpdate_UnknownSeriesLoggedAsError() - { - _kavitaPlusApiService.PostScrobbleUpdate(null!, "") - .ReturnsForAnyArgs(new ScrobbleResponseDto() - { - ErrorMessage = "Unknown Series" - }); - - await SeedData(); - var evt = await CreateScrobbleEvent(1); - - await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); - await UnitOfWork.CommitAsync(); - Assert.True(evt.IsErrored); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); - Assert.True(series.IsBlacklisted); - - var errors = await UnitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(1); - Assert.Single(errors); - Assert.Equal("Series cannot be matched for Scrobbling", errors.First().Comment); - Assert.Equal(series.Id, errors.First().SeriesId); - } - - [Fact] - public async Task PostScrobbleUpdate_InvalidAccessToken() - { - _kavitaPlusApiService.PostScrobbleUpdate(null!, "") - .ReturnsForAnyArgs(new ScrobbleResponseDto() - { - ErrorMessage = "Access token is invalid" - }); - - var evt = await CreateScrobbleEvent(); - - await Assert.ThrowsAsync(async () => - { - await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); - }); - - Assert.True(evt.IsErrored); - Assert.Equal("Access Token needs to be rotated to continue scrobbling", evt.ErrorDetails); - } - - #endregion - - #region K+ API Request data tests - - [Fact] - public async Task ProcessReadEvents_CreatesNoEventsWhenNoProgress() - { - await ResetDb(); - await SeedData(); - - // Set Returns - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - _kavitaPlusApiService.GetRateLimit(Arg.Any(), Arg.Any()) - .Returns(100); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - // Ensure CanProcessScrobbleEvent returns true - user.AniListAccessToken = ValidJwtToken; - UnitOfWork.UserRepository.Update(user); - await UnitOfWork.CommitAsync(); - - var chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(4); - Assert.NotNull(chapter); - - var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); - Assert.NotNull(volume); - - // Call Scrobble without having any progress - await _service.ScrobbleReadingUpdate(1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ProcessReadEvents_UpdateVolumeAndChapterData() - { - await ResetDb(); - await SeedData(); - - // Set Returns - _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); - _kavitaPlusApiService.GetRateLimit(Arg.Any(), Arg.Any()) - .Returns(100); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - // Ensure CanProcessScrobbleEvent returns true - user.AniListAccessToken = ValidJwtToken; - UnitOfWork.UserRepository.Update(user); - await UnitOfWork.CommitAsync(); - - var chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(4); - Assert.NotNull(chapter); - - var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); - Assert.NotNull(volume); - - // Mark something as read to trigger event creation - await _readerService.MarkChaptersAsRead(user, 1, new List() {volume.Chapters[0]}); - await UnitOfWork.CommitAsync(); - - // Call Scrobble while having some progress - await _service.ScrobbleReadingUpdate(user.Id, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - - // Give it some (more) read progress - await _readerService.MarkChaptersAsRead(user, 1, volume.Chapters); - await _readerService.MarkChaptersAsRead(user, 1, [chapter]); - await UnitOfWork.CommitAsync(); - - await _service.ProcessUpdatesSinceLastSync(); - - await _kavitaPlusApiService.Received(1).PostScrobbleUpdate( - Arg.Is(data => - data.ChapterNumber == (int)chapter.MaxNumber && - data.VolumeNumber == (int)volume.MaxNumber - ), - Arg.Any()); - } - - #endregion - - #region Scrobble Reading Update Tests - - [Fact] - public async Task ScrobbleReadingUpdate_IgnoreNoLicense() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(false); - - await _service.ScrobbleReadingUpdate(1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ScrobbleReadingUpdate_RemoveWhenNoProgress() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(true); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); - Assert.NotNull(volume); - - await _readerService.MarkChaptersAsRead(user, 1, new List() {volume.Chapters[0]}); - await UnitOfWork.CommitAsync(); - - await _service.ScrobbleReadingUpdate(1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - - var readEvent = events.First(); - Assert.False(readEvent.IsProcessed); - - await _hookedUpReaderService.MarkSeriesAsUnread(user, 1); - await UnitOfWork.CommitAsync(); - - // Existing event is deleted - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - - await _hookedUpReaderService.MarkSeriesAsUnread(user, 1); - await UnitOfWork.CommitAsync(); - - // No new events are added - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ScrobbleReadingUpdate_UpdateExistingNotIsProcessed() - { - await ResetDb(); - await SeedData(); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - var chapter1 = await UnitOfWork.ChapterRepository.GetChapterAsync(1); - var chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(2); - var chapter3 = await UnitOfWork.ChapterRepository.GetChapterAsync(3); - Assert.NotNull(chapter1); - Assert.NotNull(chapter2); - Assert.NotNull(chapter3); - - _licenseService.HasActiveLicense().Returns(true); - - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - - - await _readerService.MarkChaptersAsRead(user, 1, [chapter1]); - await UnitOfWork.CommitAsync(); - - // Scrobble update - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - - var readEvent = events[0]; - Assert.False(readEvent.IsProcessed); - Assert.Equal(1, readEvent.ChapterNumber); - - // Mark as processed - readEvent.IsProcessed = true; - await UnitOfWork.CommitAsync(); - - await _readerService.MarkChaptersAsRead(user, 1, [chapter2]); - await UnitOfWork.CommitAsync(); - - // Scrobble update - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Equal(2, events.Count); - Assert.Single(events.Where(e => e.IsProcessed).ToList()); - Assert.Single(events.Where(e => !e.IsProcessed).ToList()); - - // Should update the existing non processed event - await _readerService.MarkChaptersAsRead(user, 1, [chapter3]); - await UnitOfWork.CommitAsync(); - - // Scrobble update - await _service.ScrobbleReadingUpdate(1, 1); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Equal(2, events.Count); - Assert.Single(events.Where(e => e.IsProcessed).ToList()); - Assert.Single(events.Where(e => !e.IsProcessed).ToList()); - } - - #endregion - #region ScrobbleWantToReadUpdate Tests [Fact] @@ -561,59 +203,6 @@ public class ScrobblingServiceTests : AbstractDbTest #endregion - #region Scrobble Rating Update Test - - [Fact] - public async Task ScrobbleRatingUpdate_IgnoreNoLicense() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(false); - - await _service.ScrobbleRatingUpdate(1, 1, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Empty(events); - } - - [Fact] - public async Task ScrobbleRatingUpdate_UpdateExistingNotIsProcessed() - { - await ResetDb(); - await SeedData(); - - _licenseService.HasActiveLicense().Returns(true); - - var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); - Assert.NotNull(user); - - var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); - Assert.NotNull(series); - - await _service.ScrobbleRatingUpdate(user.Id, series.Id, 1); - var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events); - Assert.Equal(1, events.First().Rating); - - // Mark as processed - events.First().IsProcessed = true; - await UnitOfWork.CommitAsync(); - - await _service.ScrobbleRatingUpdate(user.Id, series.Id, 5); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Equal(2, events.Count); - Assert.Single(events, evt => evt.IsProcessed); - Assert.Single(events, evt => !evt.IsProcessed); - - await _service.ScrobbleRatingUpdate(user.Id, series.Id, 5); - events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); - Assert.Single(events, evt => !evt.IsProcessed); - Assert.Equal(5, events.First(evt => !evt.IsProcessed).Rating); - - } - - #endregion - [Theory] [InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)] [InlineData("https://anilist.co/manga/30105", 30105)] diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 55babf815..4bf0e6782 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -8,7 +8,6 @@ using API.Data; using API.Data.Repositories; using API.DTOs; using API.DTOs.Metadata; -using API.DTOs.Person; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json deleted file mode 100644 index d6e91183b..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "Immoral Guild/Immoral Guild v01.cbz", - "Immoral Guild/Immoral Guild v02.cbz", - "Immoral Guild/Futoku No Guild - Vol. 12 Ch. 67 - Take Responsibility.cbz" -] diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json deleted file mode 100644 index fc2bee18c..000000000 --- a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "The Avengers/The Avengers vol 1.pdf" -] \ No newline at end of file diff --git a/API/API.csproj b/API/API.csproj index a7d1177dc..1ddb37d7f 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -50,9 +50,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -62,45 +62,45 @@ - + - + - + - - - - - + + + + + - - - + + + - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + + @@ -111,16 +111,17 @@ + + - - - + + + - @@ -138,7 +139,6 @@ - @@ -188,6 +188,7 @@ + Always diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index d8b9164af..c504e1ce7 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -153,9 +153,6 @@ public class AccountController : BaseApiController // Assign default streams AddDefaultStreamsToUser(user); - // Assign default reading profile - await AddDefaultReadingProfileToUser(user); - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen")); if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token)); @@ -612,7 +609,7 @@ public class AccountController : BaseApiController } /// - /// Requests the Invite Url for the AppUserId. Will return error if user is already validated. + /// Requests the Invite Url for the UserId. Will return error if user is already validated. /// /// /// Include the "https://ip:port/" in the generated link @@ -672,9 +669,6 @@ public class AccountController : BaseApiController // Assign default streams AddDefaultStreamsToUser(user); - // Assign default reading profile - await AddDefaultReadingProfileToUser(user); - // Assign Roles var roles = dto.Roles; var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole); @@ -785,16 +779,6 @@ public class AccountController : BaseApiController } } - private async Task AddDefaultReadingProfileToUser(AppUser user) - { - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Default Profile") - .WithKind(ReadingProfileKind.Default) - .Build(); - _unitOfWork.AppUserReadingProfileRepository.Add(profile); - await _unitOfWork.CommitAsync(); - } - /// /// Last step in authentication flow, confirms the email token for email /// diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index e1d7da9e8..251811346 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -82,6 +82,7 @@ public class BookController : BaseApiController SeriesFormat = dto.SeriesFormat, SeriesId = dto.SeriesId, LibraryId = dto.LibraryId, + LibraryType = dto.LibraryType, IsSpecial = dto.IsSpecial, Pages = dto.Pages, }); diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index 94535d499..8de26cf97 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -9,7 +9,6 @@ using API.DTOs; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.MetadataMatching; using API.Entities.Person; using API.Extensions; using API.Helpers; @@ -209,7 +208,6 @@ public class ChapterController : BaseApiController if (chapter.AgeRating != dto.AgeRating) { chapter.AgeRating = dto.AgeRating; - chapter.KPlusOverrides.Remove(MetadataSettingField.AgeRating); } dto.Summary ??= string.Empty; @@ -217,7 +215,6 @@ public class ChapterController : BaseApiController if (chapter.Summary != dto.Summary.Trim()) { chapter.Summary = dto.Summary.Trim(); - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterSummary); } if (chapter.Language != dto.Language) @@ -233,13 +230,11 @@ public class ChapterController : BaseApiController if (chapter.TitleName != dto.TitleName) { chapter.TitleName = dto.TitleName; - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterTitle); } if (chapter.ReleaseDate != dto.ReleaseDate) { chapter.ReleaseDate = dto.ReleaseDate; - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterReleaseDate); } if (!string.IsNullOrEmpty(dto.ISBN) && ArticleNumberHelper.IsValidIsbn10(dto.ISBN) || @@ -338,8 +333,6 @@ public class ChapterController : BaseApiController _unitOfWork ); - // TODO: Only remove field if changes were made - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterPublisher); // Update publishers await PersonHelper.UpdateChapterPeopleAsync( chapter, diff --git a/API/Controllers/KoreaderController.cs b/API/Controllers/KoreaderController.cs deleted file mode 100644 index 8c4c41585..000000000 --- a/API/Controllers/KoreaderController.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Koreader; -using API.Entities; -using API.Extensions; -using API.Services; -using Kavita.Common; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using static System.Net.WebRequestMethods; - -namespace API.Controllers; -#nullable enable - -/// -/// The endpoint to interface with Koreader's Progress Sync plugin. -/// -/// -/// Koreader uses a different form of authentication. It stores the username and password in headers. -/// https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua -/// -[AllowAnonymous] -public class KoreaderController : BaseApiController -{ - - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IKoreaderService _koreaderService; - private readonly ILogger _logger; - - public KoreaderController(IUnitOfWork unitOfWork, ILocalizationService localizationService, - IKoreaderService koreaderService, ILogger logger) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _koreaderService = koreaderService; - _logger = logger; - } - - // We won't allow users to be created from Koreader. Rather, they - // must already have an account. - /* - [HttpPost("/users/create")] - public IActionResult CreateUser(CreateUserRequest request) - { - } - */ - - [HttpGet("{apiKey}/users/auth")] - public async Task Authenticate(string apiKey) - { - var userId = await GetUserId(apiKey); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null) return Unauthorized(); - - return Ok(new { username = user.UserName }); - } - - /// - /// Syncs book progress with Kavita. Will attempt to save the underlying reader position if possible. - /// - /// - /// - /// - [HttpPut("{apiKey}/syncs/progress")] - public async Task> UpdateProgress(string apiKey, KoreaderBookDto request) - { - try - { - var userId = await GetUserId(apiKey); - await _koreaderService.SaveProgress(request, userId); - - return Ok(new KoreaderProgressUpdateDto{ Document = request.Document, Timestamp = DateTime.UtcNow }); - } - catch (KavitaException ex) - { - return BadRequest(ex.Message); - } - } - - /// - /// Gets book progress from Kavita, if not found will return a 400 - /// - /// - /// - /// - [HttpGet("{apiKey}/syncs/progress/{ebookHash}")] - public async Task> GetProgress(string apiKey, string ebookHash) - { - try - { - var userId = await GetUserId(apiKey); - var response = await _koreaderService.GetProgress(ebookHash, userId); - _logger.LogDebug("Koreader response progress for User ({UserId}): {Progress}", userId, response.Progress.Sanitize()); - - return Ok(response); - } - catch (KavitaException ex) - { - return BadRequest(ex.Message); - } - } - - private async Task GetUserId(string apiKey) - { - try - { - return await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); - } - catch - { - throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist")); - } - } -} diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 8f9b18317..dbd809cee 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -623,9 +624,6 @@ public class LibraryController : BaseApiController library.ManageReadingLists = dto.ManageReadingLists; library.AllowScrobbling = dto.AllowScrobbling; library.AllowMetadataMatching = dto.AllowMetadataMatching; - library.EnableMetadata = dto.EnableMetadata; - library.RemovePrefixForSortName = dto.RemovePrefixForSortName; - library.LibraryFileTypes = dto.FileGroupTypes .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) .Distinct() @@ -657,4 +655,14 @@ public class LibraryController : BaseApiController { return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); } + + /// + /// Return pairs of all types + /// + /// + [HttpGet("types")] + public async Task>> GetLibraryTypes() + { + return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypesAsync(User.GetUserId())); + } } diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index cab33692a..b08ac1f38 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -9,8 +9,6 @@ using API.Data.Repositories; using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.DTOs.Person; using API.DTOs.Recommendation; using API.DTOs.SeriesDetail; using API.Entities.Enums; @@ -48,22 +46,6 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context)); } - /// - /// Returns a list of Genres with counts for counts when Genre is on Series/Chapter - /// - /// - [HttpPost("genres-with-counts")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] - public async Task>> GetBrowseGenres(UserParams? userParams = null) - { - userParams ??= UserParams.Default; - - var list = await unitOfWork.GenreRepository.GetBrowseableGenre(User.GetUserId(), userParams); - Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); - - return Ok(list); - } - /// /// Fetches people from the instance by role /// @@ -92,7 +74,6 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc { return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId(), ids)); } - return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId())); } @@ -113,22 +94,6 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId())); } - /// - /// Returns a list of Tags with counts for counts when Tag is on Series/Chapter - /// - /// - [HttpPost("tags-with-counts")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] - public async Task>> GetBrowseTags(UserParams? userParams = null) - { - userParams ??= UserParams.Default; - - var list = await unitOfWork.TagRepository.GetBrowseableTag(User.GetUserId(), userParams); - Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); - - return Ok(list); - } - /// /// Fetches all age ratings from the instance /// diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 6e96c3063..fcc4ca58f 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -15,7 +15,6 @@ using API.DTOs.CollectionTags; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.OPDS; -using API.DTOs.Person; using API.DTOs.Progress; using API.DTOs.Search; using API.Entities; diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs index 7328ff954..1094a1137 100644 --- a/API/Controllers/PersonController.cs +++ b/API/Controllers/PersonController.cs @@ -1,13 +1,7 @@ using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using API.Data; -using API.Data.Repositories; using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; using API.Entities.Enums; using API.Extensions; using API.Helpers; @@ -30,10 +24,9 @@ public class PersonController : BaseApiController private readonly ICoverDbService _coverDbService; private readonly IImageService _imageService; private readonly IEventHub _eventHub; - private readonly IPersonService _personService; public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper, - ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService) + ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub) { _unitOfWork = unitOfWork; _localizationService = localizationService; @@ -41,7 +34,6 @@ public class PersonController : BaseApiController _coverDbService = coverDbService; _imageService = imageService; _eventHub = eventHub; - _personService = personService; } @@ -51,17 +43,6 @@ public class PersonController : BaseApiController return Ok(await _unitOfWork.PersonRepository.GetPersonDtoByName(name, User.GetUserId())); } - /// - /// Find a person by name or alias against a query string - /// - /// - /// - [HttpGet("search")] - public async Task>> SearchPeople([FromQuery] string queryString) - { - return Ok(await _unitOfWork.PersonRepository.SearchPeople(queryString)); - } - /// /// Returns all roles for a Person /// @@ -73,20 +54,17 @@ public class PersonController : BaseApiController return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(personId, User.GetUserId())); } - /// /// Returns a list of authors and artists for browsing /// /// /// [HttpPost("all")] - public async Task>> GetPeopleForBrowse(BrowsePersonFilterDto filter, [FromQuery] UserParams? userParams) + public async Task>> GetAuthorsForBrowse([FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - - var list = await _unitOfWork.PersonRepository.GetBrowsePersonDtos(User.GetUserId(), filter, userParams); + var list = await _unitOfWork.PersonRepository.GetAllWritersAndSeriesCount(User.GetUserId(), userParams); Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); - return Ok(list); } @@ -100,7 +78,7 @@ public class PersonController : BaseApiController public async Task> UpdatePerson(UpdatePersonDto dto) { // This needs to get all people and update them equally - var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); + var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id); if (person == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-required")); @@ -112,12 +90,7 @@ public class PersonController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-unique")); } - var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases); - if (!success) return BadRequest(await _localizationService.Translate(User.GetUserId(), "aliases-have-overlap")); - - person.Name = dto.Name?.Trim(); - person.NormalizedName = person.Name.ToNormalized(); person.Description = dto.Description ?? string.Empty; person.CoverImageLocked = dto.CoverImageLocked; @@ -185,7 +158,7 @@ public class PersonController : BaseApiController [HttpGet("series-known-for")] public async Task>> GetKnownSeries(int personId) { - return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId, User.GetUserId())); + return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId)); } /// @@ -200,42 +173,5 @@ public class PersonController : BaseApiController return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role)); } - /// - /// Merges Persons into one, this action is irreversible - /// - /// - /// - [HttpPost("merge")] - [Authorize("RequireAdminRole")] - public async Task> MergePeople(PersonMergeDto dto) - { - var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); - if (dst == null) return BadRequest(); - - var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); - if (src == null) return BadRequest(); - - await _personService.MergePeopleAsync(src, dst); - await _eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); - - return Ok(_mapper.Map(dst)); - } - - /// - /// Ensure the alias is valid to be added. For example, the alias cannot be on another person or be the same as the current person name/alias. - /// - /// - /// - /// - [HttpGet("valid-alias")] - public async Task> IsValidAlias(int personId, string alias) - { - var person = await _unitOfWork.PersonRepository.GetPersonById(personId, PersonIncludes.Aliases); - if (person == null) return NotFound(); - - var existingAlias = await _unitOfWork.PersonRepository.AnyAliasExist(alias); - return Ok(!existingAlias && person.NormalizedName != alias.ToNormalized()); - } - } diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index f39462bbf..c7f48cf54 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -45,7 +45,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService throw new KavitaUnauthenticatedUserException(); } var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); - logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({AppUserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId); + logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId); return new UserDto { diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 38a5ad482..207dbabb5 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -246,6 +246,7 @@ public class ReaderController : BaseApiController SeriesFormat = dto.SeriesFormat, SeriesId = dto.SeriesId, LibraryId = dto.LibraryId, + LibraryType = dto.LibraryType, IsSpecial = dto.IsSpecial, Pages = dto.Pages, SeriesTotalPages = series.Pages, @@ -287,6 +288,7 @@ public class ReaderController : BaseApiController return Ok(info); } + /// /// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading. /// diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 1187992bc..6c9be6c75 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.Data.Repositories; -using API.DTOs.Person; +using API.DTOs; using API.DTOs.ReadingLists; using API.Entities.Enums; using API.Extensions; diff --git a/API/Controllers/ReadingProfileController.cs b/API/Controllers/ReadingProfileController.cs deleted file mode 100644 index bc1b4fa52..000000000 --- a/API/Controllers/ReadingProfileController.cs +++ /dev/null @@ -1,198 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Extensions; -using API.Services; -using AutoMapper; -using Kavita.Common; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace API.Controllers; - -[Route("api/reading-profile")] -public class ReadingProfileController(ILogger logger, IUnitOfWork unitOfWork, - IReadingProfileService readingProfileService): BaseApiController -{ - - /// - /// Gets all non-implicit reading profiles for a user - /// - /// - [HttpGet("all")] - public async Task>> GetAllReadingProfiles() - { - return Ok(await unitOfWork.AppUserReadingProfileRepository.GetProfilesDtoForUser(User.GetUserId(), true)); - } - - /// - /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. - /// Series -> Library -> Default - /// - /// - /// - /// - [HttpGet("{seriesId:int}")] - public async Task> GetProfileForSeries(int seriesId, [FromQuery] bool skipImplicit) - { - return Ok(await readingProfileService.GetReadingProfileDtoForSeries(User.GetUserId(), seriesId, skipImplicit)); - } - - /// - /// Returns the (potential) Reading Profile bound to the library - /// - /// - /// - [HttpGet("library")] - public async Task> GetProfileForLibrary(int libraryId) - { - return Ok(await readingProfileService.GetReadingProfileDtoForLibrary(User.GetUserId(), libraryId)); - } - - /// - /// Creates a new reading profile for the current user - /// - /// - /// - [HttpPost("create")] - public async Task> CreateReadingProfile([FromBody] UserReadingProfileDto dto) - { - return Ok(await readingProfileService.CreateReadingProfile(User.GetUserId(), dto)); - } - - /// - /// Promotes the implicit profile to a user profile. Removes the series from other profiles - /// - /// - /// - [HttpPost("promote")] - public async Task> PromoteImplicitReadingProfile([FromQuery] int profileId) - { - return Ok(await readingProfileService.PromoteImplicitProfile(User.GetUserId(), profileId)); - } - - /// - /// Update the implicit reading profile for a series, creates one if none exists - /// - /// Any modification to the reader settings during reading will create an implicit profile. Use "update-parent" to save to the bound series profile. - /// - /// - /// - [HttpPost("series")] - public async Task> UpdateReadingProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId) - { - var updatedProfile = await readingProfileService.UpdateImplicitReadingProfile(User.GetUserId(), seriesId, dto); - return Ok(updatedProfile); - } - - /// - /// Updates the non-implicit reading profile for the given series, and removes implicit profiles - /// - /// - /// - /// - [HttpPost("update-parent")] - public async Task> UpdateParentProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId) - { - var newParentProfile = await readingProfileService.UpdateParent(User.GetUserId(), seriesId, dto); - return Ok(newParentProfile); - } - - /// - /// Updates the given reading profile, must belong to the current user - /// - /// - /// The updated reading profile - /// - /// This does not update connected series and libraries. - /// - [HttpPost] - public async Task> UpdateReadingProfile(UserReadingProfileDto dto) - { - return Ok(await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto)); - } - - /// - /// Deletes the given profile, requires the profile to belong to the logged-in user - /// - /// - /// - /// - /// - [HttpDelete] - public async Task DeleteReadingProfile([FromQuery] int profileId) - { - await readingProfileService.DeleteReadingProfile(User.GetUserId(), profileId); - return Ok(); - } - - /// - /// Sets the reading profile for a given series, removes the old one - /// - /// - /// - /// - [HttpPost("series/{seriesId:int}")] - public async Task AddProfileToSeries(int seriesId, [FromQuery] int profileId) - { - await readingProfileService.AddProfileToSeries(User.GetUserId(), profileId, seriesId); - return Ok(); - } - - /// - /// Clears the reading profile for the given series for the currently logged-in user - /// - /// - /// - [HttpDelete("series/{seriesId:int}")] - public async Task ClearSeriesProfile(int seriesId) - { - await readingProfileService.ClearSeriesProfile(User.GetUserId(), seriesId); - return Ok(); - } - - /// - /// Sets the reading profile for a given library, removes the old one - /// - /// - /// - /// - [HttpPost("library/{libraryId:int}")] - public async Task AddProfileToLibrary(int libraryId, [FromQuery] int profileId) - { - await readingProfileService.AddProfileToLibrary(User.GetUserId(), profileId, libraryId); - return Ok(); - } - - /// - /// Clears the reading profile for the given library for the currently logged-in user - /// - /// - /// - /// - [HttpDelete("library/{libraryId:int}")] - public async Task ClearLibraryProfile(int libraryId) - { - await readingProfileService.ClearLibraryProfile(User.GetUserId(), libraryId); - return Ok(); - } - - /// - /// Assigns the reading profile to all passes series, and deletes their implicit profiles - /// - /// - /// - /// - [HttpPost("bulk")] - public async Task BulkAddReadingProfile([FromQuery] int profileId, [FromBody] IList seriesIds) - { - await readingProfileService.BulkAddProfileToSeries(User.GetUserId(), profileId, seriesIds); - return Ok(); - } - -} diff --git a/API/Controllers/ScrobblingController.cs b/API/Controllers/ScrobblingController.cs index 986f4f8e7..3904cb8e0 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/API/Controllers/ScrobblingController.cs @@ -254,7 +254,7 @@ public class ScrobblingController : BaseApiController } /// - /// Remove a hold against the Series for user's scrobbling + /// Adds a hold against the Series for user's scrobbling /// /// /// @@ -281,18 +281,4 @@ public class ScrobblingController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); return Ok(user is {HasRunScrobbleEventGeneration: true}); } - - /// - /// Delete the given scrobble events if they belong to that user - /// - /// - /// - [HttpPost("bulk-remove-events")] - public async Task BulkRemoveScrobbleEvents(IList eventIds) - { - var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(User.GetUserId(), eventIds); - _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); - return Ok(); - } } diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index cc89a124e..5aa54d1db 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -63,7 +63,6 @@ public class SearchController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); - var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList(); if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted")); diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 389ff33a7..7cd897c32 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -14,7 +14,6 @@ using API.DTOs.Recommendation; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.MetadataMatching; using API.Extensions; using API.Helpers; using API.Services; @@ -225,7 +224,6 @@ public class SeriesController : BaseApiController needsRefreshMetadata = true; series.CoverImage = null; series.CoverImageLocked = false; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers); _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); series.ResetColorScape(); @@ -312,7 +310,7 @@ public class SeriesController : BaseApiController /// /// /// - /// This is not in use + /// /// [HttpPost("all-v2")] public async Task>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams, @@ -323,6 +321,8 @@ public class SeriesController : BaseApiController await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto, context); // Apply progress/rating information (I can't work out how to do this in initial query) + if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series")); + await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index 9652ba494..4b935a1bf 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -6,7 +6,6 @@ using API.Data; using API.Data.Repositories; using API.DTOs.Uploads; using API.Entities.Enums; -using API.Entities.MetadataMatching; using API.Extensions; using API.Services; using API.Services.Tasks.Metadata; @@ -113,10 +112,8 @@ public class UploadController : BaseApiController series.CoverImage = filePath; series.CoverImageLocked = lockState; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers); _imageService.UpdateColorScape(series); _unitOfWork.SeriesRepository.Update(series); - _unitOfWork.SeriesRepository.Update(series.Metadata); if (_unitOfWork.HasChanges()) { @@ -280,7 +277,6 @@ public class UploadController : BaseApiController chapter.CoverImage = filePath; chapter.CoverImageLocked = lockState; - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterCovers); _unitOfWork.ChapterRepository.Update(chapter); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId); if (volume != null) diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 17ebc758e..944ea987b 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -103,13 +103,38 @@ public class UsersController : BaseApiController var existingPreferences = user!.UserPreferences; + existingPreferences.ReadingDirection = preferencesDto.ReadingDirection; + existingPreferences.ScalingOption = preferencesDto.ScalingOption; + existingPreferences.PageSplitOption = preferencesDto.PageSplitOption; + existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu; + existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints; + existingPreferences.EmulateBook = preferencesDto.EmulateBook; + existingPreferences.ReaderMode = preferencesDto.ReaderMode; + existingPreferences.LayoutMode = preferencesDto.LayoutMode; + existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor; + existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin; + existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing; + existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily; + existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize; + existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate; + existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection; + existingPreferences.BookReaderWritingStyle = preferencesDto.BookReaderWritingStyle; + existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName; + existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode; + existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode; existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode; existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries; + existingPreferences.LayoutMode = preferencesDto.LayoutMode; existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize; existingPreferences.NoTransitions = preferencesDto.NoTransitions; + existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate; existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships; existingPreferences.ShareReviews = preferencesDto.ShareReviews; + existingPreferences.PdfTheme = preferencesDto.PdfTheme; + existingPreferences.PdfScrollMode = preferencesDto.PdfScrollMode; + existingPreferences.PdfSpreadMode = preferencesDto.PdfSpreadMode; + if (await _licenseService.HasActiveLicense()) { existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled; diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 85624b51c..70fb12e85 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using API.DTOs.Metadata; -using API.DTOs.Person; using API.Entities.Enums; using API.Entities.Interfaces; diff --git a/API/DTOs/Filtering/PersonSortField.cs b/API/DTOs/Filtering/PersonSortField.cs deleted file mode 100644 index 5268a1bf9..000000000 --- a/API/DTOs/Filtering/PersonSortField.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace API.DTOs.Filtering; - -public enum PersonSortField -{ - Name = 1, - SeriesCount = 2, - ChapterCount = 3 -} diff --git a/API/DTOs/Filtering/SortOptions.cs b/API/DTOs/Filtering/SortOptions.cs index 18f2b17ea..a08e2968e 100644 --- a/API/DTOs/Filtering/SortOptions.cs +++ b/API/DTOs/Filtering/SortOptions.cs @@ -8,12 +8,3 @@ public sealed record SortOptions public SortField SortField { get; set; } public bool IsAscending { get; set; } = true; } - -/// -/// All Sorting Options for a query related to Person Entity -/// -public sealed record PersonSortOptions -{ - public PersonSortField SortField { get; set; } - public bool IsAscending { get; set; } = true; -} diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs index 246a92a90..5323f2b48 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/API/DTOs/Filtering/v2/FilterField.cs @@ -56,12 +56,5 @@ public enum FilterField /// Last time User Read /// ReadLast = 32, -} -public enum PersonFilterField -{ - Role = 1, - Name = 2, - SeriesCount = 3, - ChapterCount = 4, } diff --git a/API/DTOs/Filtering/v2/FilterStatementDto.cs b/API/DTOs/Filtering/v2/FilterStatementDto.cs index 8c99bd24c..ebe6d16af 100644 --- a/API/DTOs/Filtering/v2/FilterStatementDto.cs +++ b/API/DTOs/Filtering/v2/FilterStatementDto.cs @@ -1,6 +1,4 @@ -using API.DTOs.Metadata.Browse.Requests; - -namespace API.DTOs.Filtering.v2; +namespace API.DTOs.Filtering.v2; public sealed record FilterStatementDto { @@ -8,10 +6,3 @@ public sealed record FilterStatementDto public FilterField Field { get; set; } public string Value { get; set; } } - -public sealed record PersonFilterStatementDto -{ - public FilterComparison Comparison { get; set; } - public PersonFilterField Field { get; set; } - public string Value { get; set; } -} diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/API/DTOs/Filtering/v2/FilterV2Dto.cs index a247a17a6..11dc42a6b 100644 --- a/API/DTOs/Filtering/v2/FilterV2Dto.cs +++ b/API/DTOs/Filtering/v2/FilterV2Dto.cs @@ -16,7 +16,7 @@ public sealed record FilterV2Dto /// The name of the filter /// public string? Name { get; set; } - public ICollection Statements { get; set; } = []; + public ICollection Statements { get; set; } = new List(); public FilterCombination Combination { get; set; } = FilterCombination.And; public SortOptions? SortOptions { get; set; } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index c05ff0567..2b7dea8e6 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.KavitaPlus.ExternalMetadata; /// /// Used for matching and fetching metadata on a series /// -public sealed record ExternalMetadataIdsDto +internal sealed record ExternalMetadataIdsDto { public long? MalId { get; set; } public int? AniListId { get; set; } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index a7359d69b..6cd911700 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -4,18 +4,14 @@ using API.DTOs.Scrobbling; namespace API.DTOs.KavitaPlus.ExternalMetadata; #nullable enable -/// -/// Represents a request to match some series from Kavita to an external id which K+ uses. -/// -public sealed record MatchSeriesRequestDto +internal sealed record MatchSeriesRequestDto { - public required string SeriesName { get; set; } - public ICollection AlternativeNames { get; set; } = []; + public string SeriesName { get; set; } + public ICollection AlternativeNames { get; set; } public int Year { get; set; } = 0; - public string? Query { get; set; } + public string Query { get; set; } public int? AniListId { get; set; } public long? MalId { get; set; } public string? HardcoverId { get; set; } - public int? CbrId { get; set; } public PlusMediaFormat Format { get; set; } } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs index 84e9bbf3e..d0cbb7bd3 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs @@ -6,7 +6,7 @@ using API.DTOs.SeriesDetail; namespace API.DTOs.KavitaPlus.ExternalMetadata; -public sealed record SeriesDetailPlusApiDto +internal sealed record SeriesDetailPlusApiDto { public IEnumerable Recommendations { get; set; } public IEnumerable Reviews { get; set; } diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs index c394cf8d4..8eb38c98a 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs +++ b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs @@ -15,9 +15,5 @@ public enum MatchStateOption public sealed record ManageMatchFilterDto { public MatchStateOption MatchStateOption { get; set; } = MatchStateOption.All; - /// - /// Library Type in int form. -1 indicates to ignore the field. - /// - public int LibraryType { get; set; } = -1; public string SearchTerm { get; set; } = string.Empty; } diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs index add9ca723..1dcd8494c 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using API.DTOs.SeriesDetail; namespace API.DTOs.KavitaPlus.Metadata; -#nullable enable /// /// Information about an individual issue/chapter/book from Kavita+ diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs index 6704bf697..a3cd378b2 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs @@ -29,9 +29,7 @@ public sealed record ExternalSeriesDetailDto public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public int AverageScore { get; set; } - /// AniList returns the total count of unique chapters, includes 1.1 for example public int Chapters { get; set; } - /// AniList returns the total count of unique volumes, includes 1.1 for example public int Volumes { get; set; } public IList? Relations { get; set; } = []; public IList? Characters { get; set; } = []; diff --git a/API/DTOs/Koreader/KoreaderBookDto.cs b/API/DTOs/Koreader/KoreaderBookDto.cs deleted file mode 100644 index b66b7da3a..000000000 --- a/API/DTOs/Koreader/KoreaderBookDto.cs +++ /dev/null @@ -1,33 +0,0 @@ -using API.DTOs.Progress; - -namespace API.DTOs.Koreader; - -/// -/// This is the interface for receiving and sending updates to Koreader. The only fields -/// that are actually used are the Document and Progress fields. -/// -public class KoreaderBookDto -{ - /// - /// This is the Koreader hash of the book. It is used to identify the book. - /// - public string Document { get; set; } - /// - /// A randomly generated id from the koreader device. Only used to maintain the Koreader interface. - /// - public string Device_id { get; set; } - /// - /// The Koreader device name. Only used to maintain the Koreader interface. - /// - public string Device { get; set; } - /// - /// Percent progress of the book. Only used to maintain the Koreader interface. - /// - public float Percentage { get; set; } - /// - /// An XPath string read by Koreader to determine the location within the epub. - /// Essentially, it is Koreader's equivalent to ProgressDto.BookScrollId. - /// - /// - public string Progress { get; set; } -} diff --git a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs b/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs deleted file mode 100644 index 52a1d6cbd..000000000 --- a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace API.DTOs.Koreader; - -public class KoreaderProgressUpdateDto -{ - /// - /// This is the Koreader hash of the book. It is used to identify the book. - /// - public string Document { get; set; } - /// - /// UTC Timestamp to return to KOReader - /// - public DateTime Timestamp { get; set; } -} diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index bd72ad2f0..8ba687346 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -66,12 +66,4 @@ public sealed record LibraryDto /// This does not exclude the library from being linked to wrt Series Relationships /// Requires a valid LicenseKey public bool AllowMetadataMatching { get; set; } = true; - /// - /// Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF) - /// - public bool EnableMetadata { get; set; } = true; - /// - /// Should Kavita remove sort articles "The" for the sort name - /// - public bool RemovePrefixForSortName { get; set; } = false; } diff --git a/API/DTOs/LibraryTypeDto.cs b/API/DTOs/LibraryTypeDto.cs new file mode 100644 index 000000000..9f448e7b7 --- /dev/null +++ b/API/DTOs/LibraryTypeDto.cs @@ -0,0 +1,12 @@ +using API.Entities.Enums; + +namespace API.DTOs; + +/// +/// Simple pairing of LibraryId and LibraryType +/// +public sealed record LibraryTypeDto +{ + public int LibraryId { get; set; } + public LibraryType LibraryType { get; set; } +} diff --git a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs b/API/DTOs/Metadata/Browse/BrowseGenreDto.cs deleted file mode 100644 index 8044c7914..000000000 --- a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace API.DTOs.Metadata.Browse; - -public sealed record BrowseGenreDto : GenreTagDto -{ - /// - /// Number of Series this Entity is on - /// - public int SeriesCount { get; set; } - /// - /// Number of Chapters this Entity is on - /// - public int ChapterCount { get; set; } -} diff --git a/API/DTOs/Metadata/Browse/BrowseTagDto.cs b/API/DTOs/Metadata/Browse/BrowseTagDto.cs deleted file mode 100644 index 9a71876e3..000000000 --- a/API/DTOs/Metadata/Browse/BrowseTagDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace API.DTOs.Metadata.Browse; - -public sealed record BrowseTagDto : TagDto -{ - /// - /// Number of Series this Entity is on - /// - public int SeriesCount { get; set; } - /// - /// Number of Chapters this Entity is on - /// - public int ChapterCount { get; set; } -} diff --git a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs b/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs deleted file mode 100644 index d41cf37f3..000000000 --- a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; - -namespace API.DTOs.Metadata.Browse.Requests; -#nullable enable - -public sealed record BrowsePersonFilterDto -{ - /// - /// Not used - For parity with Series Filter - /// - public int Id { get; set; } - /// - /// Not used - For parity with Series Filter - /// - public string? Name { get; set; } - public ICollection Statements { get; set; } = []; - public FilterCombination Combination { get; set; } = FilterCombination.And; - public PersonSortOptions? SortOptions { get; set; } - - /// - /// Limit the number of rows returned. Defaults to not applying a limit (aka 0) - /// - public int LimitTo { get; set; } = 0; -} diff --git a/API/DTOs/Metadata/ChapterMetadataDto.cs b/API/DTOs/Metadata/ChapterMetadataDto.cs index c79436e24..1adc52cd1 100644 --- a/API/DTOs/Metadata/ChapterMetadataDto.cs +++ b/API/DTOs/Metadata/ChapterMetadataDto.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using API.DTOs.Person; using API.Entities.Enums; namespace API.DTOs.Metadata; diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/API/DTOs/Metadata/GenreTagDto.cs index 13a339d38..4846048d2 100644 --- a/API/DTOs/Metadata/GenreTagDto.cs +++ b/API/DTOs/Metadata/GenreTagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public record GenreTagDto +public sealed record GenreTagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/TagDto.cs b/API/DTOs/Metadata/TagDto.cs index f5c925e1f..f8deb6913 100644 --- a/API/DTOs/Metadata/TagDto.cs +++ b/API/DTOs/Metadata/TagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public record TagDto +public sealed record TagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs b/API/DTOs/Person/BrowsePersonDto.cs similarity index 65% rename from API/DTOs/Metadata/Browse/BrowsePersonDto.cs rename to API/DTOs/Person/BrowsePersonDto.cs index 20f84b783..8d6999973 100644 --- a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs +++ b/API/DTOs/Person/BrowsePersonDto.cs @@ -1,6 +1,4 @@ -using API.DTOs.Person; - -namespace API.DTOs.Metadata.Browse; +namespace API.DTOs; /// /// Used to browse writers and click in to see their series @@ -12,7 +10,7 @@ public class BrowsePersonDto : PersonDto /// public int SeriesCount { get; set; } /// - /// Number of Issues this Person is the Writer for + /// Number or Issues this Person is the Writer for /// - public int ChapterCount { get; set; } + public int IssueCount { get; set; } } diff --git a/API/DTOs/Person/PersonDto.cs b/API/DTOs/Person/PersonDto.cs index db152e3b1..511317f2a 100644 --- a/API/DTOs/Person/PersonDto.cs +++ b/API/DTOs/Person/PersonDto.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Runtime.Serialization; -namespace API.DTOs.Person; +namespace API.DTOs; #nullable enable public class PersonDto @@ -13,7 +13,6 @@ public class PersonDto public string? SecondaryColor { get; set; } public string? CoverImage { get; set; } - public List Aliases { get; set; } = []; public string? Description { get; set; } /// diff --git a/API/DTOs/Person/PersonMergeDto.cs b/API/DTOs/Person/PersonMergeDto.cs deleted file mode 100644 index b5dc23375..000000000 --- a/API/DTOs/Person/PersonMergeDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace API.DTOs; - -public sealed record PersonMergeDto -{ - /// - /// The id of the person being merged into - /// - [Required] - public int DestId { get; init; } - /// - /// The id of the person being merged. This person will be removed, and become an alias of - /// - [Required] - public int SrcId { get; init; } -} diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/API/DTOs/Person/UpdatePersonDto.cs index b43a45e88..29190151f 100644 --- a/API/DTOs/Person/UpdatePersonDto.cs +++ b/API/DTOs/Person/UpdatePersonDto.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace API.DTOs; #nullable enable @@ -12,7 +11,6 @@ public sealed record UpdatePersonDto public bool CoverImageLocked { get; set; } [Required] public string Name {get; set;} - public IList Aliases { get; set; } = []; public string? Description { get; set; } public int? AniListId { get; set; } diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index 2473cd5dc..5c4e530c6 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -15,4 +15,5 @@ public sealed record BookInfoDto : IChapterInfoDto public int Pages { get; set; } public bool IsSpecial { get; set; } public string ChapterTitle { get; set; } = default! ; + public LibraryType LibraryType { get; set; } } diff --git a/API/DTOs/Reader/IChapterInfoDto.cs b/API/DTOs/Reader/IChapterInfoDto.cs index 6a9a74a2c..568adf345 100644 --- a/API/DTOs/Reader/IChapterInfoDto.cs +++ b/API/DTOs/Reader/IChapterInfoDto.cs @@ -14,5 +14,6 @@ public interface IChapterInfoDto public int Pages { get; set; } public bool IsSpecial { get; set; } public string ChapterTitle { get; set; } + public LibraryType LibraryType { get; set; } } diff --git a/API/DTOs/ReadingLists/ReadingListCast.cs b/API/DTOs/ReadingLists/ReadingListCast.cs index 855bb12b7..8f2587426 100644 --- a/API/DTOs/ReadingLists/ReadingListCast.cs +++ b/API/DTOs/ReadingLists/ReadingListCast.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using API.DTOs.Person; namespace API.DTOs.ReadingLists; diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs index 47a526411..cbc16275d 100644 --- a/API/DTOs/ReadingLists/ReadingListDto.cs +++ b/API/DTOs/ReadingLists/ReadingListDto.cs @@ -49,11 +49,6 @@ public sealed record ReadingListDto : IHasCoverImage /// public required AgeRating AgeRating { get; set; } = AgeRating.Unknown; - /// - /// Username of the User that owns (in the case of a promoted list) - /// - public string OwnerUserName { get; set; } - public void ResetColorScape() { PrimaryColor = string.Empty; diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index 562d923ff..7b1ccd75a 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -5,7 +5,6 @@ namespace API.DTOs.Scrobbling; public sealed record ScrobbleEventDto { - public long Id { get; init; } public string SeriesName { get; set; } public int SeriesId { get; set; } public int LibraryId { get; set; } diff --git a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs index ad66729d0..53d3a0cc9 100644 --- a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs @@ -8,6 +8,5 @@ public sealed record ScrobbleResponseDto { public bool Successful { get; set; } public string? ErrorMessage { get; set; } - public string? ExtraInformation {get; set;} public int RateLeft { get; set; } } diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/API/DTOs/Search/SearchResultGroupDto.cs index 11c4bdc08..20a53f853 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/API/DTOs/Search/SearchResultGroupDto.cs @@ -2,7 +2,6 @@ using API.DTOs.Collection; using API.DTOs.CollectionTags; using API.DTOs.Metadata; -using API.DTOs.Person; using API.DTOs.Reader; using API.DTOs.ReadingLists; diff --git a/API/DTOs/SeriesMetadataDto.cs b/API/DTOs/SeriesMetadataDto.cs index fa745148e..701034d80 100644 --- a/API/DTOs/SeriesMetadataDto.cs +++ b/API/DTOs/SeriesMetadataDto.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using API.DTOs.Metadata; -using API.DTOs.Person; using API.Entities.Enums; namespace API.DTOs; diff --git a/API/DTOs/UpdateChapterDto.cs b/API/DTOs/UpdateChapterDto.cs index 9ead8adc8..ec2f1cf62 100644 --- a/API/DTOs/UpdateChapterDto.cs +++ b/API/DTOs/UpdateChapterDto.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using API.DTOs.Metadata; -using API.DTOs.Person; using API.Entities.Enums; namespace API.DTOs; diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index d7f314208..9bd47fd39 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -28,10 +28,6 @@ public sealed record UpdateLibraryDto public bool AllowScrobbling { get; init; } [Required] public bool AllowMetadataMatching { get; init; } - [Required] - public bool EnableMetadata { get; init; } - [Required] - public bool RemovePrefixForSortName { get; init; } /// /// What types of files to allow the scanner to pickup /// diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 46f42306e..6645a8f39 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -9,6 +9,61 @@ namespace API.DTOs; public sealed record UserPreferencesDto { + /// + [Required] + public ReadingDirection ReadingDirection { get; set; } + /// + [Required] + public ScalingOption ScalingOption { get; set; } + /// + [Required] + public PageSplitOption PageSplitOption { get; set; } + /// + [Required] + public ReaderMode ReaderMode { get; set; } + /// + [Required] + public LayoutMode LayoutMode { get; set; } + /// + [Required] + public bool EmulateBook { get; set; } + /// + [Required] + public string BackgroundColor { get; set; } = "#000000"; + /// + [Required] + public bool SwipeToPaginate { get; set; } + /// + [Required] + public bool AutoCloseMenu { get; set; } + /// + [Required] + public bool ShowScreenHints { get; set; } = true; + /// + [Required] + public bool AllowAutomaticWebtoonReaderDetection { get; set; } + + /// + [Required] + public int BookReaderMargin { get; set; } + /// + [Required] + public int BookReaderLineSpacing { get; set; } + /// + [Required] + public int BookReaderFontSize { get; set; } + /// + [Required] + public string BookReaderFontFamily { get; set; } = null!; + /// + [Required] + public bool BookReaderTapToPaginate { get; set; } + /// + [Required] + public ReadingDirection BookReaderReadingDirection { get; set; } + /// + [Required] + public WritingStyle BookReaderWritingStyle { get; set; } /// /// UI Site Global Setting: The UI theme the user should use. @@ -17,6 +72,15 @@ public sealed record UserPreferencesDto [Required] public SiteThemeDto? Theme { get; set; } + [Required] public string BookReaderThemeName { get; set; } = null!; + /// + [Required] + public BookPageLayoutMode BookReaderLayoutMode { get; set; } + /// + [Required] + public bool BookReaderImmersiveMode { get; set; } = false; + /// + [Required] public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards; /// [Required] @@ -37,6 +101,16 @@ public sealed record UserPreferencesDto [Required] public string Locale { get; set; } + /// + [Required] + public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark; + /// + [Required] + public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; + /// + [Required] + public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; + /// public bool AniListScrobblingEnabled { get; set; } /// diff --git a/API/DTOs/UserReadingProfileDto.cs b/API/DTOs/UserReadingProfileDto.cs deleted file mode 100644 index 24dbf1c34..000000000 --- a/API/DTOs/UserReadingProfileDto.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; - -namespace API.DTOs; - -public sealed record UserReadingProfileDto -{ - - public int Id { get; set; } - public int UserId { get; init; } - - public string Name { get; init; } - public ReadingProfileKind Kind { get; init; } - - #region MangaReader - - /// - [Required] - public ReadingDirection ReadingDirection { get; set; } - - /// - [Required] - public ScalingOption ScalingOption { get; set; } - - /// - [Required] - public PageSplitOption PageSplitOption { get; set; } - - /// - [Required] - public ReaderMode ReaderMode { get; set; } - - /// - [Required] - public bool AutoCloseMenu { get; set; } - - /// - [Required] - public bool ShowScreenHints { get; set; } = true; - - /// - [Required] - public bool EmulateBook { get; set; } - - /// - [Required] - public LayoutMode LayoutMode { get; set; } - - /// - [Required] - public string BackgroundColor { get; set; } = "#000000"; - - /// - [Required] - public bool SwipeToPaginate { get; set; } - - /// - [Required] - public bool AllowAutomaticWebtoonReaderDetection { get; set; } - - /// - public int? WidthOverride { get; set; } - - /// - public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never; - - #endregion - - #region EpubReader - - /// - [Required] - public int BookReaderMargin { get; set; } - - /// - [Required] - public int BookReaderLineSpacing { get; set; } - - /// - [Required] - public int BookReaderFontSize { get; set; } - - /// - [Required] - public string BookReaderFontFamily { get; set; } = null!; - - /// - [Required] - public bool BookReaderTapToPaginate { get; set; } - - /// - [Required] - public ReadingDirection BookReaderReadingDirection { get; set; } - - /// - [Required] - public WritingStyle BookReaderWritingStyle { get; set; } - - /// - [Required] - public string BookReaderThemeName { get; set; } = null!; - - /// - [Required] - public BookPageLayoutMode BookReaderLayoutMode { get; set; } - - /// - [Required] - public bool BookReaderImmersiveMode { get; set; } = false; - - #endregion - - #region PdfReader - - /// - [Required] - public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark; - - /// - [Required] - public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; - - /// - [Required] - public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; - - #endregion - -} diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 7d529b1da..714e29fdf 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using API.DTOs.KavitaPlus.Metadata; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; @@ -17,6 +18,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace API.Data; @@ -41,13 +43,12 @@ public sealed class DataContext : IdentityDbContext ServerSetting { get; set; } = null!; public DbSet AppUserPreferences { get; set; } = null!; public DbSet SeriesMetadata { get; set; } = null!; - [Obsolete("Use AppUserCollection")] + [Obsolete] public DbSet CollectionTag { get; set; } = null!; public DbSet AppUserBookmark { get; set; } = null!; public DbSet ReadingList { get; set; } = null!; public DbSet ReadingListItem { get; set; } = null!; public DbSet Person { get; set; } = null!; - public DbSet PersonAlias { get; set; } = null!; public DbSet Genre { get; set; } = null!; public DbSet Tag { get; set; } = null!; public DbSet SiteTheme { get; set; } = null!; @@ -70,7 +71,7 @@ public sealed class DataContext : IdentityDbContext ExternalSeriesMetadata { get; set; } = null!; public DbSet ExternalRecommendation { get; set; } = null!; public DbSet ManualMigrationHistory { get; set; } = null!; - [Obsolete("Use IsBlacklisted field on Series")] + [Obsolete] public DbSet SeriesBlacklist { get; set; } = null!; public DbSet AppUserCollection { get; set; } = null!; public DbSet ChapterPeople { get; set; } = null!; @@ -79,7 +80,6 @@ public sealed class DataContext : IdentityDbContext MetadataSettings { get; set; } = null!; public DbSet MetadataFieldMapping { get; set; } = null!; public DbSet AppUserChapterRating { get; set; } = null!; - public DbSet AppUserReadingProfiles { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) { @@ -145,9 +145,6 @@ public sealed class DataContext : IdentityDbContext() .Property(b => b.AllowMetadataMatching) .HasDefaultValue(true); - builder.Entity() - .Property(b => b.EnableMetadata) - .HasDefaultValue(true); builder.Entity() .Property(b => b.WebLinks) @@ -258,48 +255,6 @@ public sealed class DataContext : IdentityDbContext() .Property(b => b.EnableCoverImage) .HasDefaultValue(true); - - builder.Entity() - .Property(b => b.BookThemeName) - .HasDefaultValue("Dark"); - builder.Entity() - .Property(b => b.BackgroundColor) - .HasDefaultValue("#000000"); - builder.Entity() - .Property(b => b.BookReaderWritingStyle) - .HasDefaultValue(WritingStyle.Horizontal); - builder.Entity() - .Property(b => b.AllowAutomaticWebtoonReaderDetection) - .HasDefaultValue(true); - - builder.Entity() - .Property(rp => rp.LibraryIds) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) - .HasColumnType("TEXT"); - builder.Entity() - .Property(rp => rp.SeriesIds) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) - .HasColumnType("TEXT"); - - builder.Entity() - .Property(sm => sm.KPlusOverrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? - new List()) - .HasColumnType("TEXT") - .HasDefaultValue(new List()); - builder.Entity() - .Property(sm => sm.KPlusOverrides) - .HasConversion( - v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()) - .HasColumnType("TEXT") - .HasDefaultValue(new List()); } #nullable enable diff --git a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs b/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs deleted file mode 100644 index b2afde98a..000000000 --- a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -public static class ManualMigrateReadingProfiles -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateReadingProfiles")) - { - return; - } - - logger.LogCritical("Running ManualMigrateReadingProfiles migration - Please be patient, this may take some time. This is not an error"); - - var users = await context.AppUser - .Include(u => u.UserPreferences) - .Include(u => u.ReadingProfiles) - .ToListAsync(); - - foreach (var user in users) - { - var readingProfile = new AppUserReadingProfile - { - Name = "Default", - NormalizedName = "Default".ToNormalized(), - Kind = ReadingProfileKind.Default, - LibraryIds = [], - SeriesIds = [], - BackgroundColor = user.UserPreferences.BackgroundColor, - EmulateBook = user.UserPreferences.EmulateBook, - AppUser = user, - PdfTheme = user.UserPreferences.PdfTheme, - ReaderMode = user.UserPreferences.ReaderMode, - ReadingDirection = user.UserPreferences.ReadingDirection, - ScalingOption = user.UserPreferences.ScalingOption, - LayoutMode = user.UserPreferences.LayoutMode, - WidthOverride = null, - AppUserId = user.Id, - AutoCloseMenu = user.UserPreferences.AutoCloseMenu, - BookReaderMargin = user.UserPreferences.BookReaderMargin, - PageSplitOption = user.UserPreferences.PageSplitOption, - BookThemeName = user.UserPreferences.BookThemeName, - PdfSpreadMode = user.UserPreferences.PdfSpreadMode, - PdfScrollMode = user.UserPreferences.PdfScrollMode, - SwipeToPaginate = user.UserPreferences.SwipeToPaginate, - BookReaderFontFamily = user.UserPreferences.BookReaderFontFamily, - BookReaderFontSize = user.UserPreferences.BookReaderFontSize, - BookReaderImmersiveMode = user.UserPreferences.BookReaderImmersiveMode, - BookReaderLayoutMode = user.UserPreferences.BookReaderLayoutMode, - BookReaderLineSpacing = user.UserPreferences.BookReaderLineSpacing, - BookReaderReadingDirection = user.UserPreferences.BookReaderReadingDirection, - BookReaderWritingStyle = user.UserPreferences.BookReaderWritingStyle, - AllowAutomaticWebtoonReaderDetection = user.UserPreferences.AllowAutomaticWebtoonReaderDetection, - BookReaderTapToPaginate = user.UserPreferences.BookReaderTapToPaginate, - ShowScreenHints = user.UserPreferences.ShowScreenHints, - }; - user.ReadingProfiles.Add(readingProfile); - } - - await context.SaveChangesAsync(); - - context.ManualMigrationHistory.Add(new ManualMigrationHistory - { - Name = "ManualMigrateReadingProfiles", - ProductVersion = BuildInfo.Version.ToString(), - RanAt = DateTime.UtcNow, - }); - await context.SaveChangesAsync(); - - - logger.LogCritical("Running ManualMigrateReadingProfiles migration - Completed. This is not an error"); - - } -} diff --git a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs b/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs deleted file mode 100644 index 5d76571e1..000000000 --- a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs +++ /dev/null @@ -1,3571 +0,0 @@ -// -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("20250507221026_PersonAliases")] - partial class PersonAliases - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - 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.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .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.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - 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.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .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.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - 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("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .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("TeamLocked") - .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.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - 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("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .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("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .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("LowestFolderPath") - .HasColumnType("TEXT"); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .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("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - 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("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - 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("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("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.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - 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("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("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("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250507221026_PersonAliases.cs b/API/Data/Migrations/20250507221026_PersonAliases.cs deleted file mode 100644 index cb046a131..000000000 --- a/API/Data/Migrations/20250507221026_PersonAliases.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class PersonAliases : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "PersonAlias", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Alias = table.Column(type: "TEXT", nullable: true), - NormalizedAlias = table.Column(type: "TEXT", nullable: true), - PersonId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PersonAlias", x => x.Id); - table.ForeignKey( - name: "FK_PersonAlias_Person_PersonId", - column: x => x.PersonId, - principalTable: "Person", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_PersonAlias_PersonId", - table: "PersonAlias", - column: "PersonId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "PersonAlias"); - } - } -} diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs b/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs deleted file mode 100644 index 79f6f9504..000000000 --- a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs +++ /dev/null @@ -1,3574 +0,0 @@ -// -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("20250519151126_KoreaderHash")] - partial class KoreaderHash - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - 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.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .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.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - 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.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - 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.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - 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("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .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("TeamLocked") - .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.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - 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("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .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("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .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("LowestFolderPath") - .HasColumnType("TEXT"); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .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("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - 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("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - 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("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("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.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.HasOne("API.Entities.AppRole", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUser", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - 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("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("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("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.cs b/API/Data/Migrations/20250519151126_KoreaderHash.cs deleted file mode 100644 index 006070b72..000000000 --- a/API/Data/Migrations/20250519151126_KoreaderHash.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class KoreaderHash : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "KoreaderHash", - table: "MangaFile", - type: "TEXT", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "KoreaderHash", - table: "MangaFile"); - } - } -} diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs b/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs deleted file mode 100644 index 762eae142..000000000 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs +++ /dev/null @@ -1,3698 +0,0 @@ -// -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("20250601200056_ReadingProfiles")] - partial class ReadingProfiles - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - 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.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - 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("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.PrimitiveCollection("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .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.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - 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.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .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.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - 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("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .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("TeamLocked") - .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.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - 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("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .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("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .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("LowestFolderPath") - .HasColumnType("TEXT"); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .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("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - 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("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - 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("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("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.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .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.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - 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("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("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("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.cs b/API/Data/Migrations/20250601200056_ReadingProfiles.cs deleted file mode 100644 index 66b9e53e5..000000000 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class ReadingProfiles : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AppUserReadingProfiles", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", nullable: true), - NormalizedName = table.Column(type: "TEXT", nullable: true), - AppUserId = table.Column(type: "INTEGER", nullable: false), - Kind = table.Column(type: "INTEGER", nullable: false), - LibraryIds = table.Column(type: "TEXT", nullable: true), - SeriesIds = table.Column(type: "TEXT", nullable: true), - ReadingDirection = table.Column(type: "INTEGER", nullable: false), - ScalingOption = table.Column(type: "INTEGER", nullable: false), - PageSplitOption = table.Column(type: "INTEGER", nullable: false), - ReaderMode = table.Column(type: "INTEGER", nullable: false), - AutoCloseMenu = table.Column(type: "INTEGER", nullable: false), - ShowScreenHints = table.Column(type: "INTEGER", nullable: false), - EmulateBook = table.Column(type: "INTEGER", nullable: false), - LayoutMode = table.Column(type: "INTEGER", nullable: false), - BackgroundColor = table.Column(type: "TEXT", nullable: true, defaultValue: "#000000"), - SwipeToPaginate = table.Column(type: "INTEGER", nullable: false), - AllowAutomaticWebtoonReaderDetection = table.Column(type: "INTEGER", nullable: false, defaultValue: true), - WidthOverride = table.Column(type: "INTEGER", nullable: true), - BookReaderMargin = table.Column(type: "INTEGER", nullable: false), - BookReaderLineSpacing = table.Column(type: "INTEGER", nullable: false), - BookReaderFontSize = table.Column(type: "INTEGER", nullable: false), - BookReaderFontFamily = table.Column(type: "TEXT", nullable: true), - BookReaderTapToPaginate = table.Column(type: "INTEGER", nullable: false), - BookReaderReadingDirection = table.Column(type: "INTEGER", nullable: false), - BookReaderWritingStyle = table.Column(type: "INTEGER", nullable: false, defaultValue: 0), - BookThemeName = table.Column(type: "TEXT", nullable: true, defaultValue: "Dark"), - BookReaderLayoutMode = table.Column(type: "INTEGER", nullable: false), - BookReaderImmersiveMode = table.Column(type: "INTEGER", nullable: false), - PdfTheme = table.Column(type: "INTEGER", nullable: false), - PdfScrollMode = table.Column(type: "INTEGER", nullable: false), - PdfSpreadMode = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AppUserReadingProfiles", x => x.Id); - table.ForeignKey( - name: "FK_AppUserReadingProfiles_AspNetUsers_AppUserId", - column: x => x.AppUserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AppUserReadingProfiles_AppUserId", - table: "AppUserReadingProfiles", - column: "AppUserId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AppUserReadingProfiles"); - } - } -} diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs deleted file mode 100644 index 0e9f00b4e..000000000 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs +++ /dev/null @@ -1,3701 +0,0 @@ -// -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("20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint")] - partial class AppUserReadingProfileDisableWidthOverrideBreakPoint - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); - - modelBuilder.Entity("API.Entities.AppRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("AgeRestriction") - .HasColumnType("INTEGER"); - - b.Property("AgeRestrictionIncludeUnknowns") - .HasColumnType("INTEGER"); - - b.Property("AniListAccessToken") - .HasColumnType("TEXT"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("ConfirmationToken") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - 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.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - 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("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .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.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - 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.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .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.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - 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("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .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("TeamLocked") - .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.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - 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("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .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("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .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("LowestFolderPath") - .HasColumnType("TEXT"); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .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("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - 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("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - 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("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("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.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .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.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - 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("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("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("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs deleted file mode 100644 index 11a554bdf..000000000 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class AppUserReadingProfileDisableWidthOverrideBreakPoint : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DisableWidthOverride", - table: "AppUserReadingProfiles", - type: "INTEGER", - nullable: false, - defaultValue: 0); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DisableWidthOverride", - table: "AppUserReadingProfiles"); - } - } -} diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs deleted file mode 100644 index c15f9f77b..000000000 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs +++ /dev/null @@ -1,3709 +0,0 @@ -// -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("20250620215058_EnableMetadataLibrary")] - partial class EnableMetadataLibrary - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); - - 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("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - 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.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - 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("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .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.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - 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.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - 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.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - 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("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .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("TeamLocked") - .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.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - 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("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .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("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .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("LowestFolderPath") - .HasColumnType("TEXT"); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .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("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - 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("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - 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("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("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.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .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.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - 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("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("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("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs deleted file mode 100644 index f9e38c01d..000000000 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class EnableMetadataLibrary : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "EnableMetadata", - table: "Library", - type: "INTEGER", - nullable: false, - defaultValue: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "EnableMetadata", - table: "Library"); - } - } -} diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs b/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs deleted file mode 100644 index b72239924..000000000 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs +++ /dev/null @@ -1,3721 +0,0 @@ -// -using System; -using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -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("20250626162548_TrackKavitaPlusMetadata")] - partial class TrackKavitaPlusMetadata - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); - - 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("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - 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.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - 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("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .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.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - 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.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - 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.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - 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("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .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("TeamLocked") - .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.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - 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("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .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("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .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("LowestFolderPath") - .HasColumnType("TEXT"); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .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("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - 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("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - 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("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("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.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .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.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - 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("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("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("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs b/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs deleted file mode 100644 index ac253e0a8..000000000 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class TrackKavitaPlusMetadata : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "KPlusOverrides", - table: "SeriesMetadata", - type: "TEXT", - nullable: true, - defaultValue: "[]"); - - migrationBuilder.AddColumn( - name: "KPlusOverrides", - table: "Chapter", - type: "TEXT", - nullable: true, - defaultValue: "[]"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "KPlusOverrides", - table: "SeriesMetadata"); - - migrationBuilder.DropColumn( - name: "KPlusOverrides", - table: "Chapter"); - } - } -} diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs deleted file mode 100644 index 165663f3d..000000000 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs +++ /dev/null @@ -1,3724 +0,0 @@ -// -using System; -using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -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("20250629153840_LibraryRemoveSortPrefix")] - partial class LibraryRemoveSortPrefix - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); - - 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("HasRunScrobbleEventGeneration") - .HasColumnType("INTEGER"); - - b.Property("LastActive") - .HasColumnType("TEXT"); - - b.Property("LastActiveUtc") - .HasColumnType("TEXT"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("MalAccessToken") - .HasColumnType("TEXT"); - - b.Property("MalUserName") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ScrobbleEventGenerationRan") - .HasColumnType("TEXT"); - - 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.AppUserChapterRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserChapterRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LastSyncUtc") - .HasColumnType("TEXT"); - - b.Property("MissingSeriesFromSource") - .HasColumnType("TEXT"); - - b.Property("NormalizedTitle") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Source") - .HasColumnType("INTEGER"); - - b.Property("SourceUrl") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TotalSourceCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserCollection"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(4); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserDashboardStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApiKey") - .HasColumnType("TEXT"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Host") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserExternalSource"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserOnDeckRemoval"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AniListScrobblingEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - b.Property("BlurUnreadSummaries") - .HasColumnType("INTEGER"); - - b.Property("BookReaderFontFamily") - .HasColumnType("TEXT"); - - b.Property("BookReaderFontSize") - .HasColumnType("INTEGER"); - - b.Property("BookReaderImmersiveMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLayoutMode") - .HasColumnType("INTEGER"); - - b.Property("BookReaderLineSpacing") - .HasColumnType("INTEGER"); - - b.Property("BookReaderMargin") - .HasColumnType("INTEGER"); - - b.Property("BookReaderReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("BookReaderTapToPaginate") - .HasColumnType("INTEGER"); - - b.Property("BookReaderWritingStyle") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("BookThemeName") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("Dark"); - - b.Property("CollapseSeriesRelationships") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("GlobalPageLayoutMode") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("Locale") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("en"); - - b.Property("NoTransitions") - .HasColumnType("INTEGER"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("PromptForDownloadSize") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("ShareReviews") - .HasColumnType("INTEGER"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("ThemeId") - .HasColumnType("INTEGER"); - - b.Property("WantToReadSync") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.HasKey("Id"); - - b.HasIndex("AppUserId") - .IsUnique(); - - b.HasIndex("ThemeId"); - - b.ToTable("AppUserPreferences"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserProgresses"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserRating"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - 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("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - - modelBuilder.Entity("API.Entities.AppUserRole", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSourceId") - .HasColumnType("INTEGER"); - - b.Property("IsProvided") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("SmartFilterId") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(5); - - b.Property("Visible") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SmartFilterId"); - - b.HasIndex("Visible"); - - b.ToTable("AppUserSideNavStream"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Filter") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserSmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("BookScrollId") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PageNumber") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserTableOfContent"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("AppUserWantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRating") - .HasColumnType("INTEGER"); - - b.Property("AgeRatingLocked") - .HasColumnType("INTEGER"); - - b.Property("AlternateCount") - .HasColumnType("INTEGER"); - - b.Property("AlternateNumber") - .HasColumnType("TEXT"); - - b.Property("AlternateSeries") - .HasColumnType("TEXT"); - - b.Property("AverageExternalRating") - .HasColumnType("REAL"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CharacterLocked") - .HasColumnType("INTEGER"); - - b.Property("ColoristLocked") - .HasColumnType("INTEGER"); - - b.Property("Count") - .HasColumnType("INTEGER"); - - b.Property("CoverArtistLocked") - .HasColumnType("INTEGER"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EditorLocked") - .HasColumnType("INTEGER"); - - b.Property("GenresLocked") - .HasColumnType("INTEGER"); - - b.Property("ISBN") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("ISBNLocked") - .HasColumnType("INTEGER"); - - b.Property("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("IsSpecial") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .HasColumnType("INTEGER"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Number") - .HasColumnType("TEXT"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PencillerLocked") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("PublisherLocked") - .HasColumnType("INTEGER"); - - b.Property("Range") - .HasColumnType("TEXT"); - - b.Property("ReleaseDate") - .HasColumnType("TEXT"); - - b.Property("ReleaseDateLocked") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesGroup") - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("REAL"); - - b.Property("SortOrderLocked") - .HasColumnType("INTEGER"); - - b.Property("StoryArc") - .HasColumnType("TEXT"); - - b.Property("StoryArcNumber") - .HasColumnType("TEXT"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("SummaryLocked") - .HasColumnType("INTEGER"); - - b.Property("TagsLocked") - .HasColumnType("INTEGER"); - - b.Property("TeamLocked") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("TitleName") - .HasColumnType("TEXT"); - - b.Property("TitleNameLocked") - .HasColumnType("INTEGER"); - - b.Property("TotalCount") - .HasColumnType("INTEGER"); - - b.Property("TranslatorLocked") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WebLinks") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.Property("WriterLocked") - .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.EmailHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DeliveryStatus") - .HasColumnType("TEXT"); - - b.Property("EmailTemplate") - .HasColumnType("TEXT"); - - b.Property("ErrorMessage") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SendDate") - .HasColumnType("TEXT"); - - b.Property("Sent") - .HasColumnType("INTEGER"); - - b.Property("Subject") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); - - b.ToTable("EmailHistory"); - }); - - 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.History.ManualMigrationHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("ProductVersion") - .HasColumnType("TEXT"); - - b.Property("RanAt") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ManualMigrationHistory"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowMetadataMatching") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("RemovePrefixForSortName") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("Pattern") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryExcludePattern"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FileTypeGroup") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("LibraryFileTypeGroup"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bytes") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Extension") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("FilePath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - - 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.ExternalRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("AverageScore") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("FavoriteCount") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ProviderUrl") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalRating"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("CoverUrl") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("Summary") - .HasColumnType("TEXT"); - - b.Property("Url") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("ExternalRecommendation"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Authority") - .HasColumnType("INTEGER"); - - b.Property("Body") - .HasColumnType("TEXT"); - - b.Property("BodyJustText") - .HasColumnType("TEXT"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("INTEGER"); - - b.Property("RawBody") - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("SiteUrl") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("TotalVotes") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChapterId"); - - b.ToTable("ExternalReview"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("AverageExternalRating") - .HasColumnType("INTEGER"); - - b.Property("CbrId") - .HasColumnType("INTEGER"); - - b.Property("GoogleBooksId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("ValidUntilUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.ToTable("ExternalSeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastChecked") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("SeriesBlacklist"); - }); - - 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("ImprintLocked") - .HasColumnType("INTEGER"); - - b.Property("InkerLocked") - .HasColumnType("INTEGER"); - - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("LanguageLocked") - .HasColumnType("INTEGER"); - - b.Property("LettererLocked") - .HasColumnType("INTEGER"); - - b.Property("LocationLocked") - .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("TeamLocked") - .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.MetadataFieldMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DestinationType") - .HasColumnType("INTEGER"); - - b.Property("DestinationValue") - .HasColumnType("TEXT"); - - b.Property("ExcludeFromSource") - .HasColumnType("INTEGER"); - - b.Property("MetadataSettingsId") - .HasColumnType("INTEGER"); - - b.Property("SourceType") - .HasColumnType("INTEGER"); - - b.Property("SourceValue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("MetadataSettingsId"); - - b.ToTable("MetadataFieldMapping"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AgeRatingMappings") - .HasColumnType("TEXT"); - - b.Property("Blacklist") - .HasColumnType("TEXT"); - - b.Property("EnableChapterCoverImage") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterPublisher") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterReleaseDate") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableChapterTitle") - .HasColumnType("INTEGER"); - - b.Property("EnableCoverImage") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("EnableGenres") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalizedName") - .HasColumnType("INTEGER"); - - b.Property("EnablePeople") - .HasColumnType("INTEGER"); - - b.Property("EnablePublicationStatus") - .HasColumnType("INTEGER"); - - b.Property("EnableRelationships") - .HasColumnType("INTEGER"); - - b.Property("EnableStartDate") - .HasColumnType("INTEGER"); - - b.Property("EnableSummary") - .HasColumnType("INTEGER"); - - b.Property("EnableTags") - .HasColumnType("INTEGER"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("FirstLastPeopleNaming") - .HasColumnType("INTEGER"); - - b.Property("Overrides") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("PersonRoles") - .HasColumnType("TEXT"); - - b.Property("Whitelist") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .HasColumnType("INTEGER"); - - b.HasKey("ChapterId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("ChapterPeople"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AniListId") - .HasColumnType("INTEGER"); - - b.Property("Asin") - .HasColumnType("TEXT"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("HardcoverId") - .HasColumnType("TEXT"); - - b.Property("MalId") - .HasColumnType("INTEGER"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.Property("SeriesMetadataId") - .HasColumnType("INTEGER"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.Property("Role") - .HasColumnType("INTEGER"); - - b.Property("KavitaPlusConnection") - .HasColumnType("INTEGER"); - - b.Property("OrderWeight") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("SeriesMetadataId", "PersonId", "Role"); - - b.HasIndex("PersonId"); - - b.ToTable("SeriesMetadataPeople"); - }); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("Promoted") - .HasColumnType("INTEGER"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - 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("ErrorDetails") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsErrored") - .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("REAL"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleEvent"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("SeriesId"); - - b.ToTable("ScrobbleHold"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AvgHoursToRead") - .HasColumnType("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("DontMatch") - .HasColumnType("INTEGER"); - - b.Property("FolderPath") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("IsBlacklisted") - .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("LowestFolderPath") - .HasColumnType("TEXT"); - - 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("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("SortNameLocked") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("LibraryId"); - - b.ToTable("Series"); - }); - - modelBuilder.Entity("API.Entities.ServerSetting", b => - { - b.Property("Key") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Key"); - - b.ToTable("ServerSetting"); - }); - - modelBuilder.Entity("API.Entities.ServerStatistics", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChapterCount") - .HasColumnType("INTEGER"); - - b.Property("FileCount") - .HasColumnType("INTEGER"); - - b.Property("GenreCount") - .HasColumnType("INTEGER"); - - b.Property("PersonCount") - .HasColumnType("INTEGER"); - - b.Property("SeriesCount") - .HasColumnType("INTEGER"); - - b.Property("TagCount") - .HasColumnType("INTEGER"); - - b.Property("UserCount") - .HasColumnType("INTEGER"); - - b.Property("VolumeCount") - .HasColumnType("INTEGER"); - - b.Property("Year") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("ServerStatistics"); - }); - - modelBuilder.Entity("API.Entities.SiteTheme", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Author") - .HasColumnType("TEXT"); - - b.Property("CompatibleVersion") - .HasColumnType("TEXT"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("FileName") - .HasColumnType("TEXT"); - - b.Property("GitHubPath") - .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("PreviewUrls") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.Property("ShaHash") - .HasColumnType("TEXT"); - - 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("REAL"); - - b.Property("CoverImage") - .HasColumnType("TEXT"); - - b.Property("CoverImageLocked") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LookupName") - .HasColumnType("TEXT"); - - b.Property("MaxHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MaxNumber") - .HasColumnType("REAL"); - - b.Property("MinHoursToRead") - .HasColumnType("INTEGER"); - - b.Property("MinNumber") - .HasColumnType("REAL"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Number") - .HasColumnType("INTEGER"); - - b.Property("Pages") - .HasColumnType("INTEGER"); - - b.Property("PrimaryColor") - .HasColumnType("TEXT"); - - b.Property("SecondaryColor") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("WordCount") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("SeriesId"); - - b.ToTable("Volume"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.Property("CollectionsId") - .HasColumnType("INTEGER"); - - b.Property("ItemsId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionsId", "ItemsId"); - - b.HasIndex("ItemsId"); - - b.ToTable("AppUserCollectionSeries"); - }); - - 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("ChapterTag", b => - { - b.Property("ChaptersId") - .HasColumnType("INTEGER"); - - b.Property("TagsId") - .HasColumnType("INTEGER"); - - b.HasKey("ChaptersId", "TagsId"); - - b.HasIndex("TagsId"); - - b.ToTable("ChapterTag"); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.Property("CollectionTagsId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("CollectionTagsId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("CollectionTagSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.Property("ExternalRatingsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRatingExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.Property("ExternalRecommendationsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalRecommendationExternalSeriesMetadata"); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.Property("ExternalReviewsId") - .HasColumnType("INTEGER"); - - b.Property("ExternalSeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); - - b.HasIndex("ExternalSeriesMetadatasId"); - - b.ToTable("ExternalReviewExternalSeriesMetadata"); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("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.AppUserChapterRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ChapterRatings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Ratings") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserCollection", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Collections") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("DashboardStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ExternalSources") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserPreferences", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.SiteTheme", "Theme") - .WithMany() - .HasForeignKey("ThemeId"); - - b.Navigation("AppUser"); - - b.Navigation("Theme"); - }); - - modelBuilder.Entity("API.Entities.AppUserProgress", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Progresses") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", null) - .WithMany("UserProgress") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany("Progress") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Ratings") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany("Ratings") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .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.AppUserSideNavStream", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SideNavStreams") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") - .WithMany() - .HasForeignKey("SmartFilterId"); - - b.Navigation("AppUser"); - - b.Navigation("SmartFilter"); - }); - - modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("SmartFilters") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("TableOfContents") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.AppUserWantToRead", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.HasOne("API.Entities.Volume", "Volume") - .WithMany("Chapters") - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Devices") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.EmailHistory", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.FolderPath", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Folders") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryExcludePatterns") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("LibraryFileTypes") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.MangaFile", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("Files") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalRatings") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany("ExternalReviews") - .HasForeignKey("ChapterId"); - }); - - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Relations") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "TargetSeries") - .WithMany("RelationOf") - .HasForeignKey("TargetSeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - - b.Navigation("TargetSeries"); - }); - - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => - { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") - .WithMany("FieldMappings") - .HasForeignKey("MetadataSettingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("MetadataSettings"); - }); - - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany("People") - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("ChapterPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("SeriesMetadataPeople") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") - .WithMany("People") - .HasForeignKey("SeriesMetadataId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - - b.Navigation("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingLists") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - - modelBuilder.Entity("API.Entities.ReadingListItem", b => - { - b.HasOne("API.Entities.Chapter", "Chapter") - .WithMany() - .HasForeignKey("ChapterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.ReadingList", "ReadingList") - .WithMany("Items") - .HasForeignKey("ReadingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Volume", "Volume") - .WithMany() - .HasForeignKey("VolumeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chapter"); - - b.Navigation("ReadingList"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => - { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") - .WithMany() - .HasForeignKey("ScrobbleEventId1"); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ScrobbleEvent"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany() - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Library", "Library") - .WithMany() - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Library"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ScrobbleHolds") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") - .WithMany() - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.HasOne("API.Entities.Library", "Library") - .WithMany("Series") - .HasForeignKey("LibraryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Library"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.HasOne("API.Entities.Series", "Series") - .WithMany("Volumes") - .HasForeignKey("SeriesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("AppUserCollectionSeries", b => - { - b.HasOne("API.Entities.AppUserCollection", null) - .WithMany() - .HasForeignKey("CollectionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", null) - .WithMany() - .HasForeignKey("ItemsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - 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("ChapterTag", b => - { - b.HasOne("API.Entities.Chapter", null) - .WithMany() - .HasForeignKey("ChaptersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Tag", null) - .WithMany() - .HasForeignKey("TagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("CollectionTagSeriesMetadata", b => - { - b.HasOne("API.Entities.CollectionTag", null) - .WithMany() - .HasForeignKey("CollectionTagsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRating", null) - .WithMany() - .HasForeignKey("ExternalRatingsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) - .WithMany() - .HasForeignKey("ExternalRecommendationsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => - { - b.HasOne("API.Entities.Metadata.ExternalReview", null) - .WithMany() - .HasForeignKey("ExternalReviewsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) - .WithMany() - .HasForeignKey("ExternalSeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("GenreSeriesMetadata", b => - { - b.HasOne("API.Entities.Genre", null) - .WithMany() - .HasForeignKey("GenresId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) - .WithMany() - .HasForeignKey("SeriesMetadatasId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("API.Entities.AppRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("API.Entities.AppUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("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("ChapterRatings"); - - b.Navigation("Collections"); - - b.Navigation("DashboardStreams"); - - b.Navigation("Devices"); - - b.Navigation("ExternalSources"); - - b.Navigation("Progresses"); - - b.Navigation("Ratings"); - - b.Navigation("ReadingLists"); - - b.Navigation("ReadingProfiles"); - - b.Navigation("ScrobbleHolds"); - - b.Navigation("SideNavStreams"); - - b.Navigation("SmartFilters"); - - b.Navigation("TableOfContents"); - - b.Navigation("UserPreferences"); - - b.Navigation("UserRoles"); - - b.Navigation("WantToRead"); - }); - - modelBuilder.Entity("API.Entities.Chapter", b => - { - b.Navigation("ExternalRatings"); - - b.Navigation("ExternalReviews"); - - b.Navigation("Files"); - - b.Navigation("People"); - - b.Navigation("Ratings"); - - b.Navigation("UserProgress"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.Navigation("Folders"); - - b.Navigation("LibraryExcludePatterns"); - - b.Navigation("LibraryFileTypes"); - - b.Navigation("Series"); - }); - - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => - { - b.Navigation("People"); - }); - - modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => - { - b.Navigation("FieldMappings"); - }); - - modelBuilder.Entity("API.Entities.Person.Person", b => - { - b.Navigation("Aliases"); - - b.Navigation("ChapterPeople"); - - b.Navigation("SeriesMetadataPeople"); - }); - - modelBuilder.Entity("API.Entities.ReadingList", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("API.Entities.Series", b => - { - b.Navigation("ExternalSeriesMetadata"); - - b.Navigation("Metadata"); - - b.Navigation("Progress"); - - b.Navigation("Ratings"); - - b.Navigation("RelationOf"); - - b.Navigation("Relations"); - - b.Navigation("Volumes"); - }); - - modelBuilder.Entity("API.Entities.Volume", b => - { - b.Navigation("Chapters"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs deleted file mode 100644 index 4800cf3fa..000000000 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace API.Data.Migrations -{ - /// - public partial class LibraryRemoveSortPrefix : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "RemovePrefixForSortName", - table: "Library", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "RemovePrefixForSortName", - table: "Library"); - } - } -} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 62d1fb1ef..a66568dcc 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1,8 +1,6 @@ // using System; -using System.Collections.Generic; using API.Data; -using API.Entities.MetadataMatching; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -17,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -611,123 +609,6 @@ namespace API.Data.Migrations b.ToTable("AppUserRating"); }); - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AllowAutomaticWebtoonReaderDetection") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("AutoCloseMenu") - .HasColumnType("INTEGER"); - - b.Property("BackgroundColor") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("#000000"); - - 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("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.Property("LibraryIds") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("PageSplitOption") - .HasColumnType("INTEGER"); - - b.Property("PdfScrollMode") - .HasColumnType("INTEGER"); - - b.Property("PdfSpreadMode") - .HasColumnType("INTEGER"); - - b.Property("PdfTheme") - .HasColumnType("INTEGER"); - - b.Property("ReaderMode") - .HasColumnType("INTEGER"); - - b.Property("ReadingDirection") - .HasColumnType("INTEGER"); - - b.Property("ScalingOption") - .HasColumnType("INTEGER"); - - b.Property("SeriesIds") - .HasColumnType("TEXT"); - - b.Property("ShowScreenHints") - .HasColumnType("INTEGER"); - - b.Property("SwipeToPaginate") - .HasColumnType("INTEGER"); - - b.Property("WidthOverride") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.ToTable("AppUserReadingProfiles"); - }); - modelBuilder.Entity("API.Entities.AppUserRole", b => { b.Property("UserId") @@ -959,11 +840,6 @@ namespace API.Data.Migrations b.Property("IsSpecial") .HasColumnType("INTEGER"); - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - b.Property("Language") .HasColumnType("TEXT"); @@ -1303,11 +1179,6 @@ namespace API.Data.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); - b.Property("EnableMetadata") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - b.Property("FolderWatching") .HasColumnType("INTEGER"); @@ -1341,9 +1212,6 @@ namespace API.Data.Migrations b.Property("PrimaryColor") .HasColumnType("TEXT"); - b.Property("RemovePrefixForSortName") - .HasColumnType("INTEGER"); - b.Property("SecondaryColor") .HasColumnType("TEXT"); @@ -1423,9 +1291,6 @@ namespace API.Data.Migrations b.Property("Format") .HasColumnType("INTEGER"); - b.Property("KoreaderHash") - .HasColumnType("TEXT"); - b.Property("LastFileAnalysis") .HasColumnType("TEXT"); @@ -1693,11 +1558,6 @@ namespace API.Data.Migrations b.Property("InkerLocked") .HasColumnType("INTEGER"); - b.Property("KPlusOverrides") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - b.Property("Language") .HasColumnType("TEXT"); @@ -1976,28 +1836,6 @@ namespace API.Data.Migrations b.ToTable("Person"); }); - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Alias") - .HasColumnType("TEXT"); - - b.Property("NormalizedAlias") - .HasColumnType("TEXT"); - - b.Property("PersonId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PersonId"); - - b.ToTable("PersonAlias"); - }); - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => { b.Property("SeriesMetadataId") @@ -2978,17 +2816,6 @@ namespace API.Data.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("ReadingProfiles") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("AppUser"); - }); - modelBuilder.Entity("API.Entities.AppUserRole", b => { b.HasOne("API.Entities.AppRole", "Role") @@ -3255,17 +3082,6 @@ namespace API.Data.Migrations b.Navigation("Person"); }); - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => - { - b.HasOne("API.Entities.Person.Person", "Person") - .WithMany("Aliases") - .HasForeignKey("PersonId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Person"); - }); - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => { b.HasOne("API.Entities.Person.Person", "Person") @@ -3627,8 +3443,6 @@ namespace API.Data.Migrations b.Navigation("ReadingLists"); - b.Navigation("ReadingProfiles"); - b.Navigation("ScrobbleHolds"); b.Navigation("SideNavStreams"); @@ -3682,8 +3496,6 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Person.Person", b => { - b.Navigation("Aliases"); - b.Navigation("ChapterPeople"); b.Navigation("SeriesMetadataPeople"); diff --git a/API/Data/Repositories/AppUserReadingProfileRepository.cs b/API/Data/Repositories/AppUserReadingProfileRepository.cs deleted file mode 100644 index 11b97f21a..000000000 --- a/API/Data/Repositories/AppUserReadingProfileRepository.cs +++ /dev/null @@ -1,112 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - - -public interface IAppUserReadingProfileRepository -{ - - /// - /// Return the given profile if it belongs the user - /// - /// - /// - /// - Task GetUserProfile(int userId, int profileId); - /// - /// Returns all reading profiles for the user - /// - /// - /// - Task> GetProfilesForUser(int userId, bool skipImplicit = false); - /// - /// Returns all reading profiles for the user - /// - /// - /// - Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false); - /// - /// Is there a user reading profile with this name (normalized) - /// - /// - /// - /// - Task IsProfileNameInUse(int userId, string name); - - void Add(AppUserReadingProfile readingProfile); - void Update(AppUserReadingProfile readingProfile); - void Remove(AppUserReadingProfile readingProfile); - void RemoveRange(IEnumerable readingProfiles); -} - -public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository -{ - public async Task GetUserProfile(int userId, int profileId) - { - return await context.AppUserReadingProfiles - .Where(rp => rp.AppUserId == userId && rp.Id == profileId) - .FirstOrDefaultAsync(); - } - - public async Task> GetProfilesForUser(int userId, bool skipImplicit = false) - { - return await context.AppUserReadingProfiles - .Where(rp => rp.AppUserId == userId) - .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) - .ToListAsync(); - } - - /// - /// Returns all Reading Profiles for the User - /// - /// - /// - public async Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false) - { - return await context.AppUserReadingProfiles - .Where(rp => rp.AppUserId == userId) - .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) - .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task IsProfileNameInUse(int userId, string name) - { - var normalizedName = name.ToNormalized(); - - return await context.AppUserReadingProfiles - .Where(rp => rp.NormalizedName == normalizedName && rp.AppUserId == userId) - .AnyAsync(); - } - - public void Add(AppUserReadingProfile readingProfile) - { - context.AppUserReadingProfiles.Add(readingProfile); - } - - public void Update(AppUserReadingProfile readingProfile) - { - context.AppUserReadingProfiles.Update(readingProfile).State = EntityState.Modified; - } - - public void Remove(AppUserReadingProfile readingProfile) - { - context.AppUserReadingProfiles.Remove(readingProfile); - } - - public void RemoveRange(IEnumerable readingProfiles) - { - context.AppUserReadingProfiles.RemoveRange(readingProfiles); - } -} diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 27d21df74..ce7c44baa 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -137,11 +137,11 @@ public class ChapterRepository : IChapterRepository LibraryId = data.LibraryId, Pages = data.Pages, ChapterTitle = data.TitleName, - LibraryType = data.LibraryType + LibraryType = data.LibraryType, }) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(); + .FirstOrDefaultAsync(); return chapterInfo; } diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index 377344a3c..45882b5c4 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -108,17 +108,14 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor public async Task NeedsDataRefresh(int seriesId) { - // TODO: Add unit test var row = await _context.ExternalSeriesMetadata .Where(s => s.SeriesId == seriesId) .FirstOrDefaultAsync(); - return row == null || row.ValidUntilUtc <= DateTime.UtcNow; } public async Task GetSeriesDetailPlusDto(int seriesId) { - // TODO: Add unit test var seriesDetailDto = await _context.ExternalSeriesMetadata .Where(m => m.SeriesId == seriesId) .Include(m => m.ExternalRatings) @@ -147,7 +144,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - IEnumerable reviews = []; + IEnumerable reviews = new List(); if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any()) { reviews = seriesDetailDto.ExternalReviews @@ -234,7 +231,6 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .Include(s => s.ExternalSeriesMetadata) .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .Where(s => s.Library.AllowMetadataMatching) - .WhereIf(filter.LibraryType >= 0, s => s.Library.Type == (LibraryType) filter.LibraryType) .FilterMatchState(filter.MatchStateOption) .OrderBy(s => s.NormalizedName) .ProjectTo(_mapper.ConfigurationProvider) diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index d3baa4de6..ef9dfa7ec 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -3,11 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Helpers; using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -29,7 +27,6 @@ public interface IGenreRepository Task GetRandomGenre(); Task GetGenreById(int id); Task> GetAllGenresNotInListAsync(ICollection genreNames); - Task> GetBrowseableGenre(int userId, UserParams userParams); } public class GenreRepository : IGenreRepository @@ -114,7 +111,7 @@ public class GenreRepository : IGenreRepository /// /// Returns a set of Genre tags for a set of library Ids. - /// AppUserId will restrict returned Genres based on user's age restriction and library access. + /// UserId will restrict returned Genres based on user's age restriction and library access. /// /// /// @@ -168,38 +165,4 @@ public class GenreRepository : IGenreRepository // Return the original non-normalized genres for the missing ones return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } - - public async Task> GetBrowseableGenre(int userId, UserParams userParams) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); - - var query = _context.Genre - .RestrictAgainstAgeRestriction(ageRating) - .WhereIf(allLibrariesCount != userLibs.Count, - genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || - genre.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId))) - .Select(g => new BrowseGenreDto - { - Id = g.Id, - Title = g.Title, - SeriesCount = g.SeriesMetadatas - .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - ChapterCount = g.Chapters - .Where(cp => allLibrariesCount == userLibs.Count || seriesIds.Contains(cp.Volume.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - }) - .OrderBy(g => g.Title); - - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); - } } diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 78022fa9a..b3f905dce 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -58,6 +58,7 @@ public interface ILibraryRepository Task GetAllowsScrobblingBySeriesId(int seriesId); Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds); + Task> GetLibraryTypesAsync(int userId); } public class LibraryRepository : ILibraryRepository @@ -369,4 +370,17 @@ public class LibraryRepository : ILibraryRepository }) .ToDictionaryAsync(entity => entity.Id, entity => entity.Type); } + + public async Task> GetLibraryTypesAsync(int userId) + { + return await _context.Library + .Where(l => l.AppUsers.Any(u => u.Id == userId)) + .Select(l => new LibraryTypeDto() + { + LibraryType = l.Type, + LibraryId = l.Id + }) + .AsSplitQuery() + .ToListAsync(); + } } diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs index 89c6bb418..debd52199 100644 --- a/API/Data/Repositories/MangaFileRepository.cs +++ b/API/Data/Repositories/MangaFileRepository.cs @@ -5,13 +5,11 @@ using API.Entities; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; -#nullable enable public interface IMangaFileRepository { void Update(MangaFile file); Task> GetAllWithMissingExtension(); - Task GetByKoreaderHash(string hash); } public class MangaFileRepository : IMangaFileRepository @@ -34,13 +32,4 @@ public class MangaFileRepository : IMangaFileRepository .Where(f => string.IsNullOrEmpty(f.Extension)) .ToListAsync(); } - - public async Task GetByKoreaderHash(string hash) - { - if (string.IsNullOrEmpty(hash)) return null; - - return await _context.MangaFile - .FirstOrDefaultAsync(f => f.KoreaderHash != null && - f.KoreaderHash.Equals(hash.ToUpper())); - } } diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index 76ae94735..db66ecd79 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -1,20 +1,12 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; using API.Entities.Enums; using API.Entities.Person; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; using API.Helpers; -using API.Helpers.Converters; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; @@ -22,17 +14,6 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; #nullable enable -[Flags] -public enum PersonIncludes -{ - None = 1 << 0, - Aliases = 1 << 1, - ChapterPeople = 1 << 2, - SeriesPeople = 1 << 3, - - All = Aliases | ChapterPeople | SeriesPeople, -} - public interface IPersonRepository { void Attach(Person person); @@ -42,41 +23,24 @@ public interface IPersonRepository void Remove(SeriesMetadataPeople person); void Update(Person person); - Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases); - Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None); - Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None); + Task> GetAllPeople(); + Task> GetAllPersonDtosAsync(int userId); + Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role); Task RemoveAllPeopleNoLongerAssociated(); - Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.None); + Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null); Task GetCoverImageAsync(int personId); Task GetCoverImageByNameAsync(string name); Task> GetRolesForPersonByName(int personId, int userId); - Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams); - Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None); - Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases); - /// - /// Returns a person matched on normalized name or alias - /// - /// - /// - /// - Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases); + Task> GetAllWritersAndSeriesCount(int userId, UserParams userParams); + Task GetPersonById(int personId); + Task GetPersonDtoByName(string name, int userId); Task IsNameUnique(string name); - Task> GetSeriesKnownFor(int personId, int userId); + Task> GetSeriesKnownFor(int personId); Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role); - /// - /// Returns all people with a matching name, or alias - /// - /// - /// - /// - Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases); - Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases); - - Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases); - - Task AnyAliasExist(string alias); + Task> GetPeopleByNames(List normalizedNames); + Task GetPersonByAniListId(int aniListId); } public class PersonRepository : IPersonRepository @@ -135,7 +99,7 @@ public class PersonRepository : IPersonRepository } - public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); @@ -149,7 +113,6 @@ public class PersonRepository : IPersonRepository .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(ageRating) .SelectMany(s => s.Metadata.People.Select(p => p.Person)) - .Includes(includes) .Distinct() .OrderBy(p => p.Name) .AsNoTracking() @@ -179,25 +142,20 @@ public class PersonRepository : IPersonRepository public async Task> GetRolesForPersonByName(int personId, int userId) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); // Query roles from ChapterPeople var chapterRoles = await _context.Person .Where(p => p.Id == personId) - .SelectMany(p => p.ChapterPeople) .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .Select(cp => cp.Role) + .SelectMany(p => p.ChapterPeople.Select(cp => cp.Role)) .Distinct() .ToListAsync(); // Query roles from SeriesMetadataPeople var seriesRoles = await _context.Person .Where(p => p.Id == personId) - .SelectMany(p => p.SeriesMetadataPeople) .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .Select(smp => smp.Role) + .SelectMany(p => p.SeriesMetadataPeople.Select(smp => smp.Role)) .Distinct() .ToListAsync(); @@ -205,142 +163,67 @@ public class PersonRepository : IPersonRepository return chapterRoles.Union(seriesRoles).Distinct(); } - public async Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams) + public async Task> GetAllWritersAndSeriesCount(int userId, UserParams userParams) { + List roles = [PersonRole.Writer, PersonRole.CoverArtist]; var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var query = await CreateFilteredPersonQueryable(userId, filter, ageRating); + var query = _context.Person + .Where(p => p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) || p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))) + .RestrictAgainstAgeRestriction(ageRating) + .Select(p => new BrowsePersonDto + { + Id = p.Id, + Name = p.Name, + Description = p.Description, + CoverImage = p.CoverImage, + SeriesCount = p.SeriesMetadataPeople + .Where(smp => roles.Contains(smp.Role)) + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count(), + IssueCount = p.ChapterPeople + .Where(cp => roles.Contains(cp.Role)) + .Select(cp => cp.Chapter.Id) + .Distinct() + .Count() + }) + .OrderBy(p => p.Name); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } - private async Task> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating) - { - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); - - var query = _context.Person.AsNoTracking(); - - // Apply filtering based on statements - query = BuildPersonFilterQuery(userId, filter, query); - - // Apply restrictions - query = query.RestrictAgainstAgeRestriction(ageRating) - .WhereIf(allLibrariesCount != userLibs.Count, - person => person.ChapterPeople.Any(cp => seriesIds.Contains(cp.Chapter.Volume.SeriesId)) || - person.SeriesMetadataPeople.Any(smp => seriesIds.Contains(smp.SeriesMetadata.SeriesId))); - - // Apply sorting and limiting - var sortedQuery = query.SortBy(filter.SortOptions); - - var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo); - - return limitedQuery.Select(p => new BrowsePersonDto - { - Id = p.Id, - Name = p.Name, - Description = p.Description, - CoverImage = p.CoverImage, - SeriesCount = p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata) - .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - ChapterCount = p.ChapterPeople - .Select(chp => chp.Chapter) - .Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - }); - } - - private static IQueryable BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable query) - { - if (filterDto.Statements == null || filterDto.Statements.Count == 0) return query; - - var queries = filterDto.Statements - .Select(statement => BuildPersonFilterGroup(userId, statement, query)) - .ToList(); - - return filterDto.Combination == FilterCombination.And - ? queries.Aggregate((q1, q2) => q1.Intersect(q2)) - : queries.Aggregate((q1, q2) => q1.Union(q2)); - } - - private static IQueryable BuildPersonFilterGroup(int userId, PersonFilterStatementDto statement, IQueryable query) - { - var value = PersonFilterFieldValueConverter.ConvertValue(statement.Field, statement.Value); - - return statement.Field switch - { - PersonFilterField.Name => query.HasPersonName(true, statement.Comparison, (string)value), - PersonFilterField.Role => query.HasPersonRole(true, statement.Comparison, (IList)value), - PersonFilterField.SeriesCount => query.HasPersonSeriesCount(true, statement.Comparison, (int)value), - PersonFilterField.ChapterCount => query.HasPersonChapterCount(true, statement.Comparison, (int)value), - _ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}") - }; - } - - private static IQueryable ApplyPersonLimit(IQueryable query, int limit) - { - return limit <= 0 ? query : query.Take(limit); - } - - public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None) + public async Task GetPersonById(int personId) { return await _context.Person.Where(p => p.Id == personId) - .Includes(includes) .FirstOrDefaultAsync(); } - public async Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task GetPersonDtoByName(string name, int userId) { var normalized = name.ToNormalized(); var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); return await _context.Person .Where(p => p.NormalizedName == normalized) - .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); } - public Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases) - { - var normalized = name.ToNormalized(); - return _context.Person - .Includes(includes) - .Where(p => p.NormalizedName == normalized || p.Aliases.Any(pa => pa.NormalizedAlias == normalized)) - .FirstOrDefaultAsync(); - } - public async Task IsNameUnique(string name) { - // Should this use Normalized to check? - return !(await _context.Person - .Includes(PersonIncludes.Aliases) - .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name))); + return !(await _context.Person.AnyAsync(p => p.Name == name)); } - public async Task> GetSeriesKnownFor(int personId, int userId) + public async Task> GetSeriesKnownFor(int personId) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - + List notValidRoles = [PersonRole.Location, PersonRole.Team, PersonRole.Other, PersonRole.Publisher, PersonRole.Translator]; return await _context.Person .Where(p => p.Id == personId) - .SelectMany(p => p.SeriesMetadataPeople) + .SelectMany(p => p.SeriesMetadataPeople.Where(smp => !notValidRoles.Contains(smp.Role))) .Select(smp => smp.SeriesMetadata) .Select(sm => sm.Series) - .RestrictAgainstAgeRestriction(ageRating) - .Where(s => userLibs.Contains(s.LibraryId)) .Distinct() .OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating) .Take(20) @@ -351,88 +234,58 @@ public class PersonRepository : IPersonRepository public async Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); return await _context.ChapterPeople .Where(cp => cp.PersonId == personId && cp.Role == role) .Select(cp => cp.Chapter) .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) .OrderBy(ch => ch.SortOrder) .Take(20) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - public async Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetPeopleByNames(List normalizedNames) { return await _context.Person - .Includes(includes) - .Where(p => normalizedNames.Contains(p.NormalizedName) || p.Aliases.Any(pa => normalizedNames.Contains(pa.NormalizedAlias))) + .Where(p => normalizedNames.Contains(p.NormalizedName)) .OrderBy(p => p.Name) .ToListAsync(); } - public async Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task GetPersonByAniListId(int aniListId) { return await _context.Person .Where(p => p.AniListId == aniListId) - .Includes(includes) .FirstOrDefaultAsync(); } - public async Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases) - { - searchQuery = searchQuery.ToNormalized(); - - return await _context.Person - .Includes(includes) - .Where(p => EF.Functions.Like(p.NormalizedName, $"%{searchQuery}%") - || p.Aliases.Any(pa => EF.Functions.Like(pa.NormalizedAlias, $"%{searchQuery}%"))) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - - public async Task AnyAliasExist(string alias) - { - return await _context.PersonAlias.AnyAsync(pa => pa.NormalizedAlias == alias.ToNormalized()); - } - - - public async Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPeople() { return await _context.Person - .Includes(includes) .OrderBy(p => p.Name) .ToListAsync(); } - public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None) + public async Task> GetAllPersonDtosAsync(int userId) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); return await _context.Person - .Includes(includes) + .OrderBy(p => p.Name) .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) - .OrderBy(p => p.Name) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None) + public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); return await _context.Person .Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters - .Includes(includes) - .RestrictAgainstAgeRestriction(ageRating) - .RestrictByLibrary(userLibs) .OrderBy(p => p.Name) + .RestrictAgainstAgeRestriction(ageRating) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 6992b2950..6d4a14bd9 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data.Misc; -using API.DTOs.Person; +using API.DTOs; using API.DTOs.ReadingLists; using API.Entities; using API.Entities.Enums; diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index 144a3b88e..c5f30c2ec 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -29,23 +29,8 @@ public interface IScrobbleRepository Task> GetAllScrobbleErrorsForSeries(int seriesId); Task ClearScrobbleErrors(); Task HasErrorForSeries(int seriesId); - /// - /// Get all events for a specific user and type - /// - /// - /// - /// - /// If true, only returned not processed events - /// - Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false); + Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType); Task> GetUserEventsForSeries(int userId, int seriesId); - /// - /// Return the events with given ids, when belonging to the passed user - /// - /// - /// - /// - Task> GetUserEvents(int userId, IList scrobbleEventIds); Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination); Task> GetAllEventsForSeries(int seriesId); Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds); @@ -161,32 +146,22 @@ public class ScrobbleRepository : IScrobbleRepository return await _context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId); } - public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false) + public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType) { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType) - .WhereIf(isNotProcessed, e => !e.IsProcessed) - .OrderBy(e => e.LastModifiedUtc) - .FirstOrDefaultAsync(); + return await _context.ScrobbleEvent.FirstOrDefaultAsync(e => + e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType); } public async Task> GetUserEventsForSeries(int userId, int seriesId) { return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId) + .Where(e => e.AppUserId == userId && !e.IsProcessed) .Include(e => e.Series) .OrderBy(e => e.LastModifiedUtc) .AsSplitQuery() .ToListAsync(); } - public async Task> GetUserEvents(int userId, IList scrobbleEventIds) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id)) - .ToListAsync(); - } - public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination) { var query = _context.ScrobbleEvent diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 0c4b8350a..d9c78c770 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -15,7 +15,6 @@ using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Metadata; -using API.DTOs.Person; using API.DTOs.ReadingLists; using API.DTOs.Recommendation; using API.DTOs.Scrobbling; @@ -82,7 +81,6 @@ public interface ISeriesRepository void Attach(Series series); void Attach(SeriesRelation relation); void Update(Series series); - void Update(SeriesMetadata seriesMetadata); void Remove(Series series); void Remove(IEnumerable series); void Detach(Series series); @@ -220,11 +218,6 @@ public class SeriesRepository : ISeriesRepository _context.Entry(series).State = EntityState.Modified; } - public void Update(SeriesMetadata seriesMetadata) - { - _context.Entry(seriesMetadata).State = EntityState.Modified; - } - public void Remove(Series series) { _context.Series.Remove(series); @@ -462,18 +455,11 @@ public class SeriesRepository : ISeriesRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - // I can't work out how to map people in DB layer - var personIds = await _context.SeriesMetadata + result.Persons = await _context.SeriesMetadata .SearchPeople(searchQuery, seriesIds) - .Select(p => p.Id) - .Distinct() - .OrderBy(id => id) .Take(maxRecords) - .ToListAsync(); - - result.Persons = await _context.Person - .Where(p => personIds.Contains(p.Id)) - .OrderBy(p => p.NormalizedName) + .OrderBy(t => t.NormalizedName) + .Distinct() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -489,8 +475,8 @@ public class SeriesRepository : ISeriesRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - result.Files = []; - result.Chapters = (List) []; + result.Files = new List(); + result.Chapters = new List(); if (includeChapterAndFiles) @@ -741,7 +727,6 @@ public class SeriesRepository : ISeriesRepository { return await _context.Series .Where(s => s.Id == seriesId) - .Include(s => s.ExternalSeriesMetadata) .Select(series => new PlusSeriesRequestDto() { MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), @@ -751,7 +736,6 @@ public class SeriesRepository : ISeriesRepository ScrobblingService.AniListWeblinkWebsite), MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite), - CbrId = series.ExternalSeriesMetadata.CbrId, GoogleBooksId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.GoogleBooksWeblinkWebsite), MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, @@ -1096,6 +1080,8 @@ public class SeriesRepository : ISeriesRepository return query.Where(s => false); } + + // First setup any FilterField.Libraries in the statements, as these don't have any traditional query statements applied here query = ApplyLibraryFilter(filter, query); @@ -1296,7 +1282,7 @@ public class SeriesRepository : ISeriesRepository FilterField.ReadingDate => query.HasReadingDate(true, statement.Comparison, (DateTime) value, userId), FilterField.ReadLast => query.HasReadLast(true, statement.Comparison, (int) value, userId), FilterField.AverageRating => query.HasAverageRating(true, statement.Comparison, (float) value), - _ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}") + _ => throw new ArgumentOutOfRangeException() }; } diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index 40d40a675..c4f189957 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -2,11 +2,9 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; -using API.Helpers; using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -25,7 +23,6 @@ public interface ITagRepository Task RemoveAllTagNoLongerAssociated(); Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null); Task> GetAllTagsNotInListAsync(ICollection tags); - Task> GetBrowseableTag(int userId, UserParams userParams); } public class TagRepository : ITagRepository @@ -107,40 +104,6 @@ public class TagRepository : ITagRepository return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } - public async Task> GetBrowseableTag(int userId, UserParams userParams) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); - - var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id); - - var query = _context.Tag - .RestrictAgainstAgeRestriction(ageRating) - .WhereIf(userLibs.Count != allLibrariesCount, - tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || - tag.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId))) - .Select(g => new BrowseTagDto - { - Id = g.Id, - Title = g.Title, - SeriesCount = g.SeriesMetadatas - .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count(), - ChapterCount = g.Chapters - .Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId)) - .RestrictAgainstAgeRestriction(ageRating) - .Distinct() - .Count() - }) - .OrderBy(g => g.Title); - - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); - } - public async Task> GetAllTagsAsync() { return await _context.Tag.ToListAsync(); diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 6437cfcfe..e55338c8b 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -757,7 +757,7 @@ public class UserRepository : IUserRepository /// - /// Fetches the AppUserId by API Key. This does not include any extra information + /// Fetches the UserId by API Key. This does not include any extra information /// /// /// diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index c08f80afa..74bfbb296 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -120,7 +120,7 @@ public static class Seed new AppUserSideNavStream() { Name = "browse-authors", - StreamType = SideNavStreamType.BrowsePeople, + StreamType = SideNavStreamType.BrowseAuthors, Order = 6, IsProvided = true, Visible = true diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index d72dd3bc7..c4a07dee7 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -33,7 +33,6 @@ public interface IUnitOfWork IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } IEmailHistoryRepository EmailHistoryRepository { get; } - IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } bool Commit(); Task CommitAsync(); bool HasChanges(); @@ -75,7 +74,6 @@ public class UnitOfWork : IUnitOfWork AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper); ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper); EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper); - AppUserReadingProfileRepository = new AppUserReadingProfileRepository(_context, _mapper); } /// @@ -105,7 +103,6 @@ public class UnitOfWork : IUnitOfWork public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } public IEmailHistoryRepository EmailHistoryRepository { get; } - public IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 848636209..50f795041 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -21,7 +21,6 @@ public class AppUser : IdentityUser, IHasConcurrencyToken public ICollection Ratings { get; set; } = null!; public ICollection ChapterRatings { get; set; } = null!; public AppUserPreferences UserPreferences { get; set; } = null!; - public ICollection ReadingProfiles { get; set; } = null!; /// /// Bookmarks associated with this User /// diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index b0f21bcba..b728e84e5 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using API.Data; +using API.Data; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; diff --git a/API/Entities/AppUserReadingProfile.cs b/API/Entities/AppUserReadingProfile.cs deleted file mode 100644 index 9b238b4f5..000000000 --- a/API/Entities/AppUserReadingProfile.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; - -namespace API.Entities; - -public enum BreakPoint -{ - [Description("Never")] - Never = 0, - [Description("Mobile")] - Mobile = 1, - [Description("Tablet")] - Tablet = 2, - [Description("Desktop")] - Desktop = 3, -} - -public class AppUserReadingProfile -{ - public int Id { get; set; } - - public string Name { get; set; } - public string NormalizedName { get; set; } - - public int AppUserId { get; set; } - public AppUser AppUser { get; set; } - - public ReadingProfileKind Kind { get; set; } - public List LibraryIds { get; set; } - public List SeriesIds { get; set; } - - #region MangaReader - - /// - /// Manga Reader Option: What direction should the next/prev page buttons go - /// - public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight; - /// - /// Manga Reader Option: How should the image be scaled to screen - /// - public ScalingOption ScalingOption { get; set; } = ScalingOption.Automatic; - /// - /// Manga Reader Option: Which side of a split image should we show first - /// - public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.FitSplit; - /// - /// Manga Reader Option: How the manga reader should perform paging or reading of the file - /// - /// Webtoon uses scrolling to page, MANGA_LR uses paging by clicking left/right side of reader, MANGA_UD uses paging - /// by clicking top/bottom sides of reader. - /// - /// - public ReaderMode ReaderMode { get; set; } - /// - /// Manga Reader Option: Allow the menu to close after 6 seconds without interaction - /// - public bool AutoCloseMenu { get; set; } = true; - /// - /// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change - /// - public bool ShowScreenHints { get; set; } = true; - /// - /// Manga Reader Option: Emulate a book by applying a shadow effect on the pages - /// - public bool EmulateBook { get; set; } = false; - /// - /// Manga Reader Option: How many pages to display in the reader at once - /// - public LayoutMode LayoutMode { get; set; } = LayoutMode.Single; - /// - /// Manga Reader Option: Background color of the reader - /// - public string BackgroundColor { get; set; } = "#000000"; - /// - /// Manga Reader Option: Should swiping trigger pagination - /// - public bool SwipeToPaginate { get; set; } - /// - /// Manga Reader Option: Allow Automatic Webtoon detection - /// - public bool AllowAutomaticWebtoonReaderDetection { get; set; } - /// - /// Manga Reader Option: Optional fixed width override - /// - public int? WidthOverride { get; set; } = null; - /// - /// Manga Reader Option: Disable the width override if the screen is past the breakpoint - /// - public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never; - - #endregion - - #region EpubReader - - /// - /// Book Reader Option: Override extra Margin - /// - public int BookReaderMargin { get; set; } = 15; - /// - /// Book Reader Option: Override line-height - /// - public int BookReaderLineSpacing { get; set; } = 100; - /// - /// Book Reader Option: Override font size - /// - public int BookReaderFontSize { get; set; } = 100; - /// - /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override - /// - public string BookReaderFontFamily { get; set; } = "default"; - /// - /// Book Reader Option: Allows tapping on side of screens to paginate - /// - public bool BookReaderTapToPaginate { get; set; } = false; - /// - /// Book Reader Option: What direction should the next/prev page buttons go - /// - public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight; - /// - /// Book Reader Option: Defines the writing styles vertical/horizontal - /// - public WritingStyle BookReaderWritingStyle { get; set; } = WritingStyle.Horizontal; - /// - /// Book Reader Option: The color theme to decorate the book contents - /// - /// Should default to Dark - public string BookThemeName { get; set; } = "Dark"; - /// - /// Book Reader Option: The way a page from a book is rendered. Default is as book dictates, 1 column is fit to height, - /// 2 column is fit to height, 2 columns - /// - /// Defaults to Default - public BookPageLayoutMode BookReaderLayoutMode { get; set; } = BookPageLayoutMode.Default; - /// - /// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this. - /// - /// Defaults to false - public bool BookReaderImmersiveMode { get; set; } = false; - #endregion - - #region PdfReader - - /// - /// PDF Reader: Theme of the Reader - /// - public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark; - /// - /// PDF Reader: Scroll mode of the reader - /// - public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; - /// - /// PDF Reader: Spread Mode of the reader - /// - public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; - - - #endregion -} diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index fe3646943..61a70c8a2 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -4,14 +4,13 @@ using System.Globalization; using API.Entities.Enums; using API.Entities.Interfaces; using API.Entities.Metadata; -using API.Entities.MetadataMatching; using API.Entities.Person; using API.Extensions; using API.Services.Tasks.Scanner.Parser; namespace API.Entities; -public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKPlusMetadata +public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage { public int Id { get; set; } /// @@ -127,11 +126,6 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP public string WebLinks { get; set; } = string.Empty; public string ISBN { get; set; } = string.Empty; - /// - /// Tracks which metadata has been set by K+ - /// - public IList KPlusOverrides { get; set; } = []; - /// /// (Kavita+) Average rating from Kavita+ metadata /// diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index a8d943b2d..b79315803 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -34,4 +34,9 @@ public enum LibraryType /// [Description("Comic")] ComicVine = 5, + /// + /// Uses Magazine regex and is restricted to PDF and Archive by default + /// + [Description("Magazine")] + Magazine = 6 } diff --git a/API/Entities/Enums/ReadingProfileKind.cs b/API/Entities/Enums/ReadingProfileKind.cs deleted file mode 100644 index 0f9cfa20b..000000000 --- a/API/Entities/Enums/ReadingProfileKind.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace API.Entities.Enums; - -public enum ReadingProfileKind -{ - /// - /// Generate by Kavita when registering a user, this is your default profile - /// - Default, - /// - /// Created by the user in the UI or via the API - /// - User, - /// - /// Automatically generated by Kavita to track changes made in the readers. Can be converted to a User Reading Profile. - /// - Implicit -} diff --git a/API/Entities/Interfaces/IHasKPlusMetadata.cs b/API/Entities/Interfaces/IHasKPlusMetadata.cs deleted file mode 100644 index 062afd7e1..000000000 --- a/API/Entities/Interfaces/IHasKPlusMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using API.Entities.MetadataMatching; - -namespace API.Entities.Interfaces; - -public interface IHasKPlusMetadata -{ - /// - /// Tracks which metadata has been set by K+ - /// - public IList KPlusOverrides { get; set; } -} diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 4a48fed99..abab81378 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -48,14 +48,6 @@ public class Library : IEntityDate, IHasCoverImage /// This does not exclude the library from being linked to wrt Series Relationships /// Requires a valid LicenseKey public bool AllowMetadataMatching { get; set; } = true; - /// - /// Should Kavita read metadata files from the library - /// - public bool EnableMetadata { get; set; } = true; - /// - /// Should Kavita remove sort articles "The" for the sort name - /// - public bool RemovePrefixForSortName { get; set; } = false; public DateTime Created { get; set; } diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index afcb23e97..f104f4c72 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -21,11 +21,6 @@ public class MangaFile : IEntityDate /// public required string FilePath { get; set; } /// - /// A hash of the document using Koreader's unique hashing algorithm - /// - /// KoreaderHash is only available for epub types - public string? KoreaderHash { get; set; } - /// /// Number of pages for the given file /// public int Pages { get; set; } diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index 8bb33fdc0..46e7241f5 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -4,14 +4,13 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using API.Entities.Enums; using API.Entities.Interfaces; -using API.Entities.MetadataMatching; using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Entities.Metadata; [Index(nameof(Id), nameof(SeriesId), IsUnique = true)] -public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata +public class SeriesMetadata : IHasConcurrencyToken { public int Id { get; set; } @@ -43,10 +42,6 @@ public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata /// /// This is not populated from Chapters of the Series public string WebLinks { get; set; } = string.Empty; - /// - /// Tracks which metadata has been set by K+ - /// - public IList KPlusOverrides { get; set; } = []; #region Locks diff --git a/API/Entities/Person/Person.cs b/API/Entities/Person/Person.cs index ed57fd6d3..8eed08f5c 100644 --- a/API/Entities/Person/Person.cs +++ b/API/Entities/Person/Person.cs @@ -8,7 +8,8 @@ public class Person : IHasCoverImage public int Id { get; set; } public required string Name { get; set; } public required string NormalizedName { get; set; } - public ICollection Aliases { get; set; } = []; + + //public ICollection Aliases { get; set; } = default!; public string? CoverImage { get; set; } public bool CoverImageLocked { get; set; } @@ -46,8 +47,8 @@ public class Person : IHasCoverImage //public long MetronId { get; set; } = 0; // Relationships - public ICollection ChapterPeople { get; set; } = []; - public ICollection SeriesMetadataPeople { get; set; } = []; + public ICollection ChapterPeople { get; set; } = new List(); + public ICollection SeriesMetadataPeople { get; set; } = new List(); public void ResetColorScape() diff --git a/API/Entities/Person/PersonAlias.cs b/API/Entities/Person/PersonAlias.cs deleted file mode 100644 index f053f608d..000000000 --- a/API/Entities/Person/PersonAlias.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace API.Entities.Person; - -public class PersonAlias -{ - public int Id { get; set; } - public required string Alias { get; set; } - public required string NormalizedAlias { get; set; } - - public int PersonId { get; set; } - public Person Person { get; set; } -} diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/API/Entities/Scrobble/ScrobbleEvent.cs index 8adfdcc2e..b8708c115 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/API/Entities/Scrobble/ScrobbleEvent.cs @@ -68,14 +68,4 @@ public class ScrobbleEvent : IEntityDate public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } public DateTime LastModifiedUtc { get; set; } - - /// - /// Sets the ErrorDetail and marks the event as - /// - /// - public void SetErrorMessage(string errorMessage) - { - ErrorDetails = errorMessage; - IsErrored = true; - } } diff --git a/API/Entities/SideNavStreamType.cs b/API/Entities/SideNavStreamType.cs index 62f429889..545c630d8 100644 --- a/API/Entities/SideNavStreamType.cs +++ b/API/Entities/SideNavStreamType.cs @@ -10,5 +10,5 @@ public enum SideNavStreamType ExternalSource = 6, AllSeries = 7, WantToRead = 8, - BrowsePeople = 9 + BrowseAuthors = 9 } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index bd4783f25..e004fcc25 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -53,9 +53,6 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -76,7 +73,6 @@ public static class ApplicationServiceExtensions services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs index 9bc06bab4..4e84e2fa5 100644 --- a/API/Extensions/EnumerableExtensions.cs +++ b/API/Extensions/EnumerableExtensions.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using API.Data.Misc; -using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; namespace API.Extensions; #nullable enable @@ -44,28 +42,4 @@ public static class EnumerableExtensions return q; } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } } diff --git a/API/Extensions/IHasKPlusMetadataExtensions.cs b/API/Extensions/IHasKPlusMetadataExtensions.cs deleted file mode 100644 index 84e35adc4..000000000 --- a/API/Extensions/IHasKPlusMetadataExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using API.Entities.Interfaces; -using API.Entities.MetadataMatching; - -namespace API.Extensions; - -public static class IHasKPlusMetadataExtensions -{ - - public static bool HasSetKPlusMetadata(this IHasKPlusMetadata hasKPlusMetadata, MetadataSettingField field) - { - return hasKPlusMetadata.KPlusOverrides.Contains(field); - } - - public static void AddKPlusOverride(this IHasKPlusMetadata hasKPlusMetadata, MetadataSettingField field) - { - if (hasKPlusMetadata.KPlusOverrides.Contains(field)) return; - - hasKPlusMetadata.KPlusOverrides.Add(field); - } - -} diff --git a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs b/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs deleted file mode 100644 index c36164d9d..000000000 --- a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Entities.Person; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; - -public static class PersonFilter -{ - public static IQueryable HasPersonName(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (string.IsNullOrEmpty(queryString) || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(p => p.Name.Equals(queryString)), - FilterComparison.BeginsWith => queryable.Where(p => EF.Functions.Like(p.Name, $"{queryString}%")), - FilterComparison.EndsWith => queryable.Where(p => EF.Functions.Like(p.Name, $"%{queryString}")), - FilterComparison.Matches => queryable.Where(p => EF.Functions.Like(p.Name, $"%{queryString}%")), - FilterComparison.NotEqual => queryable.Where(p => p.Name != queryString), - FilterComparison.NotContains or FilterComparison.GreaterThan or FilterComparison.GreaterThanEqual - or FilterComparison.LessThan or FilterComparison.LessThanEqual or FilterComparison.Contains - or FilterComparison.IsBefore or FilterComparison.IsAfter or FilterComparison.IsInLast - or FilterComparison.IsNotInLast or FilterComparison.MustContains - or FilterComparison.IsEmpty => - throw new KavitaException($"{comparison} not applicable for Person.Name"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, - "Filter Comparison is not supported") - }; - } - public static IQueryable HasPersonRole(this IQueryable queryable, bool condition, - FilterComparison comparison, IList roles) - { - if (roles == null || roles.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Contains or FilterComparison.MustContains => queryable.Where(p => - p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) || - p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))), - FilterComparison.NotContains => queryable.Where(p => - !p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) && - !p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))), - FilterComparison.Equal or FilterComparison.NotEqual or FilterComparison.BeginsWith - or FilterComparison.EndsWith or FilterComparison.Matches or FilterComparison.GreaterThan - or FilterComparison.GreaterThanEqual or FilterComparison.LessThan or FilterComparison.LessThanEqual - or FilterComparison.IsBefore or FilterComparison.IsAfter or FilterComparison.IsInLast - or FilterComparison.IsNotInLast - or FilterComparison.IsEmpty => - throw new KavitaException($"{comparison} not applicable for Person.Role"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, - "Filter Comparison is not supported") - }; - } - - public static IQueryable HasPersonSeriesCount(this IQueryable queryable, bool condition, - FilterComparison comparison, int count) - { - if (!condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() == count), - FilterComparison.GreaterThan => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() > count), - FilterComparison.GreaterThanEqual => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() >= count), - FilterComparison.LessThan => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() < count), - FilterComparison.LessThanEqual => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() <= count), - FilterComparison.NotEqual => queryable.Where(p => p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count() != count), - FilterComparison.BeginsWith or FilterComparison.EndsWith or FilterComparison.Matches - or FilterComparison.Contains or FilterComparison.NotContains or FilterComparison.IsBefore - or FilterComparison.IsAfter or FilterComparison.IsInLast or FilterComparison.IsNotInLast - or FilterComparison.MustContains - or FilterComparison.IsEmpty => throw new KavitaException( - $"{comparison} not applicable for Person.SeriesCount"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported") - }; - } - - public static IQueryable HasPersonChapterCount(this IQueryable queryable, bool condition, - FilterComparison comparison, int count) - { - if (!condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(p => - p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() == count), - FilterComparison.GreaterThan => queryable.Where(p => p.ChapterPeople - .Select(cp => cp.Chapter.Id) - .Distinct() - .Count() > count), - FilterComparison.GreaterThanEqual => queryable.Where(p => p.ChapterPeople - .Select(cp => cp.Chapter.Id) - .Distinct() - .Count() >= count), - FilterComparison.LessThan => queryable.Where(p => - p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() < count), - FilterComparison.LessThanEqual => queryable.Where(p => p.ChapterPeople - .Select(cp => cp.Chapter.Id) - .Distinct() - .Count() <= count), - FilterComparison.NotEqual => queryable.Where(p => - p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() != count), - FilterComparison.BeginsWith or FilterComparison.EndsWith or FilterComparison.Matches - or FilterComparison.Contains or FilterComparison.NotContains or FilterComparison.IsBefore - or FilterComparison.IsAfter or FilterComparison.IsInLast or FilterComparison.IsNotInLast - or FilterComparison.MustContains - or FilterComparison.IsEmpty => throw new KavitaException( - $"{comparison} not applicable for Person.ChapterCount"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported") - }; - } -} diff --git a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs index d7acf9381..cc40491d0 100644 --- a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using API.Data.Misc; using API.Data.Repositories; @@ -50,26 +49,23 @@ public static class SearchQueryableExtensions // Get people from SeriesMetadata var peopleFromSeriesMetadata = queryable .Where(sm => seriesIds.Contains(sm.SeriesId)) - .SelectMany(sm => sm.People.Select(sp => sp.Person)) - .Where(p => - EF.Functions.Like(p.Name, $"%{searchQuery}%") || - p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%")) - ); + .SelectMany(sm => sm.People) + .Where(p => p.Person.Name != null && EF.Functions.Like(p.Person.Name, $"%{searchQuery}%")) + .Select(p => p.Person); + // Get people from ChapterPeople by navigating through Volume -> Series var peopleFromChapterPeople = queryable .Where(sm => seriesIds.Contains(sm.SeriesId)) .SelectMany(sm => sm.Series.Volumes) .SelectMany(v => v.Chapters) - .SelectMany(ch => ch.People.Select(cp => cp.Person)) - .Where(p => - EF.Functions.Like(p.Name, $"%{searchQuery}%") || - p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%")) - ); + .SelectMany(ch => ch.People) + .Where(cp => cp.Person.Name != null && EF.Functions.Like(cp.Person.Name, $"%{searchQuery}%")) + .Select(cp => cp.Person); // Combine both queries and ensure distinct results return peopleFromSeriesMetadata .Union(peopleFromChapterPeople) - .Select(p => p) + .Distinct() .OrderBy(p => p.NormalizedName); } diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index bfc585455..864c4e5a1 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -1,7 +1,7 @@ using System.Linq; using API.Data.Repositories; using API.Entities; -using API.Entities.Person; +using API.Entities.Metadata; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; @@ -321,25 +321,4 @@ public static class IncludesExtensions return query.AsSplitQuery(); } - - public static IQueryable Includes(this IQueryable queryable, PersonIncludes includeFlags) - { - - if (includeFlags.HasFlag(PersonIncludes.Aliases)) - { - queryable = queryable.Include(p => p.Aliases); - } - - if (includeFlags.HasFlag(PersonIncludes.ChapterPeople)) - { - queryable = queryable.Include(p => p.ChapterPeople); - } - - if (includeFlags.HasFlag(PersonIncludes.SeriesPeople)) - { - queryable = queryable.Include(p => p.SeriesMetadataPeople); - } - - return queryable; - } } diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index ef2af721f..a2db1dde7 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -5,13 +5,10 @@ using System.Linq.Expressions; using System.Threading.Tasks; using API.Data.Misc; using API.Data.Repositories; -using API.DTOs; using API.DTOs.Filtering; using API.DTOs.KavitaPlus.Manage; -using API.DTOs.Metadata.Browse; using API.Entities; using API.Entities.Enums; -using API.Entities.Person; using API.Entities.Scrobble; using Microsoft.EntityFrameworkCore; @@ -276,27 +273,6 @@ public static class QueryableExtensions }; } - public static IQueryable SortBy(this IQueryable query, PersonSortOptions? sort) - { - if (sort == null) - { - return query.OrderBy(p => p.Name); - } - - return sort.SortField switch - { - PersonSortField.Name when sort.IsAscending => query.OrderBy(p => p.Name), - PersonSortField.Name => query.OrderByDescending(p => p.Name), - PersonSortField.SeriesCount when sort.IsAscending => query.OrderBy(p => p.SeriesMetadataPeople.Count), - PersonSortField.SeriesCount => query.OrderByDescending(p => p.SeriesMetadataPeople.Count), - PersonSortField.ChapterCount when sort.IsAscending => query.OrderBy(p => p.ChapterPeople.Count), - PersonSortField.ChapterCount => query.OrderByDescending(p => p.ChapterPeople.Count), - _ => query.OrderBy(p => p.Name) - }; - - - } - /// /// Performs either OrderBy or OrderByDescending on the given query based on the value of SortOptions.IsAscending. /// diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index e0738bdf3..fc3314f58 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -3,7 +3,6 @@ using System.Linq; using API.Data.Misc; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Entities.Person; namespace API.Extensions.QueryExtensions; @@ -27,20 +26,6 @@ public static class RestrictByAgeExtensions return q; } - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - var q = queryable.Where(s => s.SeriesMetadata.AgeRating <= restriction.AgeRating); - - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.SeriesMetadata.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; @@ -54,20 +39,21 @@ public static class RestrictByAgeExtensions return q; } - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) + [Obsolete] + public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - var q = queryable.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) + if (restriction.IncludeUnknowns) { - return q.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating != AgeRating.Unknown); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating)); } - return q; + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); } - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; @@ -82,27 +68,18 @@ public static class RestrictByAgeExtensions sm.Metadata.AgeRating <= restriction.AgeRating && sm.Metadata.AgeRating > AgeRating.Unknown)); } - /// - /// Returns all Genres where any of the linked Series/Chapters are less than or equal to restriction age rating - /// - /// - /// - /// public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; if (restriction.IncludeUnknowns) { - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating && sm.AgeRating != AgeRating.Unknown) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating && cp.AgeRating != AgeRating.Unknown) - ); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) @@ -111,15 +88,12 @@ public static class RestrictByAgeExtensions if (restriction.IncludeUnknowns) { - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => - c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating && sm.AgeRating != AgeRating.Unknown) || - c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating && cp.AgeRating != AgeRating.Unknown) - ); + return queryable.Where(c => c.SeriesMetadatas.All(sm => + sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) diff --git a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs b/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs deleted file mode 100644 index 9ec1b8621..000000000 --- a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq; -using API.Entities; -using API.Entities.Person; - -namespace API.Extensions.QueryExtensions; - -public static class RestrictByLibraryExtensions -{ - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(p => - p.ChapterPeople.Any(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)) || - p.SeriesMetadataPeople.Any(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId))); - } - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(cp => userLibs.Contains(cp.Volume.Series.LibraryId)); - } - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId)); - } - - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) - { - return query.Where(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)); - } -} diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index bb7511c64..334403ab3 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -15,7 +15,6 @@ using API.DTOs.KavitaPlus.Manage; using API.DTOs.KavitaPlus.Metadata; using API.DTOs.MediaErrors; using API.DTOs.Metadata; -using API.DTOs.Person; using API.DTOs.Progress; using API.DTOs.Reader; using API.DTOs.ReadingLists; @@ -69,8 +68,7 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName)) .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)); - CreateMap() - .ForMember(dest => dest.Aliases, opt => opt.MapFrom(src => src.Aliases.Select(s => s.Alias))); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); @@ -275,19 +273,19 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.Theme, opt => - opt.MapFrom(src => src.Theme)); - - CreateMap() + opt.MapFrom(src => src.Theme)) .ForMember(dest => dest.BookReaderThemeName, opt => - opt.MapFrom(src => src.BookThemeName)); + opt.MapFrom(src => src.BookThemeName)) + .ForMember(dest => dest.BookReaderLayoutMode, + opt => + opt.MapFrom(src => src.BookReaderLayoutMode)); CreateMap(); CreateMap() - .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)) - .ForMember(dest => dest.OwnerUserName, opt => opt.MapFrom(src => src.AppUser.UserName)); + .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)); CreateMap(); CreateMap(); CreateMap(); diff --git a/API/Helpers/BookSortTitlePrefixHelper.cs b/API/Helpers/BookSortTitlePrefixHelper.cs deleted file mode 100644 index c92df5d65..000000000 --- a/API/Helpers/BookSortTitlePrefixHelper.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace API.Helpers; - -/// -/// Responsible for parsing book titles "The man on the street" and removing the prefix -> "man on the street". -/// -/// This code is performance sensitive -public static class BookSortTitlePrefixHelper -{ - private static readonly Dictionary PrefixLookup; - private static readonly Dictionary> PrefixesByFirstChar; - - static BookSortTitlePrefixHelper() - { - var prefixes = new[] - { - // English - "the", "a", "an", - // Spanish - "el", "la", "los", "las", "un", "una", "unos", "unas", - // French - "le", "la", "les", "un", "une", "des", - // German - "der", "die", "das", "den", "dem", "ein", "eine", "einen", "einer", - // Italian - "il", "lo", "la", "gli", "le", "un", "uno", "una", - // Portuguese - "o", "a", "os", "as", "um", "uma", "uns", "umas", - // Russian (transliterated common ones) - "в", "на", "с", "к", "от", "для", - }; - - // Build lookup structures - PrefixLookup = new Dictionary(prefixes.Length, StringComparer.OrdinalIgnoreCase); - PrefixesByFirstChar = new Dictionary>(); - - foreach (var prefix in prefixes) - { - PrefixLookup[prefix] = 1; - - var firstChar = char.ToLowerInvariant(prefix[0]); - if (!PrefixesByFirstChar.TryGetValue(firstChar, out var list)) - { - list = []; - PrefixesByFirstChar[firstChar] = list; - } - list.Add(prefix); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ReadOnlySpan GetSortTitle(ReadOnlySpan title) - { - if (title.IsEmpty) return title; - - // Fast detection of script type by first character - var firstChar = title[0]; - - // CJK Unicode ranges - no processing needed for most cases - if ((firstChar >= 0x4E00 && firstChar <= 0x9FFF) || // CJK Unified - (firstChar >= 0x3040 && firstChar <= 0x309F) || // Hiragana - (firstChar >= 0x30A0 && firstChar <= 0x30FF)) // Katakana - { - return title; - } - - var firstSpaceIndex = title.IndexOf(' '); - if (firstSpaceIndex <= 0) return title; - - var potentialPrefix = title.Slice(0, firstSpaceIndex); - - // Fast path: check if first character could match any prefix - firstChar = char.ToLowerInvariant(potentialPrefix[0]); - if (!PrefixesByFirstChar.ContainsKey(firstChar)) - return title; - - // Only do the expensive lookup if first character matches - if (PrefixLookup.ContainsKey(potentialPrefix.ToString())) - { - var remainder = title.Slice(firstSpaceIndex + 1); - return remainder.IsEmpty ? title : remainder; - } - - return title; - } - - /// - /// Removes the sort prefix - /// - /// - /// - public static string GetSortTitle(string title) - { - var result = GetSortTitle(title.AsSpan()); - - return result.ToString(); - } -} diff --git a/API/Helpers/Builders/AppUserBuilder.cs b/API/Helpers/Builders/AppUserBuilder.cs index 7ffac355e..282361e41 100644 --- a/API/Helpers/Builders/AppUserBuilder.cs +++ b/API/Helpers/Builders/AppUserBuilder.cs @@ -21,7 +21,7 @@ public class AppUserBuilder : IEntityBuilder ApiKey = HashUtil.ApiKey(), UserPreferences = new AppUserPreferences { - Theme = theme ?? Seed.DefaultThemes.First(), + Theme = theme ?? Seed.DefaultThemes.First() }, ReadingLists = new List(), Bookmarks = new List(), @@ -31,8 +31,7 @@ public class AppUserBuilder : IEntityBuilder Devices = new List(), Id = 0, DashboardStreams = new List(), - SideNavStreams = new List(), - ReadingProfiles = [], + SideNavStreams = new List() }; } diff --git a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs b/API/Helpers/Builders/AppUserReadingProfileBuilder.cs deleted file mode 100644 index 26da5fd86..000000000 --- a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -using API.Entities; -using API.Entities.Enums; -using API.Extensions; - -namespace API.Helpers.Builders; - -public class AppUserReadingProfileBuilder -{ - private readonly AppUserReadingProfile _profile; - - public AppUserReadingProfile Build() => _profile; - - /// - /// The profile's kind will be unless overwritten with - /// - /// - public AppUserReadingProfileBuilder(int userId) - { - _profile = new AppUserReadingProfile - { - AppUserId = userId, - Kind = ReadingProfileKind.User, - SeriesIds = [], - LibraryIds = [] - }; - } - - public AppUserReadingProfileBuilder WithSeries(Series series) - { - _profile.SeriesIds.Add(series.Id); - return this; - } - - public AppUserReadingProfileBuilder WithLibrary(Library library) - { - _profile.LibraryIds.Add(library.Id); - return this; - } - - public AppUserReadingProfileBuilder WithKind(ReadingProfileKind kind) - { - _profile.Kind = kind; - return this; - } - - public AppUserReadingProfileBuilder WithName(string name) - { - _profile.Name = name; - _profile.NormalizedName = name.ToNormalized(); - return this; - } - - -} diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index d9976d92a..f85c21595 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -156,24 +156,4 @@ public class ChapterBuilder : IEntityBuilder return this; } - - public ChapterBuilder WithTags(IList tags) - { - _chapter.Tags ??= []; - foreach (var tag in tags) - { - _chapter.Tags.Add(tag); - } - return this; - } - - public ChapterBuilder WithGenres(IList genres) - { - _chapter.Genres ??= []; - foreach (var genre in genres) - { - _chapter.Genres.Add(genre); - } - return this; - } } diff --git a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs b/API/Helpers/Builders/KoreaderBookDtoBuilder.cs deleted file mode 100644 index debbe0347..000000000 --- a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; -using API.DTOs.Koreader; - -namespace API.Helpers.Builders; - -public class KoreaderBookDtoBuilder : IEntityBuilder -{ - private readonly KoreaderBookDto _dto; - public KoreaderBookDto Build() => _dto; - - public KoreaderBookDtoBuilder(string documentHash) - { - _dto = new KoreaderBookDto() - { - Document = documentHash, - Device = "Kavita" - }; - } - - public KoreaderBookDtoBuilder WithDocument(string documentHash) - { - _dto.Document = documentHash; - return this; - } - - public KoreaderBookDtoBuilder WithProgress(string progress) - { - _dto.Progress = progress; - return this; - } - - public KoreaderBookDtoBuilder WithPercentage(int? pageNum, int pages) - { - _dto.Percentage = (pageNum ?? 0) / (float) pages; - return this; - } - - public KoreaderBookDtoBuilder WithDeviceId(string installId, int userId) - { - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(installId + userId)); - _dto.Device_id = Convert.ToHexString(hash); - return this; - } -} diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index 950c5d3d2..30e6136a5 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -110,12 +110,6 @@ public class LibraryBuilder : IEntityBuilder return this; } - public LibraryBuilder WithEnableMetadata(bool enable) - { - _library.EnableMetadata = enable; - return this; - } - public LibraryBuilder WithAllowScrobbling(bool allowScrobbling) { _library.AllowScrobbling = allowScrobbling; diff --git a/API/Helpers/Builders/MangaFileBuilder.cs b/API/Helpers/Builders/MangaFileBuilder.cs index ea3ff0c6d..5387a3349 100644 --- a/API/Helpers/Builders/MangaFileBuilder.cs +++ b/API/Helpers/Builders/MangaFileBuilder.cs @@ -60,17 +60,4 @@ public class MangaFileBuilder : IEntityBuilder _mangaFile.Id = Math.Max(id, 0); return this; } - - /// - /// Generate the Hash on the underlying file - /// - /// Only applicable to Epubs - public MangaFileBuilder WithHash() - { - if (_mangaFile.Format != MangaFormat.Epub) return this; - - _mangaFile.KoreaderHash = KoreaderHelper.HashContents(_mangaFile.FilePath); - - return this; - } } diff --git a/API/Helpers/Builders/PersonAliasBuilder.cs b/API/Helpers/Builders/PersonAliasBuilder.cs deleted file mode 100644 index e54ea8975..000000000 --- a/API/Helpers/Builders/PersonAliasBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -using API.Entities.Person; -using API.Extensions; - -namespace API.Helpers.Builders; - -public class PersonAliasBuilder : IEntityBuilder -{ - private readonly PersonAlias _alias; - public PersonAlias Build() => _alias; - - public PersonAliasBuilder(string name) - { - _alias = new PersonAlias() - { - Alias = name.Trim(), - NormalizedAlias = name.ToNormalized(), - }; - } -} diff --git a/API/Helpers/Builders/PersonBuilder.cs b/API/Helpers/Builders/PersonBuilder.cs index afd0c84af..492d79e17 100644 --- a/API/Helpers/Builders/PersonBuilder.cs +++ b/API/Helpers/Builders/PersonBuilder.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; -using System.Linq; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; using API.Entities.Person; using API.Extensions; @@ -32,20 +34,6 @@ public class PersonBuilder : IEntityBuilder return this; } - public PersonBuilder WithAlias(string alias) - { - if (_person.Aliases.Any(a => a.NormalizedAlias.Equals(alias.ToNormalized()))) - { - return this; - } - - _person.Aliases.Add(new PersonAliasBuilder(alias).Build()); - - return this; - } - - - public PersonBuilder WithSeriesMetadata(SeriesMetadataPeople seriesMetadataPeople) { _person.SeriesMetadataPeople.Add(seriesMetadataPeople); diff --git a/API/Helpers/Builders/SeriesMetadataBuilder.cs b/API/Helpers/Builders/SeriesMetadataBuilder.cs index 462bc4455..8ceb16d95 100644 --- a/API/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/API/Helpers/Builders/SeriesMetadataBuilder.cs @@ -108,23 +108,4 @@ public class SeriesMetadataBuilder : IEntityBuilder _seriesMetadata.TagsLocked = lockStatus; return this; } - - public SeriesMetadataBuilder WithTags(List tags, bool lockStatus = false) - { - _seriesMetadata.Tags = tags; - _seriesMetadata.TagsLocked = lockStatus; - return this; - } - - public SeriesMetadataBuilder WithMaxCount(int count) - { - _seriesMetadata.MaxCount = count; - return this; - } - - public SeriesMetadataBuilder WithTotalCount(int count) - { - _seriesMetadata.TotalCount = count; - return this; - } } diff --git a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs b/API/Helpers/Converters/PersonFilterFieldValueConverter.cs deleted file mode 100644 index 822ce105a..000000000 --- a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; - -namespace API.Helpers.Converters; - -public static class PersonFilterFieldValueConverter -{ - public static object ConvertValue(PersonFilterField field, string value) - { - return field switch - { - PersonFilterField.Name => value, - PersonFilterField.Role => ParsePersonRoles(value), - PersonFilterField.SeriesCount => int.Parse(value), - PersonFilterField.ChapterCount => int.Parse(value), - _ => throw new ArgumentOutOfRangeException(nameof(field), field, "Field is not supported") - }; - } - - private static IList ParsePersonRoles(string value) - { - if (string.IsNullOrEmpty(value)) return []; - - return value.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(v => Enum.Parse(v.Trim())) - .ToList(); - } -} diff --git a/API/Helpers/KoreaderHelper.cs b/API/Helpers/KoreaderHelper.cs deleted file mode 100644 index e779cd911..000000000 --- a/API/Helpers/KoreaderHelper.cs +++ /dev/null @@ -1,113 +0,0 @@ -using API.DTOs.Progress; -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Helpers; - -/// -/// All things related to Koreader -/// -/// Original developer: https://github.com/MFDeAngelo -public static class KoreaderHelper -{ - /// - /// Hashes the document according to a custom Koreader hashing algorithm. - /// Look at the util.partialMD5 method in the attached link. - /// Note: Only applies to epub files - /// - /// The hashing algorithm is relatively quick as it only hashes ~10,000 bytes for the biggest of files. - /// - /// The path to the file to hash - public static string HashContents(string filePath) - { - if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath) || !Parser.IsEpub(filePath)) - { - return null; - } - - using var file = File.OpenRead(filePath); - - const int step = 1024; - const int size = 1024; - var md5 = MD5.Create(); - var buffer = new byte[size]; - - for (var i = -1; i < 10; i++) - { - file.Position = step << 2 * i; - var bytesRead = file.Read(buffer, 0, size); - if (bytesRead > 0) - { - md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); - } - else - { - break; - } - } - - file.Close(); - md5.TransformFinalBlock([], 0, 0); - - return md5.Hash == null ? null : Convert.ToHexString(md5.Hash).ToUpper(); - } - - /// - /// Koreader can identify documents based on contents or title. - /// For now, we only support by contents. - /// - public static string HashTitle(string filePath) - { - var fileName = Path.GetFileName(filePath); - var fileNameBytes = Encoding.ASCII.GetBytes(fileName); - var bytes = MD5.HashData(fileNameBytes); - - return Convert.ToHexString(bytes); - } - - public static void UpdateProgressDto(ProgressDto progress, string koreaderPosition) - { - var path = koreaderPosition.Split('/'); - if (path.Length < 6) - { - return; - } - - var docNumber = path[2].Replace("DocFragment[", string.Empty).Replace("]", string.Empty); - progress.PageNum = int.Parse(docNumber) - 1; - var lastTag = path[5].ToUpper(); - - if (lastTag == "A") - { - progress.BookScrollId = null; - } - else - { - // The format that Kavita accepts as a progress string. It tells Kavita where Koreader last left off. - progress.BookScrollId = $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/{lastTag}"; - } - } - - - public static string GetKoreaderPosition(ProgressDto progressDto) - { - string lastTag; - var koreaderPageNumber = progressDto.PageNum + 1; - - if (string.IsNullOrEmpty(progressDto.BookScrollId)) - { - lastTag = "a"; - } - else - { - var tokens = progressDto.BookScrollId.Split('/'); - lastTag = tokens[^1].ToLower(); - } - - // The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off. - return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}"; - } -} diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index b71ff2c1a..07161e418 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -17,20 +17,6 @@ namespace API.Helpers; public static class PersonHelper { - public static Dictionary ConstructNameAndAliasDictionary(IList people) - { - var dict = new Dictionary(); - foreach (var person in people) - { - dict.TryAdd(person.NormalizedName, person); - foreach (var alias in person.Aliases) - { - dict.TryAdd(alias.NormalizedAlias, person); - } - } - return dict; - } - public static async Task UpdateSeriesMetadataPeopleAsync(SeriesMetadata metadata, ICollection metadataPeople, IEnumerable chapterPeople, PersonRole role, IUnitOfWork unitOfWork) { @@ -52,9 +38,7 @@ public static class PersonHelper // Identify people to remove from metadataPeople var peopleToRemove = existingMetadataPeople - .Where(person => - !peopleToAddSet.Contains(person.Person.NormalizedName) && - !person.Person.Aliases.Any(pa => peopleToAddSet.Contains(pa.NormalizedAlias))) + .Where(person => !peopleToAddSet.Contains(person.Person.NormalizedName)) .ToList(); // Remove identified people from metadataPeople @@ -69,7 +53,11 @@ public static class PersonHelper .GetPeopleByNames(peopleToAdd.Select(p => p.NormalizedName).ToList()); // Prepare a dictionary for quick lookup of existing people by normalized name - var existingPeopleDict = ConstructNameAndAliasDictionary(existingPeopleInDb); + var existingPeopleDict = new Dictionary(); + foreach (var person in existingPeopleInDb) + { + existingPeopleDict.TryAdd(person.NormalizedName, person); + } // Track the people to attach (newly created people) var peopleToAttach = new List(); @@ -141,12 +129,15 @@ public static class PersonHelper var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedPeople); // Prepare a dictionary for quick lookup by normalized name - var existingPeopleDict = ConstructNameAndAliasDictionary(existingPeople); + var existingPeopleDict = new Dictionary(); + foreach (var person in existingPeople) + { + existingPeopleDict.TryAdd(person.NormalizedName, person); + } // Identify people to remove (those present in ChapterPeople but not in the new list) - var toRemove = existingChapterPeople - .Where(existingChapterPerson => !normalizedPeople.Contains(existingChapterPerson.Person.NormalizedName)); - foreach (var existingChapterPerson in toRemove) + foreach (var existingChapterPerson in existingChapterPeople + .Where(existingChapterPerson => !normalizedPeople.Contains(existingChapterPerson.Person.NormalizedName))) { chapter.People.Remove(existingChapterPerson); unitOfWork.PersonRepository.Remove(existingChapterPerson); diff --git a/API/I18N/as.json b/API/I18N/as.json deleted file mode 100644 index 0967ef424..000000000 --- a/API/I18N/as.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/API/I18N/cs.json b/API/I18N/cs.json index e136d8e75..4b9774218 100644 --- a/API/I18N/cs.json +++ b/API/I18N/cs.json @@ -207,7 +207,5 @@ "dashboard-stream-only-delete-smart-filter": "Z ovládacího panelu lze odstranit pouze streamy chytrých filtrů", "smart-filter-name-required": "Vyžaduje se název chytrého filtru", "smart-filter-system-name": "Nelze použít název streamu poskytovaného systémem", - "sidenav-stream-only-delete-smart-filter": "Z postranní navigace lze odstranit pouze streamy chytrých filtrů", - "aliases-have-overlap": "Jeden nebo více aliasů se překrývají s jinými osobami, nelze je aktualizovat", - "generated-reading-profile-name": "Generováno z {0}" + "sidenav-stream-only-delete-smart-filter": "Z postranní navigace lze odstranit pouze streamy chytrých filtrů" } diff --git a/API/I18N/de.json b/API/I18N/de.json index a6c865897..d91cc8f25 100644 --- a/API/I18N/de.json +++ b/API/I18N/de.json @@ -207,7 +207,5 @@ "sidenav-stream-only-delete-smart-filter": "Nur Smart-Filter-Streams können aus der Seitennavigation gelöscht werden", "dashboard-stream-only-delete-smart-filter": "Nur Smart-Filter-Streams können aus dem Dashboard gelöscht werden", "smart-filter-system-name": "Du kannst den Namen eines vom System bereitgestellten Streams nicht verwenden", - "smart-filter-name-required": "Name des Smart Filters erforderlich", - "aliases-have-overlap": "Ein oder mehrere Aliasnamen sind mit anderen Personen identisch und können nicht aktualisiert werden", - "generated-reading-profile-name": "Erstellt aus {0}" + "smart-filter-name-required": "Name des Smart Filters erforderlich" } diff --git a/API/I18N/en.json b/API/I18N/en.json index d3cd1ecd3..6e37a3cd9 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -212,7 +212,6 @@ "user-no-access-library-from-series": "User does not have access to the library this series belongs to", "series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions", "kavitaplus-restricted": "This is restricted to Kavita+ only", - "aliases-have-overlap": "One or more of the aliases have overlap with other people, cannot update", "volume-num": "Volume {0}", "book-num": "Book {0}", @@ -230,8 +229,6 @@ "scan-libraries": "Scan Libraries", "kavita+-data-refresh": "Kavita+ Data Refresh", "backup": "Backup", - "update-yearly-stats": "Update Yearly Stats", - - "generated-reading-profile-name": "Generated from {0}" + "update-yearly-stats": "Update Yearly Stats" } diff --git a/API/I18N/fr.json b/API/I18N/fr.json index 2b9a4f81b..6b3dc735a 100644 --- a/API/I18N/fr.json +++ b/API/I18N/fr.json @@ -207,7 +207,5 @@ "sidenav-stream-only-delete-smart-filter": "Seuls les flux de filtres intelligents peuvent être supprimés de la SideNav", "dashboard-stream-only-delete-smart-filter": "Seuls les flux de filtres intelligents peuvent être supprimés du tableau de bord", "smart-filter-name-required": "Nom du filtre intelligent requis", - "smart-filter-system-name": "Vous ne pouvez pas utiliser le nom d'un flux fourni par le système", - "aliases-have-overlap": "Un ou plusieurs alias se chevauchent avec d'autres personnes et ne peuvent pas être mis à jour", - "generated-reading-profile-name": "Généré à partir de {0}" + "smart-filter-system-name": "Vous ne pouvez pas utiliser le nom d'un flux fourni par le système" } diff --git a/API/I18N/ga.json b/API/I18N/ga.json index 142425aec..2d16bcb05 100644 --- a/API/I18N/ga.json +++ b/API/I18N/ga.json @@ -207,7 +207,5 @@ "smart-filter-system-name": "Ní féidir leat ainm srutha an chórais a sholáthair tú a úsáid", "sidenav-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh as an SideNav", "dashboard-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh ón deais", - "smart-filter-name-required": "Ainm Scagaire Cliste ag teastáil", - "aliases-have-overlap": "Tá forluí idir ceann amháin nó níos mó de na leasainmneacha agus daoine eile, ní féidir iad a nuashonrú", - "generated-reading-profile-name": "Gineadh ó {0}" + "smart-filter-name-required": "Ainm Scagaire Cliste ag teastáil" } diff --git a/API/I18N/he.json b/API/I18N/he.json index 3b2386bf6..41a9a7de7 100644 --- a/API/I18N/he.json +++ b/API/I18N/he.json @@ -21,6 +21,5 @@ "age-restriction-update": "אירעה תקלה בעת עדכון הגבלת גיל", "generic-user-update": "אירעה תקלה בעת עדכון משתמש", "user-already-registered": "משתמש רשום כבר בתור {0}", - "manual-setup-fail": "לא מתאפשר להשלים הגדרה ידנית. יש לבטל וליצור מחדש את ההזמנה", - "email-taken": "דואר אלקטרוני כבר בשימוש" + "manual-setup-fail": "לא מתאפשר להשלים הגדרה ידנית. יש לבטל וליצור מחדש את ההזמנה" } diff --git a/API/I18N/hu.json b/API/I18N/hu.json index 21649e2ce..7c9473116 100644 --- a/API/I18N/hu.json +++ b/API/I18N/hu.json @@ -196,9 +196,5 @@ "check-scrobbling-tokens": "Ellenőrizd a Feldolgozó tokeneket", "process-scrobbling-events": "Feldolgozó események feldolgozása", "process-processed-scrobbling-events": "A feldolgozott Feldolgozó események felolgozása", - "generic-cover-volume-save": "Nem lehet borítóképet menteni a kötethez", - "person-doesnt-exist": "A személy nem létezik", - "person-name-required": "A személy neve kötelező, és nem lehet üres", - "email-taken": "Az e-mail már használatban van", - "person-name-unique": "A személy nevének egyedinek kell lennie" + "generic-cover-volume-save": "Nem lehet borítóképet menteni a kötethez" } diff --git a/API/I18N/ko.json b/API/I18N/ko.json index 7fec9f60a..bb087536b 100644 --- a/API/I18N/ko.json +++ b/API/I18N/ko.json @@ -203,11 +203,5 @@ "person-name-unique": "개인 이름은 고유해야 합니다", "person-image-doesnt-exist": "CoversDB에 사람이 존재하지 않습니다", "kavitaplus-restricted": "Kavita+만 해당", - "email-taken": "이미 사용중인 이메일", - "dashboard-stream-only-delete-smart-filter": "대시보드에서 스마트 필터 스트림만 삭제할 수 있습니다", - "sidenav-stream-only-delete-smart-filter": "사이드 메뉴에서 스마트 필터 스트림만 삭제할 수 있습니다", - "smart-filter-name-required": "스마트 필터 이름이 필요합니다", - "smart-filter-system-name": "시스템 제공 스트림 이름은 사용할 수 없습니다", - "aliases-have-overlap": "하나 이상의 별명이 다른 사용자와 중복되어 업데이트할 수 없습니다", - "generated-reading-profile-name": "{0}(으)로부터 생성됨" + "email-taken": "이미 사용중인 이메일" } diff --git a/API/I18N/pl.json b/API/I18N/pl.json index 5372fddc0..68a4a1a4f 100644 --- a/API/I18N/pl.json +++ b/API/I18N/pl.json @@ -207,7 +207,5 @@ "smart-filter-name-required": "Inteligentny filtr wymaga nazwy", "sidenav-stream-only-delete-smart-filter": "Tylko inteligentne filtry mogą zostać usunięte z panelu bocznego", "dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów może zostać usunięte z głównego panelu", - "smart-filter-system-name": "Nie można użyć nazwy systemu dostarczanego strumieniem", - "aliases-have-overlap": "Jeden lub więcej aliasów pokrywa się z innymi osobami, nie można ich zaktualizować", - "generated-reading-profile-name": "Wygenerowano z {0}" + "smart-filter-system-name": "Nie można użyć nazwy systemu dostarczanego strumieniem" } diff --git a/API/I18N/pt.json b/API/I18N/pt.json index 726c843bb..d0dd3345f 100644 --- a/API/I18N/pt.json +++ b/API/I18N/pt.json @@ -207,7 +207,5 @@ "sidenav-stream-only-delete-smart-filter": "Apenas os filtros inteligentes podem ser removidos da Navegação Lateral", "dashboard-stream-only-delete-smart-filter": "Apenas os filtros inteligentes podem ser removidos do painel", "smart-filter-system-name": "Não pode usar o nome de um fluxo disponibilizado pelo sistema", - "smart-filter-name-required": "Nome requerido para o filtro inteligente", - "aliases-have-overlap": "Um ou mais pseudónimos sobrepõem-se com outras pessoas, não vai ser possível atualizar", - "generated-reading-profile-name": "Gerado de {0}" + "smart-filter-name-required": "Nome requerido para o filtro inteligente" } diff --git a/API/I18N/pt_BR.json b/API/I18N/pt_BR.json index 418e0ea3b..7180b3404 100644 --- a/API/I18N/pt_BR.json +++ b/API/I18N/pt_BR.json @@ -207,7 +207,5 @@ "smart-filter-name-required": "Nome do Filtro Inteligente obrigatório", "dashboard-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do painel", "smart-filter-system-name": "Você não pode usar o nome de um fluxo fornecido pelo sistema", - "sidenav-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do Navegador Lateral", - "aliases-have-overlap": "Um ou mais dos pseudônimos se sobrepõem a outras pessoas, não pode atualizar", - "generated-reading-profile-name": "Gerado a partir de {0}" + "sidenav-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do Navegador Lateral" } diff --git a/API/I18N/ru.json b/API/I18N/ru.json index fdea5920f..c75f58fb1 100644 --- a/API/I18N/ru.json +++ b/API/I18N/ru.json @@ -1,5 +1,5 @@ { - "confirm-email": "Сначала Вы обязаны подтвердить свою электронную почту", + "confirm-email": "Вы обязаны сначала подтвердить свою почту", "generate-token": "Возникла проблема с генерацией токена подтверждения электронной почты. Смотрите журналы", "invalid-password": "Неверный пароль", "invalid-email-confirmation": "Неверное подтверждение электронной почты", @@ -35,15 +35,15 @@ "no-user": "Пользователь не существует", "generic-invite-user": "Возникла проблема с приглашением пользователя. Пожалуйста, проверьте журналы.", "permission-denied": "Вам запрещено выполнять эту операцию", - "invalid-access": "В доступе отказано", + "invalid-access": "Недопустимый доступ", "reading-list-name-exists": "Такой список для чтения уже существует", "perform-scan": "Пожалуйста, выполните сканирование этой серии или библиотеки и повторите попытку", "generic-device-create": "При создании устройства возникла ошибка", "generic-read-progress": "Возникла проблема с сохранением прогресса", "file-doesnt-exist": "Файл не существует", "admin-already-exists": "Администратор уже существует", - "send-to-kavita-email": "Отправка на устройство не может быть использована без настройки электронной почты", - "no-image-for-page": "Нет такого изображения для страницы {0}. Попробуйте обновить, для повторного кеширования.", + "send-to-kavita-email": "Отправка на устройство не может быть использована с почтовым сервисом Kavita. Пожалуйста, настройте свой собственный.", + "no-image-for-page": "Нет такого изображения для страницы {0}. Попробуйте обновить, чтобы обновить кэш.", "reading-list-permission": "У вас нет прав на этот список чтения или список не существует", "volume-doesnt-exist": "Том не существует", "generic-library": "Возникла критическая проблема. Пожалуйста, попробуйте еще раз.", @@ -57,7 +57,7 @@ "generic-reading-list-create": "Возникла проблема с созданием списка для чтения", "no-cover-image": "Изображение на обложке отсутствует", "collection-updated": "Коллекция успешно обновлена", - "critical-email-migration": "Возникла проблема при смене электронной почты. Обратитесь в службу поддержки", + "critical-email-migration": "Возникла проблема при переносе электронной почты. Обратитесь в службу поддержки", "cache-file-find": "Не удалось найти изображение в кэше. Перезагрузитесь и попробуйте снова.", "duplicate-bookmark": "Дублирующая закладка уже существует", "collection-tag-duplicate": "Такая коллекция уже существует", @@ -72,7 +72,7 @@ "pdf-doesnt-exist": "PDF не существует, когда он должен существовать", "generic-device-delete": "При удалении устройства возникла ошибка", "bookmarks-empty": "Закладки не могут быть пустыми", - "valid-number": "Номер страницы должен быть действительным", + "valid-number": "Должен быть действительный номер страницы", "series-doesnt-exist": "Серия не существует", "no-library-access": "Пользователь не имеет доступа к этой библиотеке", "reading-list-item-delete": "Не удалось удалить элемент(ы)", @@ -121,7 +121,7 @@ "opds-disabled": "OPDS не включен на этом сервере", "stats-permission-denied": "Вы не имеете права просматривать статистику другого пользователя", "reading-list-restricted": "Список чтения не существует или у вас нет доступа", - "favicon-doesnt-exist": "Favicon не существует", + "favicon-doesnt-exist": "Фавикон не существует", "external-source-already-in-use": "Существует поток с этим внешним источником", "issue-num": "Вопрос {0}{1}", "generic-create-temp-archive": "Возникла проблема с созданием временного архива", @@ -155,7 +155,7 @@ "generic-user-delete": "Не удалось удалить пользователя", "generic-cover-reading-list-save": "Невозможно сохранить изображение обложки в списке для чтения", "unable-to-register-k+": "Невозможно зарегистрировать лицензию из-за ошибки. Обратитесь в службу поддержки Кавита+", - "encode-as-warning": "Вы не можете конвертировать в формат PNG. Для обновления обложек используйте команду \"Обновить обложку\". Закладки и значки не могут быть закодированы обратно.", + "encode-as-warning": "Конвертировать в PNG невозможно. Для обложек используйте Обновить Обложки. Закладки и фавиконки нельзя закодировать обратно.", "want-to-read": "Хотите прочитать", "generic-user-pref": "Возникла проблема с сохранением предпочтений", "external-sources": "Внешние источники", @@ -194,13 +194,5 @@ "backup": "Резервное копирование", "process-processed-scrobbling-events": "Обработка обработанных событий скроблинга", "scan-libraries": "Сканирование библиотек", - "kavita+-data-refresh": "Обновление данных Kavita+", - "kavitaplus-restricted": "Это доступно только для Kavita+", - "person-doesnt-exist": "Персона не существует", - "generic-cover-volume-save": "Не удается сохранить обложку для тома", - "generic-cover-person-save": "Не удается сохранить изображение обложки для Персоны", - "person-name-unique": "Имя персоны должно быть уникальным", - "person-image-doesnt-exist": "Персона не существует в CoversDB", - "email-taken": "Почта уже используется", - "person-name-required": "Имя персоны обязательно и не может быть пустым" + "kavita+-data-refresh": "Обновление данных Kavita+" } diff --git a/API/I18N/sk.json b/API/I18N/sk.json index ef267ed02..0967ef424 100644 --- a/API/I18N/sk.json +++ b/API/I18N/sk.json @@ -1,213 +1 @@ -{ - "disabled-account": "Váš účet je deaktivovaný. Kontaktujte správcu servera.", - "register-user": "Niečo sa pokazilo pri registrácii užívateľa", - "confirm-email": "Najprv musíte potvrdiť svoj e-mail", - "locked-out": "Boli ste zamknutí z dôvodu veľkého počtu neúspešných pokusov o prihlásenie. Počkajte 10 minút.", - "validate-email": "Pri validácii vášho e-mailu sa vyskytla chyba: {0}", - "confirm-token-gen": "Pri vytváraní potvrdzovacieho tokenu sa vyskytla chyba", - "permission-denied": "Na vykonanie tejto úlohy nemáte oprávnenie", - "password-required": "Ak nie ste administrátor, musíte na vykonanie zmien vo vašom profile zadať vaše aktuálne heslo", - "invalid-password": "Nesprávne heslo", - "invalid-token": "Nesprávny token", - "unable-to-reset-key": "Niečo sa pokazilo, kľúč nie je možné resetovať", - "invalid-payload": "Nesprávny payload", - "nothing-to-do": "Nič na vykonanie", - "share-multiple-emails": "Nemôžete zdielať e-maily medzi rôznymi účtami", - "generate-token": "Pri generovaní potvrdzovacieho tokenu e-mailu sa vyskytla chyba. Pozrite záznamy udalostí", - "age-restriction-update": "Pri aktualizovaní vekového obmedzenia sa vyskytla chyba", - "no-user": "Používateľ neexistuje", - "generic-user-update": "Aktualizácia používateľa prebehla s výnimkou", - "username-taken": "Používateľské meno už existuje", - "user-already-confirmed": "Používateľ je už potvrdený", - "user-already-registered": "Používateľ je už registrovaný ako {0}", - "user-already-invited": "Používateľ je už pod týmto e-mailom pozvaný a musí ešte prijať pozvanie.", - "generic-password-update": "Pri potvrdení nového hesla sa vyskytla neočakávaná chyba", - "generic-invite-user": "Pri pozývaní tohto používateľa sa vyskytla chyba. Pozrite záznamy udalostí.", - "password-updated": "Heslo aktualizované", - "forgot-password-generic": "E-mail bude odoslaný na zadanú adresu len v prípade, ak existuje v databáze", - "invalid-email-confirmation": "Neplatné potvrdenie e-mailu", - "not-accessible-password": "Váš server nie je dostupný. Odkaz na resetovanie vášho hesla je v záznamoch udalostí", - "email-taken": "Zadaný e-mail už existuje", - "denied": "Nepovolené", - "manual-setup-fail": "Manuálne nastavenie nie je možné dokončiť. Prosím zrušte aktuálny postup a znovu vytvorte pozvánku", - "generic-user-email-update": "Nemožno aktualizovať e-mail používateľa. Skontrolujte záznamy udalostí.", - "email-not-enabled": "E-mail nie je na tomto serveri povolený. Preto túto akciu nemôžete vykonať.", - "collection-updated": "Zbierka bola úspešne aktualizovaná", - "device-doesnt-exist": "Zariadenie neexistuje", - "generic-device-delete": "Pri odstraňovaní zariadenia sa vyskytla chyba", - "greater-0": "{0} musí byť väčší ako 0", - "send-to-size-limit": "Snažíte sa odoslať súbor(y), ktoré sú príliš veľké pre vášho e-mailového poskytovateľa", - "send-to-device-status": "Prenos súborov do vášho zariadenia", - "no-cover-image": "Žiadny prebal", - "must-be-defined": "{0} musí byť definovaný", - "generic-favicon": "Pri získavaní favicon-u domény sa vyskytla chyba", - "no-library-access": "Pozužívateľ nemá prístup do tejto knižnice", - "user-doesnt-exist": "Používateľ neexistuje", - "collection-already-exists": "Zbierka už existuje", - "not-accessible": "Váš server nie je dostupný z vonkajšieho prostredia", - "email-sent": "E-mail odoslaný", - "user-migration-needed": "Uvedený používateľ potrebuje migrovať. Odhláste ho a opäť prihláste na spustenie migrácie", - "generic-invite-email": "Pri opakovanom odosielaní pozývacieho e-mailu sa vyskytla chyba", - "email-settings-invalid": "V nastaveniach e-mailu chýbajú potrebné údaje. Uistite sa, že všetky nastavenia e-mailu sú uložené.", - "chapter-doesnt-exist": "Kapitola neexistuje", - "critical-email-migration": "Počas migrácie e-mailu sa vyskytla chyba. Kontaktujte podporu", - "collection-deleted": "Zbierka bola vymazaná", - "generic-error": "Niečo sa pokazilo, skúste to znova", - "collection-doesnt-exist": "Zbierka neexistuje", - "generic-device-update": "Pri aktualizácii zariadenia sa vyskytla chyba", - "bookmark-doesnt-exist": "Záložka neexistuje", - "person-doesnt-exist": "Osoba neexistuje", - "send-to-kavita-email": "Odoslanie do zariadenia nemôže byť použité bez nastavenia e-amilu", - "send-to-unallowed": "Nemôžete odosielať do zariadenia, ktoré nie je vaše", - "generic-library": "Vyskytla sa kritická chyba. Prosím skúste to opäť.", - "pdf-doesnt-exist": "PDF neexistuje, hoci by malo", - "generic-library-update": "Počas aktualizácie knižnice sa vyskytla kritická chyba.", - "invalid-access": "Neplatný prístup", - "perform-scan": "Prosím, vykonajte opakovaný sken na tejto sérii alebo knižnici", - "generic-read-progress": "Pri ukladaní aktuálneho stavu sa vyskytla chyba", - "generic-clear-bookmarks": "Záložky nie je možné vymazať", - "bookmark-permission": "Nemáte oprávnenie na vkladanie/odstraňovanie záložiek", - "bookmark-save": "Nemožno uložiť záložku", - "bookmarks-empty": "Záložky nemôžu byť prázdne", - "library-doesnt-exist": "Knižnica neexistuje", - "invalid-path": "Neplatné umiestnenie", - "generic-send-to": "Pri odosielaní súboru(-ov) do vášho zariadenia sa vyskytla chyba", - "no-image-for-page": "Žiadny taký obrázok pre stránku {0}. Pokúste sa ju obnoviť, aby ste ju mohli nanovo uložiť.", - "delete-library-while-scan": "Nemôžete odstrániť knižnicu počas prebiehajúceho skenovania. Prosím, vyčkajte na dokončenie skenovania alebo reštartujte Kavitu a skúste ju opäť odstrániť", - "invalid-username": "Neplatné používateľské meno", - "account-email-invalid": "E-mail uvedený v údajoch administrátora nie je platným e-mailom. Nie je možné zaslať testovací e-mail.", - "admin-already-exists": "Administrátor už existuje", - "invalid-filename": "Neplatný názov súboru", - "file-doesnt-exist": "Súbor neexistuje", - "invalid-email": "E-mail v záznamoch pre používateľov nie platný e-mail. Odkazy sú uvedené v záznamoch udalostí.", - "file-missing": "Súbor nebol nájdený v knihe", - "error-import-stack": "Pri importovaní MAL balíka sa vyskytla chyba", - "person-name-required": "Meno osoby je povinné a nesmie byť prázdne", - "person-name-unique": "Meno osoby musí byť jedinečné", - "person-image-doesnt-exist": "Osoba neexistuje v databáze CoversDB", - "generic-device-create": "Pri vytváraní zariadenia sa vyskytla chyba", - "series-doesnt-exist": "Séria neexistuje", - "volume-doesnt-exist": "Zväzok neexistuje", - "library-name-exists": "Názov knižnice už existuje. Prosím, vyberte si pre daný server jedinečný názov.", - "cache-file-find": "Nepodarilo sa nájsť obrázok vo vyrovnávacej pamäti. Znova načítajte a skúste to znova.", - "name-required": "Názov nemôže byť prázdny", - "valid-number": "Musí to byť platné číslo strany", - "duplicate-bookmark": "Duplicitný záznam záložky už existuje", - "reading-list-permission": "Nemáte povolenia na tento zoznam na čítanie alebo zoznam neexistuje", - "reading-list-position": "Nepodarilo sa aktualizovať pozíciu", - "reading-list-updated": "Aktualizované", - "reading-list-item-delete": "Položku(y) sa nepodarilo odstrániť", - "reading-list-deleted": "Zoznam na čítanie bol odstránený", - "generic-reading-list-delete": "Pri odstraňovaní zoznamu na čítanie sa vyskytol problém", - "generic-reading-list-update": "Pri aktualizácii zoznamu na čítanie sa vyskytol problém", - "generic-reading-list-create": "Pri vytváraní zoznamu na čítanie sa vyskytol problém", - "reading-list-doesnt-exist": "Zoznam na čítanie neexistuje", - "series-restricted": "Používateľ nemá prístup k tejto sérii", - "generic-scrobble-hold": "Pri pauznutí funkcie sa vyskytla chyba", - "libraries-restricted": "Používateľ nemá prístup k žiadnym knižniciam", - "no-series": "Nepodarilo sa získať sériu pre knižnicu", - "no-series-collection": "Nepodarilo sa získať sériu pre kolekciu", - "generic-series-delete": "Pri odstraňovaní série sa vyskytol problém", - "generic-series-update": "Pri aktualizácii série sa vyskytla chyba", - "series-updated": "Úspešne aktualizované", - "update-metadata-fail": "Nepodarilo sa aktualizovať metadáta", - "age-restriction-not-applicable": "Bez obmedzenia", - "generic-relationship": "Pri aktualizácii vzťahov sa vyskytol problém", - "job-already-running": "Úloha už beží", - "encode-as-warning": "Nedá sa konvertovať do formátu PNG. Pre obaly použite možnosť Obnoviť obaly. Záložky a favicony sa nedajú spätne zakódovať.", - "ip-address-invalid": "IP adresa „{0}“ je neplatná", - "bookmark-dir-permissions": "Adresár záložiek nemá správne povolenia pre použitie v aplikácii Kavita", - "total-backups": "Celkový počet záloh musí byť medzi 1 a 30", - "total-logs": "Celkový počet protokolov musí byť medzi 1 a 30", - "stats-permission-denied": "Nemáte oprávnenie zobraziť si štatistiky iného používateľa", - "url-not-valid": "URL nevracia platný obrázok alebo vyžaduje autorizáciu", - "url-required": "Na použitie musíte zadať URL adresu", - "generic-cover-series-save": "Obrázok obálky sa nepodarilo uložiť do série", - "generic-cover-collection-save": "Obrázok obálky sa nepodarilo uložiť do kolekcie", - "generic-cover-reading-list-save": "Obrázok obálky sa nepodarilo uložiť do zoznamu na čítanie", - "generic-cover-chapter-save": "Obrázok obálky sa nepodarilo uložiť do kapitoly", - "generic-cover-library-save": "Obrázok obálky sa nepodarilo uložiť do knižnice", - "generic-cover-person-save": "Obrázok obálky sa nepodarilo uložiť k tejto osobe", - "generic-cover-volume-save": "Obrázok obálky sa nepodarilo uložiť do zväzku", - "access-denied": "Nemáte prístup", - "reset-chapter-lock": "Nepodarilo sa resetovať zámok obalu pre kapitolu", - "generic-user-delete": "Používateľa sa nepodarilo odstrániť", - "generic-user-pref": "Pri ukladaní predvolieb sa vyskytol problém", - "opds-disabled": "OPDS nie je na tomto serveri povolený", - "on-deck": "Pokračovať v čítaní", - "browse-on-deck": "Prehliadať pokračovanie v čítaní", - "recently-added": "Nedávno pridané", - "want-to-read": "Chcem čítať", - "browse-want-to-read": "Prehliadať Chcem si prečítať", - "browse-recently-added": "Prehliadať nedávno pridané", - "reading-lists": "Zoznamy na čítanie", - "browse-reading-lists": "Prehliadať podľa zoznamov na čítanie", - "libraries": "Všetky knižnice", - "browse-libraries": "Prehliadať podľa knižníc", - "collections": "Všetky kolekcie", - "browse-collections": "Prehliadať podľa kolekcií", - "more-in-genre": "Viac v žánri {0}", - "browse-more-in-genre": "Prezrite si viac v {0}", - "recently-updated": "Nedávno aktualizované", - "browse-recently-updated": "Prehliadať nedávno aktualizované", - "smart-filters": "Inteligentné filtre", - "external-sources": "Externé zdroje", - "browse-external-sources": "Prehliadať externé zdroje", - "browse-smart-filters": "Prehliadať podľa inteligentných filtrov", - "reading-list-restricted": "Zoznam na čítanie neexistuje alebo k nemu nemáte prístup", - "query-required": "Musíte zadať parameter dopytu", - "search": "Hľadať", - "search-description": "Vyhľadávanie sérií, zbierok alebo zoznamov na čítanie", - "favicon-doesnt-exist": "Favicon neexistuje", - "smart-filter-doesnt-exist": "Inteligentný filter neexistuje", - "smart-filter-already-in-use": "Existuje existujúci stream s týmto inteligentným filtrom", - "dashboard-stream-doesnt-exist": "Stream dashboardu neexistuje", - "sidenav-stream-doesnt-exist": "SideNav Stream neexistuje", - "external-source-already-exists": "Externý zdroj už existuje", - "external-source-required": "Vyžaduje sa kľúč API a Host", - "external-source-doesnt-exist": "Externý zdroj neexistuje", - "external-source-already-in-use": "S týmto externým zdrojom existuje stream", - "sidenav-stream-only-delete-smart-filter": "Z bočného panela SideNav je možné odstrániť iba streamy inteligentných filtrov", - "dashboard-stream-only-delete-smart-filter": "Z ovládacieho panela je možné odstrániť iba streamy inteligentných filtrov", - "smart-filter-name-required": "Názov inteligentného filtra je povinný", - "smart-filter-system-name": "Nemôžete použiť názov streamu poskytnutého systémom", - "not-authenticated": "Používateľ nie je overený", - "unable-to-register-k+": "Licenciu sa nepodarilo zaregistrovať z dôvodu chyby. Kontaktujte podporu Kavita+", - "unable-to-reset-k+": "Licenciu Kavita+ sa nepodarilo resetovať z dôvodu chyby. Kontaktujte podporu Kavita+", - "anilist-cred-expired": "Prihlasovacie údaje AniList vypršali alebo chýbajú", - "scrobble-bad-payload": "Nesprávne údaje od poskytovateľa Scrobblovania", - "theme-doesnt-exist": "Súbor témy chýba alebo je neplatný", - "bad-copy-files-for-download": "Súbory sa nepodarilo skopírovať do dočasného adresára na stiahnutie archívu.", - "generic-create-temp-archive": "Pri vytváraní dočasného archívu sa vyskytla chyba", - "epub-malformed": "Súbor je nesprávne naformátovaný! Nedá sa prečítať.", - "epub-html-missing": "Zodpovedajúci súbor HTML pre túto stránku sa nenašiel", - "collection-tag-title-required": "Názov kolekcie nemôže byť prázdny", - "reading-list-title-required": "Názov zoznamu na čítanie nemôže byť prázdny", - "collection-tag-duplicate": "Kolekcia s týmto názvom už existuje", - "device-duplicate": "Zariadenie s týmto názvom už existuje", - "device-not-created": "Toto zariadenie ešte neexistuje. Najprv ho vytvorte", - "send-to-permission": "Nie je možné odoslať súbory iné ako EPUB alebo PDF na zariadenia, pretože nie sú podporované na Kindle", - "progress-must-exist": "Pokrok musí byť u používateľa k dispozícii", - "reading-list-name-exists": "Zoznam na prečítanie s týmto menom už existuje", - "user-no-access-library-from-series": "Používateľ nemá prístup do knižnice, do ktorej táto séria patrí", - "series-restricted-age-restriction": "Používateľ si nemôže pozrieť túto sériu z dôvodu vekového obmedzenia", - "kavitaplus-restricted": "Toto je obmedzené iba na Kavita+", - "aliases-have-overlap": "Jeden alebo viacero aliasov sa prekrýva s inými osobami, nie je možné ich aktualizovať", - "volume-num": "Zväzok {0}", - "book-num": "Kniha {0}", - "issue-num": "Problém {0}{1}", - "chapter-num": "Kapitola {0}", - "check-updates": "Skontrolovať aktualizácie", - "license-check": "Kontrola licencie", - "process-scrobbling-events": "Udalosti procesu scrobblovania", - "report-stats": "Štatistiky hlásení", - "check-scrobbling-tokens": "Skontrolujte Tokeny Scrobblingu", - "cleanup": "Čistenie", - "process-processed-scrobbling-events": "Spracovať udalosti scrobblovania", - "remove-from-want-to-read": "Upratanie listu Chcem si prečítať", - "scan-libraries": "Skenovanie knižníc", - "kavita+-data-refresh": "Obnovenie údajov Kavita+", - "backup": "Záloha", - "update-yearly-stats": "Aktualizovať ročné štatistiky", - "generated-reading-profile-name": "Vygenerované z {0}" -} +{} diff --git a/API/I18N/ta.json b/API/I18N/ta.json index cc20ab40f..83d6bd12f 100644 --- a/API/I18N/ta.json +++ b/API/I18N/ta.json @@ -202,12 +202,5 @@ "want-to-read": "படிக்க விரும்புகிறேன்", "browse-want-to-read": "உலாவு படிக்க விரும்புகிறது", "browse-recently-added": "உலாவு அண்மைக் காலத்தில் சேர்க்கப்பட்டது", - "reading-lists": "பட்டியல்களைப் படித்தல்", - "sidenav-stream-only-delete-smart-filter": "சைடனாவிலிருந்து அறிவுள்ள வடிகட்டி நீரோடைகளை மட்டுமே நீக்க முடியும்", - "dashboard-stream-only-delete-smart-filter": "டாச்போர்டில் இருந்து அறிவுள்ள வடிகட்டி ச்ட்ரீம்களை மட்டுமே நீக்க முடியும்", - "smart-filter-name-required": "அறிவுள்ள வடிகட்டி பெயர் தேவை", - "smart-filter-system-name": "வழங்கப்பட்ட ச்ட்ரீமின் பெயரை நீங்கள் பயன்படுத்த முடியாது", - "kavitaplus-restricted": "இது கவிதா+ க்கு மட்டுமே", - "aliases-have-overlap": "ஒன்று அல்லது அதற்கு மேற்பட்ட மாற்றுப்பெயர்கள் மற்றவர்களுடன் ஒன்றுடன் ஒன்று உள்ளன, புதுப்பிக்க முடியாது", - "generated-reading-profile-name": "{0 இருந்து இலிருந்து உருவாக்கப்பட்டது" + "reading-lists": "பட்டியல்களைப் படித்தல்" } diff --git a/API/I18N/zh_Hans.json b/API/I18N/zh_Hans.json index 14c8c902e..92d1751a8 100644 --- a/API/I18N/zh_Hans.json +++ b/API/I18N/zh_Hans.json @@ -207,7 +207,5 @@ "dashboard-stream-only-delete-smart-filter": "只能从仪表板中删除智能筛选器流", "smart-filter-name-required": "需要智能筛选器名称", "smart-filter-system-name": "您不能使用系统提供的流名称", - "sidenav-stream-only-delete-smart-filter": "只能从侧边栏删除智能筛选器流", - "aliases-have-overlap": "一个或多个别名与其他人有重叠,无法更新", - "generated-reading-profile-name": "由 {0} 生成" + "sidenav-stream-only-delete-smart-filter": "只能从侧边栏删除智能筛选器流" } diff --git a/API/I18N/zh_Hant.json b/API/I18N/zh_Hant.json index 31d4b69f6..ce72ea451 100644 --- a/API/I18N/zh_Hant.json +++ b/API/I18N/zh_Hant.json @@ -193,7 +193,7 @@ "update-yearly-stats": "更新年度統計", "invalid-email": "使用者檔案中的電子郵件無效。請查看日誌以獲得任何連結。", "browse-external-sources": "瀏覽外部來源", - "sidenav-stream-doesnt-exist": "側邊導覽列的串流不存在", + "sidenav-stream-doesnt-exist": "側邊導覽串流不存在", "smart-filter-already-in-use": "已存在具有此智慧篩選器的串流", "external-source-already-exists": "外部來源已存在", "generic-cover-volume-save": "無法保存封面圖片", @@ -201,13 +201,5 @@ "person-doesnt-exist": "此人不存在", "person-name-required": "名稱為必填欄位,且不得留空", "person-name-unique": "名稱不得重複", - "person-image-doesnt-exist": "CoversDB 中不存在此人", - "email-taken": "電子郵件已被使用", - "sidenav-stream-only-delete-smart-filter": "只有智慧篩選器可以從側邊導覽列中刪除", - "dashboard-stream-only-delete-smart-filter": "只有智慧篩選器串流可以從儀表板中刪除", - "smart-filter-name-required": "智慧篩選器名稱名稱不可為空", - "smart-filter-system-name": "您不能使用系統保留的串流名稱", - "kavitaplus-restricted": "此功能僅限 Kavita+ 使用", - "aliases-have-overlap": "一個或多個別名與其他人物重複,無法更新", - "generated-reading-profile-name": "由 {0} 生成" + "person-image-doesnt-exist": "CoversDB 中不存在此人" } diff --git a/API/Middleware/SecurityMiddleware.cs b/API/Middleware/SecurityMiddleware.cs index 67cb42d0c..61ca1c75d 100644 --- a/API/Middleware/SecurityMiddleware.cs +++ b/API/Middleware/SecurityMiddleware.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Net; using System.Text.Json; using System.Threading.Tasks; @@ -27,7 +26,7 @@ public class SecurityEventMiddleware(RequestDelegate next) } catch (KavitaUnauthenticatedUserException ex) { - var ipAddress = context.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? context.Connection.RemoteIpAddress?.ToString(); + var ipAddress = context.Connection.RemoteIpAddress?.ToString(); var requestMethod = context.Request.Method; var requestPath = context.Request.Path; var userAgent = context.Request.Headers.UserAgent; diff --git a/API/Program.cs b/API/Program.cs index 011a7de2a..852844f2f 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; @@ -49,13 +48,15 @@ public class Program var directoryService = new DirectoryService(null!, new FileSystem()); - - // Check if this is the first time running and if so, rename appsettings-init.json to appsettings.json - HandleFirstRunConfiguration(); - - // Before anything, check if JWT has been generated properly or if user still has default - EnsureJwtTokenKey(); + if (!Configuration.CheckIfJwtTokenSet() && + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development) + { + Log.Logger.Information("Generating JWT TokenKey for encrypting user sessions..."); + var rBytes = new byte[256]; + RandomNumberGenerator.Create().GetBytes(rBytes); + Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); + } try { @@ -69,7 +70,6 @@ public class Program { var logger = services.GetRequiredService>(); var context = services.GetRequiredService(); - var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); var isDbCreated = await context.Database.CanConnectAsync(); if (isDbCreated && pendingMigrations.Any()) @@ -157,26 +157,6 @@ public class Program } } - private static void EnsureJwtTokenKey() - { - if (Configuration.CheckIfJwtTokenSet() || Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development) return; - - Log.Logger.Information("Generating JWT TokenKey for encrypting user sessions..."); - var rBytes = new byte[256]; - RandomNumberGenerator.Create().GetBytes(rBytes); - Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); - } - - private static void HandleFirstRunConfiguration() - { - var firstRunConfigFilePath = Path.Join(Directory.GetCurrentDirectory(), "config/appsettings-init.json"); - if (File.Exists(firstRunConfigFilePath) && - !File.Exists(Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json"))) - { - File.Move(firstRunConfigFilePath, Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json")); - } - } - private static async Task GetMigrationDirectory(DataContext context, IDirectoryService directoryService) { string? currentVersion = null; diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 544efa4ce..0255b785d 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -10,9 +10,11 @@ using API.Entities.Interfaces; using API.Extensions; using Microsoft.Extensions.Logging; using NetVips; +using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; +using Color = System.Drawing.Color; using Image = NetVips.Image; namespace API.Services; @@ -748,7 +750,7 @@ public class ImageService : IImageService } - public static (int R, int G, int B) HexToRgb(string? hex) + public static Color HexToRgb(string? hex) { if (string.IsNullOrEmpty(hex)) throw new ArgumentException("Hex cannot be null"); @@ -772,7 +774,7 @@ public class ImageService : IImageService var g = Convert.ToInt32(hex.Substring(2, 2), 16); var b = Convert.ToInt32(hex.Substring(4, 2), 16); - return (r, g, b); + return Color.FromArgb(r, g, b); } diff --git a/API/Services/KoreaderService.cs b/API/Services/KoreaderService.cs deleted file mode 100644 index a38e8c468..000000000 --- a/API/Services/KoreaderService.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Koreader; -using API.DTOs.Progress; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -#nullable enable - -public interface IKoreaderService -{ - Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId); - Task GetProgress(string bookHash, int userId); -} - -public class KoreaderService : IKoreaderService -{ - private readonly IReaderService _readerService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - - public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork, ILocalizationService localizationService, ILogger logger) - { - _readerService = readerService; - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _logger = logger; - } - - /// - /// Given a Koreader hash, locate the underlying file and generate/update a progress event. - /// - /// - /// - public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId) - { - _logger.LogDebug("Saving Koreader progress for User ({UserId}): {KoreaderProgress}", userId, koreaderBookDto.Progress.Sanitize()); - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document); - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - if (userProgressDto == null) - { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId); - if (chapterDto == null) throw new KavitaException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); - - var volumeDto = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId); - if (volumeDto == null) throw new KavitaException(await _localizationService.Translate(userId, "volume-doesnt-exist")); - - userProgressDto = new ProgressDto() - { - ChapterId = file.ChapterId, - VolumeId = chapterDto.VolumeId, - SeriesId = volumeDto.SeriesId, - }; - } - // Update the bookScrollId if possible - KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.Progress); - - await _readerService.SaveReadingProgress(userProgressDto, userId); - } - - /// - /// Returns a Koreader Dto representing current book and the progress within - /// - /// - /// - /// - public async Task GetProgress(string bookHash, int userId) - { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash); - - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - var koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto); - - return new KoreaderBookDtoBuilder(bookHash).WithProgress(koreaderProgress) - .WithPercentage(progressDto?.PageNum, file.Pages) - .WithDeviceId(settingsDto.InstallId, userId) - .Build(); - } -} diff --git a/API/Services/PersonService.cs b/API/Services/PersonService.cs deleted file mode 100644 index ff0049cbe..000000000 --- a/API/Services/PersonService.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; - -namespace API.Services; - -public interface IPersonService -{ - /// - /// Adds src as an alias to dst, this is a destructive operation - /// - /// Merged person - /// Remaining person - /// The entities passed as arguments **must** include all relations - /// - Task MergePeopleAsync(Person src, Person dst); - - /// - /// Adds the alias to the person, requires that the aliases are not shared with anyone else - /// - /// This method does NOT commit changes - /// - /// - /// - Task UpdatePersonAliasesAsync(Person person, IList aliases); -} - -public class PersonService(IUnitOfWork unitOfWork): IPersonService -{ - - public async Task MergePeopleAsync(Person src, Person dst) - { - if (dst.Id == src.Id) return; - - if (string.IsNullOrWhiteSpace(dst.Description) && !string.IsNullOrWhiteSpace(src.Description)) - { - dst.Description = src.Description; - } - - if (dst.MalId == 0 && src.MalId != 0) - { - dst.MalId = src.MalId; - } - - if (dst.AniListId == 0 && src.AniListId != 0) - { - dst.AniListId = src.AniListId; - } - - if (dst.HardcoverId == null && src.HardcoverId != null) - { - dst.HardcoverId = src.HardcoverId; - } - - if (dst.Asin == null && src.Asin != null) - { - dst.Asin = src.Asin; - } - - if (dst.CoverImage == null && src.CoverImage != null) - { - dst.CoverImage = src.CoverImage; - } - - MergeChapterPeople(dst, src); - MergeSeriesMetadataPeople(dst, src); - - dst.Aliases.Add(new PersonAliasBuilder(src.Name).Build()); - - foreach (var alias in src.Aliases) - { - dst.Aliases.Add(alias); - } - - unitOfWork.PersonRepository.Remove(src); - unitOfWork.PersonRepository.Update(dst); - await unitOfWork.CommitAsync(); - } - - private static void MergeChapterPeople(Person dst, Person src) - { - - foreach (var chapter in src.ChapterPeople) - { - var alreadyPresent = dst.ChapterPeople - .Any(x => x.ChapterId == chapter.ChapterId && x.Role == chapter.Role); - - if (alreadyPresent) continue; - - dst.ChapterPeople.Add(new ChapterPeople - { - Role = chapter.Role, - ChapterId = chapter.ChapterId, - Person = dst, - KavitaPlusConnection = chapter.KavitaPlusConnection, - OrderWeight = chapter.OrderWeight, - }); - } - } - - private static void MergeSeriesMetadataPeople(Person dst, Person src) - { - foreach (var series in src.SeriesMetadataPeople) - { - var alreadyPresent = dst.SeriesMetadataPeople - .Any(x => x.SeriesMetadataId == series.SeriesMetadataId && x.Role == series.Role); - - if (alreadyPresent) continue; - - dst.SeriesMetadataPeople.Add(new SeriesMetadataPeople - { - SeriesMetadataId = series.SeriesMetadataId, - Role = series.Role, - Person = dst, - KavitaPlusConnection = series.KavitaPlusConnection, - OrderWeight = series.OrderWeight, - }); - } - } - - public async Task UpdatePersonAliasesAsync(Person person, IList aliases) - { - var normalizedAliases = aliases - .Select(a => a.ToNormalized()) - .Where(a => !string.IsNullOrEmpty(a) && a != person.NormalizedName) - .ToList(); - - if (normalizedAliases.Count == 0) - { - person.Aliases = []; - return true; - } - - var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases); - others = others.Where(p => p.Id != person.Id).ToList(); - - if (others.Count != 0) return false; - - person.Aliases = aliases.Select(a => new PersonAliasBuilder(a).Build()).ToList(); - - return true; - } -} diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 0777e1baa..f9af923a2 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -10,19 +10,15 @@ using API.DTOs.Collection; using API.DTOs.KavitaPlus.ExternalMetadata; using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Metadata.Matching; -using API.DTOs.Person; using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.Interfaces; using API.Entities.Metadata; using API.Entities.MetadataMatching; -using API.Entities.Person; using API.Extensions; using API.Helpers; -using API.Helpers.Builders; using API.Services.Tasks.Metadata; using API.Services.Tasks.Scanner.Parser; using API.SignalR; @@ -68,7 +64,6 @@ public class ExternalMetadataService : IExternalMetadataService private readonly IScrobblingService _scrobblingService; private readonly IEventHub _eventHub; private readonly ICoverDbService _coverDbService; - private readonly IKavitaPlusApiService _kavitaPlusApiService; private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30); public static readonly HashSet NonEligibleLibraryTypes = [LibraryType.Comic, LibraryType.Book, LibraryType.Image]; @@ -84,8 +79,7 @@ public class ExternalMetadataService : IExternalMetadataService private static bool IsRomanCharacters(string input) => Regex.IsMatch(input, @"^[\p{IsBasicLatin}\p{IsLatin-1Supplement}]+$"); public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper, - ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService, - IKavitaPlusApiService kavitaPlusApiService) + ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService) { _unitOfWork = unitOfWork; _logger = logger; @@ -94,7 +88,6 @@ public class ExternalMetadataService : IExternalMetadataService _scrobblingService = scrobblingService; _eventHub = eventHub; _coverDbService = coverDbService; - _kavitaPlusApiService = kavitaPlusApiService; FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } @@ -183,7 +176,9 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogDebug("Fetching Kavita+ for MAL Stacks for user {UserName}", user.MalUserName); var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var result = await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license); + var result = await ($"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={user.MalUserName}") + .WithKavitaPlusHeaders(license) + .GetJsonAsync>(); if (result == null) { @@ -202,14 +197,11 @@ public class ExternalMetadataService : IExternalMetadataService /// /// Returns the match results for a Series from UI Flow /// - /// - /// Will extract alternative names like Localized name, year will send as ReleaseYear but fallback to Comic Vine syntax if applicable - /// /// /// public async Task> MatchSeries(MatchSeriesDto dto) { - + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library); if (series == null) return []; @@ -217,33 +209,31 @@ public class ExternalMetadataService : IExternalMetadataService var potentialAnilistId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); var potentialMalId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); - var format = series.Library.Type.ConvertToPlusMediaFormat(series.Format); - var otherNames = ExtractAlternativeNames(series); - - var year = series.Metadata.ReleaseYear; - if (year == 0 && format == PlusMediaFormat.Comic && !string.IsNullOrWhiteSpace(series.Name)) + List altNames = [series.LocalizedName, series.OriginalName]; + if (potentialAnilistId == null && potentialMalId == null && !string.IsNullOrEmpty(dto.Query)) { - var potentialYear = Parser.ParseYear(series.Name); - if (!string.IsNullOrEmpty(potentialYear)) - { - year = int.Parse(potentialYear); - } + altNames.Add(dto.Query); } var matchRequest = new MatchSeriesRequestDto() { - Format = format, + Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), Query = dto.Query, SeriesName = series.Name, - AlternativeNames = otherNames, - Year = year, + AlternativeNames = altNames.Where(s => !string.IsNullOrEmpty(s)).ToList(), + Year = series.Metadata.ReleaseYear, AniListId = potentialAnilistId ?? ScrobblingService.GetAniListId(series), - MalId = potentialMalId ?? ScrobblingService.GetMalId(series) + MalId = potentialMalId ?? ScrobblingService.GetMalId(series), }; + var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + try { - var results = await _kavitaPlusApiService.MatchSeries(matchRequest); + var results = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") + .WithKavitaPlusHeaders(license, token) + .PostJsonAsync(matchRequest) + .ReceiveJson>(); // Some summaries can contain multiple
s, we need to ensure it's only 1 foreach (var result in results) @@ -261,12 +251,6 @@ public class ExternalMetadataService : IExternalMetadataService return ArraySegment.Empty; } - private static List ExtractAlternativeNames(Series series) - { - List altNames = [series.LocalizedName, series.OriginalName]; - return altNames.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); - } - /// /// Retrieves Metadata about a Recommended External Series @@ -284,7 +268,9 @@ public class ExternalMetadataService : IExternalMetadataService } // This is for the Series drawer. We can get this extra information during the initial SeriesDetail call so it's all coming from the DB - var details = await GetSeriesDetail(aniListId, malId, seriesId); + + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var details = await GetSeriesDetail(license, aniListId, malId, seriesId); return details; @@ -387,9 +373,6 @@ public class ExternalMetadataService : IExternalMetadataService { // We can't rethrow because Fix match is done in a background thread and Hangfire will requeue multiple times _logger.LogInformation(ex, "Rate limit hit for matching {SeriesName} with Kavita+", series.Name); - // Fire SignalR event about this - await _eventHub.SendMessageAsync(MessageFactory.ExternalMatchRateLimitError, - MessageFactory.ExternalMatchRateLimitErrorEvent(series.Id, series.Name)); } } @@ -440,12 +423,16 @@ public class ExternalMetadataService : IExternalMetadataService try { _logger.LogDebug("Fetching Kavita+ Series Detail data for {SeriesName}", string.IsNullOrEmpty(data.SeriesName) ? data.AniListId : data.SeriesName); + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; SeriesDetailPlusApiDto? result = null; try { - // This returns an AniListSeries and Match returns ExternalSeriesDto - result = await _kavitaPlusApiService.GetSeriesDetail(data); + result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") + .WithKavitaPlusHeaders(license, token) + .PostJsonAsync(data) + .ReceiveJson(); // This returns an AniListSeries and Match returns ExternalSeriesDto } catch (FlurlHttpException ex) { @@ -460,7 +447,11 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogDebug("Hit rate limit, will retry in 3 seconds"); await Task.Delay(3000); - result = await _kavitaPlusApiService.GetSeriesDetail(data); + result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") + .WithKavitaPlusHeaders(license, token) + .PostJsonAsync(data) + .ReceiveJson< + SeriesDetailPlusApiDto>(); } else if (errorMessage.Contains("Unknown Series")) { @@ -527,7 +518,6 @@ public class ExternalMetadataService : IExternalMetadataService if (madeMetadataModification) { _unitOfWork.SeriesRepository.Update(series); - _unitOfWork.SeriesRepository.Update(series.Metadata); } } catch (Exception ex) @@ -624,8 +614,12 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = await UpdateTags(series, settings, externalMetadata, processedTags) || madeModification; madeModification = UpdateAgeRating(series, settings, processedGenres.Concat(processedTags)) || madeModification; - var staff = await SetNameAndAddAliases(settings, externalMetadata.Staff); + var staff = (externalMetadata.Staff ?? []).Select(s => + { + s.Name = settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}"; + return s; + }).ToList(); madeModification = await UpdateWriters(series, settings, staff) || madeModification; madeModification = await UpdateArtists(series, settings, staff) || madeModification; madeModification = await UpdateCharacters(series, settings, externalMetadata.Characters) || madeModification; @@ -638,49 +632,6 @@ public class ExternalMetadataService : IExternalMetadataService return madeModification; } - private async Task> SetNameAndAddAliases(MetadataSettingsDto settings, IList? staff) - { - if (staff == null || staff.Count == 0) return []; - - var nameMappings = staff.Select(s => new - { - Staff = s, - PreferredName = settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}", - AlternativeName = !settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}" - }).ToList(); - - var preferredNames = nameMappings.Select(n => n.PreferredName.ToNormalized()).Distinct().ToList(); - var alternativeNames = nameMappings.Select(n => n.AlternativeName.ToNormalized()).Distinct().ToList(); - - var existingPeople = await _unitOfWork.PersonRepository.GetPeopleByNames(preferredNames.Union(alternativeNames).ToList()); - var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople); - - var modified = false; - foreach (var mapping in nameMappings) - { - mapping.Staff.Name = mapping.PreferredName; - - if (existingPeopleDictionary.ContainsKey(mapping.PreferredName.ToNormalized())) - { - continue; - } - - - if (existingPeopleDictionary.TryGetValue(mapping.AlternativeName.ToNormalized(), out var person)) - { - modified = true; - person.Aliases.Add(new PersonAliasBuilder(mapping.PreferredName).Build()); - } - } - - if (modified) - { - await _unitOfWork.CommitAsync(); - } - - return [.. staff]; - } - private static void GenerateGenreAndTagLists(ExternalSeriesDetailDto externalMetadata, MetadataSettingsDto settings, ref List processedTags, ref List processedGenres) { @@ -784,7 +735,7 @@ public class ExternalMetadataService : IExternalMetadataService if (externalCharacters == null || externalCharacters.Count == 0) return false; - if (series.Metadata.CharacterLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People)) + if (series.Metadata.CharacterLocked && !settings.HasOverride(MetadataSettingField.People)) { return false; } @@ -799,7 +750,7 @@ public class ExternalMetadataService : IExternalMetadataService var characters = externalCharacters .Select(w => new PersonDto() { - Name = w.Name.Trim(), + Name = w.Name, AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) @@ -851,7 +802,7 @@ public class ExternalMetadataService : IExternalMetadataService } } - series.Metadata.AddKPlusOverride(MetadataSettingField.People); + return true; } @@ -866,7 +817,7 @@ public class ExternalMetadataService : IExternalMetadataService if (upstreamArtists.Count == 0) return false; - if (series.Metadata.CoverArtistLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People)) + if (series.Metadata.CoverArtistLocked && !settings.HasOverride(MetadataSettingField.People)) { return false; } @@ -880,7 +831,7 @@ public class ExternalMetadataService : IExternalMetadataService var artists = upstreamArtists .Select(w => new PersonDto() { - Name = w.Name.Trim(), + Name = w.Name, AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) @@ -909,7 +860,6 @@ public class ExternalMetadataService : IExternalMetadataService await DownloadAndSetPersonCovers(upstreamArtists); - series.Metadata.AddKPlusOverride(MetadataSettingField.People); return true; } @@ -923,7 +873,7 @@ public class ExternalMetadataService : IExternalMetadataService if (upstreamWriters.Count == 0) return false; - if (series.Metadata.WriterLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.People)) + if (series.Metadata.WriterLocked && !settings.HasOverride(MetadataSettingField.People)) { return false; } @@ -937,7 +887,7 @@ public class ExternalMetadataService : IExternalMetadataService var writers = upstreamWriters .Select(w => new PersonDto() { - Name = w.Name.Trim(), + Name = w.Name, AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) @@ -966,7 +916,7 @@ public class ExternalMetadataService : IExternalMetadataService await _unitOfWork.CommitAsync(); await DownloadAndSetPersonCovers(upstreamWriters); - series.Metadata.AddKPlusOverride(MetadataSettingField.People); + return true; } @@ -976,7 +926,7 @@ public class ExternalMetadataService : IExternalMetadataService if (!settings.EnableTags || processedTags.Count == 0) return false; - if (series.Metadata.TagsLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Tags)) + if (series.Metadata.TagsLocked && !settings.HasOverride(MetadataSettingField.Tags)) { return false; } @@ -993,11 +943,6 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = true; }, () => series.Metadata.TagsLocked = true); - if (madeModification) - { - series.Metadata.AddKPlusOverride(MetadataSettingField.Tags); - } - return madeModification; } @@ -1022,7 +967,7 @@ public class ExternalMetadataService : IExternalMetadataService if (!settings.EnableGenres || processedGenres.Count == 0) return false; - if (series.Metadata.GenresLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Genres)) + if (series.Metadata.GenresLocked && !settings.HasOverride(MetadataSettingField.Genres)) { return false; } @@ -1043,12 +988,6 @@ public class ExternalMetadataService : IExternalMetadataService { if (series.Metadata.Genres.FirstOrDefault(g => g.NormalizedTitle == genre.NormalizedTitle) != null) continue; series.Metadata.Genres.Add(genre); - madeModification = true; - } - - if (madeModification) - { - series.Metadata.AddKPlusOverride(MetadataSettingField.Genres); } return madeModification; @@ -1058,7 +997,7 @@ public class ExternalMetadataService : IExternalMetadataService { if (!settings.EnablePublicationStatus) return false; - if (series.Metadata.PublicationStatusLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.PublicationStatus)) + if (series.Metadata.PublicationStatusLocked && !settings.HasOverride(MetadataSettingField.PublicationStatus)) { return false; } @@ -1071,8 +1010,6 @@ public class ExternalMetadataService : IExternalMetadataService var status = DeterminePublicationStatus(series, chapters, externalMetadata); series.Metadata.PublicationStatus = status; - series.Metadata.PublicationStatusLocked = true; - series.Metadata.AddKPlusOverride(MetadataSettingField.PublicationStatus); return true; } catch (Exception ex) @@ -1086,7 +1023,7 @@ public class ExternalMetadataService : IExternalMetadataService private bool UpdateAgeRating(Series series, MetadataSettingsDto settings, IEnumerable allExternalTags) { - if (series.Metadata.AgeRatingLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.AgeRating)) + if (series.Metadata.AgeRatingLocked && !settings.HasOverride(MetadataSettingField.AgeRating)) { return false; } @@ -1102,7 +1039,6 @@ public class ExternalMetadataService : IExternalMetadataService if (series.Metadata.AgeRating <= ageRating) { series.Metadata.AgeRating = ageRating; - series.Metadata.AddKPlusOverride(MetadataSettingField.AgeRating); return true; } } @@ -1143,10 +1079,7 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = UpdateChapterTitle(chapter, settings, potentialMatch.Title, series.Name) || madeModification; madeModification = UpdateChapterSummary(chapter, settings, potentialMatch.Summary) || madeModification; madeModification = UpdateChapterReleaseDate(chapter, settings, potentialMatch.ReleaseDate) || madeModification; - - var hasUpdatedPublisher = await UpdateChapterPublisher(chapter, settings, potentialMatch.Publisher); - if (hasUpdatedPublisher) chapter.AddKPlusOverride(MetadataSettingField.ChapterPublisher); - madeModification = hasUpdatedPublisher || madeModification; + madeModification = await UpdateChapterPublisher(chapter, settings, potentialMatch.Publisher) || madeModification; madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.CoverArtist, potentialMatch.Artists) || madeModification; madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; @@ -1208,39 +1141,32 @@ public class ExternalMetadataService : IExternalMetadataService #region Rating - // C# can't make the implicit conversation here - float? averageCriticRating = metadata.CriticReviews.Count > 0 ? metadata.CriticReviews.Average(r => r.Rating) : null; - float? averageUserRating = metadata.UserReviews.Count > 0 ? metadata.UserReviews.Average(r => r.Rating) : null; + var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating); + var averageUserRating = metadata.UserReviews.Average(r => r.Rating); var existingRatings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapter.Id); _unitOfWork.ExternalSeriesMetadataRepository.Remove(existingRatings); - chapter.ExternalRatings = []; - - if (averageUserRating != null) - { - chapter.ExternalRatings.Add(new ExternalRating + chapter.ExternalRatings = + [ + new ExternalRating { AverageScore = (int) averageUserRating, Provider = ScrobbleProvider.Cbr, Authority = RatingAuthority.User, ProviderUrl = metadata.IssueUrl, - - }); - chapter.AverageExternalRating = averageUserRating.Value; - } - - if (averageCriticRating != null) - { - chapter.ExternalRatings.Add(new ExternalRating + }, + new ExternalRating { AverageScore = (int) averageCriticRating, Provider = ScrobbleProvider.Cbr, Authority = RatingAuthority.Critic, ProviderUrl = metadata.IssueUrl, - }); - } + }, + ]; + + chapter.AverageExternalRating = averageUserRating; madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification; @@ -1264,18 +1190,17 @@ public class ExternalMetadataService : IExternalMetadataService if (string.IsNullOrEmpty(summary)) return false; - if (chapter.SummaryLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterSummary)) + if (chapter.SummaryLocked && !settings.HasOverride(MetadataSettingField.ChapterSummary)) { return false; } - if (!string.IsNullOrWhiteSpace(summary) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterSummary)) + if (!string.IsNullOrWhiteSpace(summary) && !settings.HasOverride(MetadataSettingField.ChapterSummary)) { return false; } chapter.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(summary)); - chapter.AddKPlusOverride(MetadataSettingField.ChapterSummary); return true; } @@ -1285,18 +1210,17 @@ public class ExternalMetadataService : IExternalMetadataService if (string.IsNullOrEmpty(title)) return false; - if (chapter.TitleNameLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterTitle)) + if (chapter.TitleNameLocked && !settings.HasOverride(MetadataSettingField.ChapterTitle)) { return false; } - if (!title.Contains(seriesName) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterTitle)) + if (!title.Contains(seriesName) && !settings.HasOverride(MetadataSettingField.ChapterTitle)) { return false; } chapter.TitleName = title; - chapter.AddKPlusOverride(MetadataSettingField.ChapterTitle); return true; } @@ -1306,18 +1230,17 @@ public class ExternalMetadataService : IExternalMetadataService if (releaseDate == null || releaseDate == DateTime.MinValue) return false; - if (chapter.ReleaseDateLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterReleaseDate)) + if (chapter.ReleaseDateLocked && !settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) { return false; } - if (!HasForceOverride(settings, chapter, MetadataSettingField.ChapterReleaseDate)) + if (!settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) { return false; } chapter.ReleaseDate = releaseDate.Value; - chapter.AddKPlusOverride(MetadataSettingField.ChapterReleaseDate); return true; } @@ -1327,12 +1250,12 @@ public class ExternalMetadataService : IExternalMetadataService if (string.IsNullOrEmpty(publisher)) return false; - if (chapter.PublisherLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterPublisher)) + if (chapter.PublisherLocked && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) { return false; } - if (!string.IsNullOrWhiteSpace(publisher) && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterPublisher)) + if (!string.IsNullOrWhiteSpace(publisher) && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) { return false; } @@ -1354,7 +1277,7 @@ public class ExternalMetadataService : IExternalMetadataService if (string.IsNullOrEmpty(coverUrl)) return false; - if (chapter.CoverImageLocked && !HasForceOverride(settings, chapter, MetadataSettingField.ChapterCovers)) + if (chapter.CoverImageLocked && !settings.HasOverride(MetadataSettingField.ChapterCovers)) { return false; } @@ -1365,7 +1288,6 @@ public class ExternalMetadataService : IExternalMetadataService } await DownloadChapterCovers(chapter, coverUrl); - chapter.AddKPlusOverride(MetadataSettingField.ChapterCovers); return true; } @@ -1375,7 +1297,7 @@ public class ExternalMetadataService : IExternalMetadataService if (staff?.Count == 0) return false; - if (chapter.IsPersonRoleLocked(role) && !HasForceOverride(settings, chapter, MetadataSettingField.People)) + if (chapter.IsPersonRoleLocked(role) && !settings.HasOverride(MetadataSettingField.People)) { return false; } @@ -1389,7 +1311,7 @@ public class ExternalMetadataService : IExternalMetadataService var people = staff! .Select(w => new PersonDto() { - Name = w.Trim(), + Name = w, }) .Concat(chapter.People .Where(p => p.Role == role) @@ -1424,7 +1346,7 @@ public class ExternalMetadataService : IExternalMetadataService if (string.IsNullOrEmpty(externalMetadata.CoverUrl)) return false; - if (series.CoverImageLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Covers)) + if (series.CoverImageLocked && !settings.HasOverride(MetadataSettingField.Covers)) { return false; } @@ -1435,7 +1357,6 @@ public class ExternalMetadataService : IExternalMetadataService } await DownloadSeriesCovers(series, externalMetadata.CoverUrl); - series.Metadata.AddKPlusOverride(MetadataSettingField.Covers); return true; } @@ -1446,18 +1367,17 @@ public class ExternalMetadataService : IExternalMetadataService if (!externalMetadata.StartDate.HasValue) return false; - if (series.Metadata.ReleaseYearLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.StartDate)) + if (series.Metadata.ReleaseYearLocked && !settings.HasOverride(MetadataSettingField.StartDate)) { return false; } - if (series.Metadata.ReleaseYear != 0 && !HasForceOverride(settings, series.Metadata, MetadataSettingField.StartDate)) + if (series.Metadata.ReleaseYear != 0 && !settings.HasOverride(MetadataSettingField.StartDate)) { return false; } series.Metadata.ReleaseYear = externalMetadata.StartDate.Value.Year; - series.Metadata.AddKPlusOverride(MetadataSettingField.StartDate); return true; } @@ -1465,12 +1385,12 @@ public class ExternalMetadataService : IExternalMetadataService { if (!settings.EnableLocalizedName) return false; - if (series.LocalizedNameLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.LocalizedName)) + if (series.LocalizedNameLocked && !settings.HasOverride(MetadataSettingField.LocalizedName)) { return false; } - if (!string.IsNullOrWhiteSpace(series.LocalizedName) && !HasForceOverride(settings, series.Metadata, MetadataSettingField.LocalizedName)) + if (!string.IsNullOrWhiteSpace(series.LocalizedName) && !settings.HasOverride(MetadataSettingField.LocalizedName)) { return false; } @@ -1496,7 +1416,6 @@ public class ExternalMetadataService : IExternalMetadataService } - series.Metadata.AddKPlusOverride(MetadataSettingField.LocalizedName); return true; } @@ -1506,18 +1425,17 @@ public class ExternalMetadataService : IExternalMetadataService if (string.IsNullOrEmpty(externalMetadata.Summary)) return false; - if (series.Metadata.SummaryLocked && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Summary)) + if (series.Metadata.SummaryLocked && !settings.HasOverride(MetadataSettingField.Summary)) { return false; } - if (!string.IsNullOrWhiteSpace(series.Metadata.Summary) && !HasForceOverride(settings, series.Metadata, MetadataSettingField.Summary)) + if (!string.IsNullOrWhiteSpace(series.Metadata.Summary) && !settings.HasOverride(MetadataSettingField.Summary)) { return false; } series.Metadata.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(externalMetadata.Summary)); - series.Metadata.AddKPlusOverride(MetadataSettingField.Summary); return true; } @@ -1536,8 +1454,7 @@ public class ExternalMetadataService : IExternalMetadataService { try { - // Only choose the better image if we're overriding a user provided cover - await _coverDbService.SetSeriesCoverByUrl(series, coverUrl, false, !series.Metadata.HasSetKPlusMetadata(MetadataSettingField.Covers)); + await _coverDbService.SetSeriesCoverByUrl(series, coverUrl, false, true); } catch (Exception ex) { @@ -1599,16 +1516,16 @@ public class ExternalMetadataService : IExternalMetadataService var maxVolume = (int)(nonSpecialVolumes.Count != 0 ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0); var maxChapter = (int)chapters.Max(c => c.MaxNumber); - if (series.Format is MangaFormat.Epub or MangaFormat.Pdf && chapters.Count == 1) + if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) { series.Metadata.MaxCount = 1; } - else if (series.Metadata.TotalCount <= 1 && chapters is [{ IsSpecial: true }]) + else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial) { series.Metadata.MaxCount = series.Metadata.TotalCount; } else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && - maxVolume <= series.Metadata.TotalCount && maxVolume != Parser.DefaultChapterNumber) + maxVolume <= series.Metadata.TotalCount) { series.Metadata.MaxCount = maxVolume; } @@ -1629,7 +1546,8 @@ public class ExternalMetadataService : IExternalMetadataService { status = PublicationStatus.Ended; - if (IsSeriesCompleted(series, chapters, externalMetadata, maxVolume)) + // Check if all volumes/chapters match the total count + if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) { status = PublicationStatus.Completed; } @@ -1645,68 +1563,6 @@ public class ExternalMetadataService : IExternalMetadataService return PublicationStatus.OnGoing; } - /// - /// Returns true if the series should be marked as completed, checks loosey with chapter and series numbers. - /// Respects Specials to reach the required amount. - /// - /// - /// - /// - /// - /// - /// Updates MaxCount and TotalCount if a loosey check is used to set as completed - public static bool IsSeriesCompleted(Series series, List chapters, ExternalSeriesDetailDto externalMetadata, int maxVolumes) - { - // A series is completed if exactly the amount is found - if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) - { - return true; - } - - // If volumes are collected, check if we reach the required volumes by including specials, and decimal volumes - // - // TODO BUG: If the series has specials, that are not included in the external count. But you do own them - // This may mark the series as completed pre-maturely - // Note: I've currently opted to keep this an equals to prevent the above bug from happening - // We *could* change this to >= in the future in case this is reported by users - // If we do; test IsSeriesCompleted_Volumes_TooManySpecials needs to be updated - if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == series.Volumes.Count) - { - series.Metadata.MaxCount = series.Volumes.Count; - series.Metadata.TotalCount = series.Volumes.Count; - return true; - } - - // Note: If Kavita has specials, we should be lenient and ignore for the volume check - var volumeModifier = series.Volumes.Any(v => v.Name == Parser.SpecialVolume) ? 1 : 0; - var modifiedMinVolumeCount = series.Volumes.Count - volumeModifier; - if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == modifiedMinVolumeCount) - { - series.Metadata.MaxCount = modifiedMinVolumeCount; - series.Metadata.TotalCount = modifiedMinVolumeCount; - return true; - } - - // If no volumes are collected, the series is completed if we reach or exceed the external chapters - if (maxVolumes == Parser.DefaultChapterNumber && series.Metadata.MaxCount >= externalMetadata.Chapters) - { - series.Metadata.TotalCount = series.Metadata.MaxCount; - return true; - } - - // If no volumes are collected, the series is complete if we reach or exceed the external chapters while including - // prologues, and extra chapters - if (maxVolumes == Parser.DefaultChapterNumber && chapters.Count >= externalMetadata.Chapters) - { - series.Metadata.TotalCount = chapters.Count; - series.Metadata.MaxCount = chapters.Count; - return true; - } - - - return false; - } - private static Dictionary> ApplyFieldMappings(IEnumerable values, MetadataFieldType sourceType, List mappings) { var result = new Dictionary>(); @@ -1863,7 +1719,7 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// - private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId) + private async Task GetSeriesDetail(string license, int? aniListId, long? malId, int? seriesId) { var payload = new ExternalMetadataIdsDto() { @@ -1895,7 +1751,11 @@ public class ExternalMetadataService : IExternalMetadataService } try { - var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload); + var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + var ret = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") + .WithKavitaPlusHeaders(license, token) + .PostJsonAsync(payload) + .ReceiveJson(); ret.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(ret.Summary)); @@ -1909,10 +1769,4 @@ public class ExternalMetadataService : IExternalMetadataService return null; } - - private static bool HasForceOverride(MetadataSettingsDto settings, IHasKPlusMetadata kPlusMetadata, - MetadataSettingField field) - { - return settings.HasOverride(field) || kPlusMetadata.HasSetKPlusMetadata(field); - } } diff --git a/API/Services/Plus/KavitaPlusApiService.cs b/API/Services/Plus/KavitaPlusApiService.cs deleted file mode 100644 index ec4f414c3..000000000 --- a/API/Services/Plus/KavitaPlusApiService.cs +++ /dev/null @@ -1,126 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Collection; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Scrobbling; -using API.Entities.Enums; -using API.Extensions; -using Flurl.Http; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services.Plus; - -/// -/// All Http requests to K+ should be contained in this service, the service will not handle any errors. -/// This is expected from the caller. -/// -public interface IKavitaPlusApiService -{ - Task HasTokenExpired(string license, string token, ScrobbleProvider provider); - Task GetRateLimit(string license, string token); - Task PostScrobbleUpdate(ScrobbleDto data, string license); - Task> GetMalStacks(string malUsername, string license); - Task> MatchSeries(MatchSeriesRequestDto request); - Task GetSeriesDetail(PlusSeriesRequestDto request); - Task GetSeriesDetailById(ExternalMetadataIdsDto request); -} - -public class KavitaPlusApiService(ILogger logger, IUnitOfWork unitOfWork): IKavitaPlusApiService -{ - private const string ScrobblingPath = "/api/scrobbling/"; - - public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider) - { - var res = await Get(ScrobblingPath + "valid-key?provider=" + provider + "&key=" + token, license, token); - var str = await res.GetStringAsync(); - return bool.Parse(str); - } - - public async Task GetRateLimit(string license, string token) - { - var res = await Get(ScrobblingPath + "rate-limit?accessToken=" + token, license, token); - var str = await res.GetStringAsync(); - return int.Parse(str); - } - - public async Task PostScrobbleUpdate(ScrobbleDto data, string license) - { - return await PostAndReceive(ScrobblingPath + "update", data, license); - } - - public async Task> GetMalStacks(string malUsername, string license) - { - return await $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={malUsername}" - .WithKavitaPlusHeaders(license) - .GetJsonAsync>(); - } - - public async Task> MatchSeries(MatchSeriesRequestDto request) - { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - - return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) - .ReceiveJson>(); - } - - public async Task GetSeriesDetail(PlusSeriesRequestDto request) - { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - - return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) - .ReceiveJson(); - } - - public async Task GetSeriesDetailById(ExternalMetadataIdsDto request) - { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - - return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) - .ReceiveJson(); - } - - /// - /// Send a GET request to K+ - /// - /// only path of the uri, the host is added - /// - /// - /// - private static async Task Get(string url, string license, string? aniListToken = null) - { - return await (Configuration.KavitaPlusApiUrl + url) - .WithKavitaPlusHeaders(license, aniListToken) - .GetAsync(); - } - - /// - /// Send a POST request to K+ - /// - /// only path of the uri, the host is added - /// - /// - /// - /// Return type - /// - private static async Task PostAndReceive(string url, object body, string license, string? aniListToken = null) - { - return await (Configuration.KavitaPlusApiUrl + url) - .WithKavitaPlusHeaders(license, aniListToken) - .PostJsonAsync(body) - .ReceiveJson(); - } -} diff --git a/API/Services/Plus/LicenseService.cs b/API/Services/Plus/LicenseService.cs index 91f5a8fdd..774103518 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/API/Services/Plus/LicenseService.cs @@ -130,23 +130,22 @@ public class LicenseService( if (cacheValue.HasValue) return cacheValue.Value; } - var result = false; try { var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - result = await IsLicenseValid(serverSetting.Value); + var result = await IsLicenseValid(serverSetting.Value); + await provider.FlushAsync(); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + return result; } catch (Exception ex) { logger.LogError(ex, "There was an issue connecting to Kavita+"); - } - finally - { await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); } - return result; + return false; } /// diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index f9c3fdb09..ef22736d2 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -38,124 +38,31 @@ public enum ScrobbleProvider Kavita = 0, AniList = 1, Mal = 2, - [Obsolete("No longer supported")] + [Obsolete] GoogleBooks = 3, Cbr = 4 } public interface IScrobblingService { - /// - /// An automated job that will run against all user's tokens and validate if they are still active - /// - /// This service can validate without license check as the task which calls will be guarded - /// Task CheckExternalAccessTokens(); - - /// - /// Checks if the token has expired with , if it has double checks with K+, - /// otherwise return false. - /// - /// - /// - /// - /// Returns true if there is no license present Task HasTokenExpired(int userId, ScrobbleProvider provider); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// - /// Task ScrobbleRatingUpdate(int userId, int seriesId, float rating); - /// - /// NOP, until hardcover support has been worked out - /// - /// - /// - /// - /// - /// Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// Task ScrobbleReadingUpdate(int userId, int seriesId); - /// - /// Creates an or for - /// the given series - /// - /// - /// - /// - /// - /// Only the result of both WantToRead types is send to K+ Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead); - /// - /// Removed all processed events that are at least 7 days old - /// - /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public Task ClearProcessedEvents(); - - /// - /// Makes K+ requests for all non-processed events until rate limits are reached - /// - /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task ProcessUpdatesSinceLastSync(); - Task CreateEventsFromExistingHistory(int userId = 0); Task CreateEventsFromExistingHistoryForSeries(int seriesId); Task ClearEventsForSeries(int userId, int seriesId); } -/// -/// Context used when syncing scrobble events. Do NOT reuse between syncs -/// -public class ScrobbleSyncContext -{ - public required List ReadEvents {get; init;} - public required List RatingEvents {get; init;} - /// Do not use this as events to send to K+, use - public required List AddToWantToRead {get; init;} - /// Do not use this as events to send to K+, use - public required List RemoveWantToRead {get; init;} - /// - /// Final events list if all AddTo- and RemoveWantToRead would be processed sequentially - /// - public required List Decisions {get; init;} - /// - /// K+ license - /// - public required string License { get; init; } - /// - /// Maps userId to left over request amount - /// - public required Dictionary RateLimits { get; init; } - - /// - /// All users being scrobbled for - /// - public List Users { get; set; } = []; - /// - /// Amount of already processed events - /// - public int ProgressCounter { get; set; } - - /// - /// Sum of all events to process - /// - public int TotalCount => ReadEvents.Count + RatingEvents.Count + AddToWantToRead.Count + RemoveWantToRead.Count; -} - public class ScrobblingService : IScrobblingService { private readonly IUnitOfWork _unitOfWork; @@ -164,7 +71,6 @@ public class ScrobblingService : IScrobblingService private readonly ILicenseService _licenseService; private readonly ILocalizationService _localizationService; private readonly IEmailService _emailService; - private readonly IKavitaPlusApiService _kavitaPlusApiService; public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; @@ -174,7 +80,7 @@ public class ScrobblingService : IScrobblingService public const string AniListCharacterWebsite = "https://anilist.co/character/"; - private static readonly Dictionary WeblinkExtractionMap = new() + private static readonly Dictionary WeblinkExtractionMap = new Dictionary() { {AniListWeblinkWebsite, 0}, {MalWeblinkWebsite, 0}, @@ -198,14 +104,10 @@ public class ScrobblingService : IScrobblingService private const string UnknownSeriesErrorMessage = "Series cannot be matched for Scrobbling"; private const string AccessTokenErrorMessage = "Access Token needs to be rotated to continue scrobbling"; - private const string InvalidKPlusLicenseErrorMessage = "Kavita+ subscription no longer active"; - private const string ReviewFailedErrorMessage = "Review was unable to be saved due to upstream requirements"; - private const string BadPayLoadErrorMessage = "Bad payload from Scrobble Provider"; public ScrobblingService(IUnitOfWork unitOfWork, IEventHub eventHub, ILogger logger, - ILicenseService licenseService, ILocalizationService localizationService, IEmailService emailService, - IKavitaPlusApiService kavitaPlusApiService) + ILicenseService licenseService, ILocalizationService localizationService, IEmailService emailService) { _unitOfWork = unitOfWork; _eventHub = eventHub; @@ -213,12 +115,10 @@ public class ScrobblingService : IScrobblingService _licenseService = licenseService; _localizationService = localizationService; _emailService = emailService; - _kavitaPlusApiService = kavitaPlusApiService; FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } - #region Access token checks /// /// An automated job that will run against all user's tokens and validate if they are still active @@ -296,6 +196,7 @@ public class ScrobblingService : IScrobblingService } + public async Task HasTokenExpired(int userId, ScrobbleProvider provider) { var token = await GetTokenForProvider(userId, provider); @@ -313,14 +214,19 @@ public class ScrobblingService : IScrobblingService private async Task HasTokenExpired(string token, ScrobbleProvider provider) { - if (string.IsNullOrEmpty(token) || !TokenService.HasTokenExpired(token)) return false; + if (string.IsNullOrEmpty(token) || + !TokenService.HasTokenExpired(token)) return false; var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); if (string.IsNullOrEmpty(license.Value)) return true; try { - return await _kavitaPlusApiService.HasTokenExpired(license.Value, token, provider); + var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/valid-key?provider=" + provider + "&key=" + token) + .WithKavitaPlusHeaders(license.Value, token) + .GetStringAsync(); + + return bool.Parse(response); } catch (HttpRequestException e) { @@ -346,14 +252,59 @@ public class ScrobblingService : IScrobblingService } ?? string.Empty; } - #endregion - - #region Scrobble ingest - - public Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody) + public async Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody) { // Currently disabled until at least hardcover is implemented - return Task.CompletedTask; + return; + if (!await _licenseService.HasActiveLicense()) return; + + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); + if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); + + _logger.LogInformation("Processing Scrobbling review event for {UserId} on {SeriesName}", userId, series.Name); + if (await CheckIfCannotScrobble(userId, seriesId, series)) return; + + if (IsAniListReviewValid(reviewTitle, reviewBody)) + { + _logger.LogDebug( + "Rejecting Scrobble event for {Series}. Review is not long enough to meet requirements", series.Name); + return; + } + + var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, + ScrobbleEventType.Review); + if (existingEvt is {IsProcessed: false}) + { + _logger.LogDebug("Overriding Review scrobble event for {Series}", existingEvt.Series.Name); + existingEvt.ReviewBody = reviewBody; + existingEvt.ReviewTitle = reviewTitle; + _unitOfWork.ScrobbleRepository.Update(existingEvt); + await _unitOfWork.CommitAsync(); + return; + } + + var evt = new ScrobbleEvent() + { + SeriesId = series.Id, + LibraryId = series.LibraryId, + ScrobbleEventType = ScrobbleEventType.Review, + AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), + MalId = GetMalId(series), + AppUserId = userId, + Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), + ReviewBody = reviewBody, + ReviewTitle = reviewTitle + }; + _unitOfWork.ScrobbleRepository.Attach(evt); + await _unitOfWork.CommitAsync(); + _logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId); + } + + private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) + { + return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || + reviewTitle.Length > 120 || + reviewTitle.Length < 20); } public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating) @@ -366,11 +317,11 @@ public class ScrobblingService : IScrobblingService var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; - _logger.LogInformation("Processing Scrobbling rating event for {AppUserId} on {SeriesName}", userId, series.Name); + _logger.LogInformation("Processing Scrobbling rating event for {UserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ScoreUpdated, true); + ScrobbleEventType.ScoreUpdated); if (existingEvt is {IsProcessed: false}) { // We need to just update Volume/Chapter number @@ -395,7 +346,19 @@ public class ScrobblingService : IScrobblingService }; _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {AppUserId}", series.Name, userId); + _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {UserId}", series.Name, userId); + } + + public static long? GetMalId(Series series) + { + var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); + return malId ?? series.ExternalSeriesMetadata?.MalId; + } + + public static int? GetAniListId(Series seriesWithExternalMetadata) + { + var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); + return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; } public async Task ScrobbleReadingUpdate(int userId, int seriesId) @@ -408,52 +371,31 @@ public class ScrobblingService : IScrobblingService var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; - _logger.LogInformation("Processing Scrobbling reading event for {AppUserId} on {SeriesName}", userId, series.Name); + _logger.LogInformation("Processing Scrobbling reading event for {UserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - var isAnyProgressOnSeries = await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId); - - var volumeNumber = (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); - var chapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); - - // Check if there is an existing not yet processed event, if so update it var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ChapterRead, true); - + ScrobbleEventType.ChapterRead); if (existingEvt is {IsProcessed: false}) { - if (!isAnyProgressOnSeries) - { - _unitOfWork.ScrobbleRepository.Remove(existingEvt); - await _unitOfWork.CommitAsync(); - _logger.LogDebug("Removed scrobble event for {Series} as there is no reading progress", series.Name); - return; - } - // We need to just update Volume/Chapter number var prevChapter = $"{existingEvt.ChapterNumber}"; var prevVol = $"{existingEvt.VolumeNumber}"; - existingEvt.VolumeNumber = volumeNumber; - existingEvt.ChapterNumber = chapterNumber; - + existingEvt.VolumeNumber = + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); + existingEvt.ChapterNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); _unitOfWork.ScrobbleRepository.Update(existingEvt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Overriding scrobble event for {Series} from vol {PrevVol} ch {PrevChap} -> vol {UpdatedVol} ch {UpdatedChap}", existingEvt.Series.Name, prevVol, prevChapter, existingEvt.VolumeNumber, existingEvt.ChapterNumber); return; } - if (!isAnyProgressOnSeries) - { - // Do not create a new scrobble event if there is no progress - return; - } - try { - var evt = new ScrobbleEvent + var evt = new ScrobbleEvent() { SeriesId = series.Id, LibraryId = series.LibraryId, @@ -461,8 +403,10 @@ public class ScrobblingService : IScrobblingService AniListId = GetAniListId(series), MalId = GetMalId(series), AppUserId = userId, - VolumeNumber = volumeNumber, - ChapterNumber = chapterNumber, + VolumeNumber = + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), + ChapterNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId), Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), }; @@ -474,7 +418,7 @@ public class ScrobblingService : IScrobblingService _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Read update on {SeriesName} - Volume: {VolumeNumber} Chapter: {ChapterNumber} for User: {AppUserId}", series.Name, evt.VolumeNumber, evt.ChapterNumber, userId); + _logger.LogDebug("Added Scrobbling Read update on {SeriesName} - Volume: {VolumeNumber} Chapter: {ChapterNumber} for User: {UserId}", series.Name, evt.VolumeNumber, evt.ChapterNumber, userId); } catch (Exception ex) { @@ -487,22 +431,23 @@ public class ScrobblingService : IScrobblingService if (!await _licenseService.HasActiveLicense()) return; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); - if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - - if (!series.Library.AllowScrobbling) return; + if (series == null || !series.Library.AllowScrobbling) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - _logger.LogInformation("Processing Scrobbling want-to-read event for {AppUserId} on {SeriesName}", userId, series.Name); + _logger.LogInformation("Processing Scrobbling want-to-read event for {UserId} on {SeriesName}", userId, series.Name); // Get existing events for this series/user var existingEvents = (await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId)) .Where(e => new[] { ScrobbleEventType.AddWantToRead, ScrobbleEventType.RemoveWantToRead }.Contains(e.ScrobbleEventType)); // Remove all existing want-to-read events for this series/user - _unitOfWork.ScrobbleRepository.Remove(existingEvents); + foreach (var existingEvent in existingEvents) + { + _unitOfWork.ScrobbleRepository.Remove(existingEvent); + } // Create the new event var evt = new ScrobbleEvent() @@ -518,30 +463,707 @@ public class ScrobblingService : IScrobblingService _unitOfWork.ScrobbleRepository.Attach(evt); await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); + _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {UserId} ", series.Name, userId); } - #endregion - - #region Scrobble provider methods - - private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) + private async Task CheckIfCannotScrobble(int userId, int seriesId, Series series) { - return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || - reviewTitle.Length > 120 || - reviewTitle.Length < 20); + if (series.DontMatch) return true; + if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId)) + { + _logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, + userId); + return true; + } + + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId); + if (library is not {AllowScrobbling: true}) return true; + if (!ExternalMetadataService.IsPlusEligible(library.Type)) return true; + + return false; } - public static long? GetMalId(Series series) + private async Task GetRateLimit(string license, string aniListToken) { - var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); - return malId ?? series.ExternalSeriesMetadata?.MalId; + if (string.IsNullOrWhiteSpace(aniListToken)) return 0; + try + { + var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/rate-limit?accessToken=" + aniListToken) + .WithKavitaPlusHeaders(license, aniListToken) + .GetStringAsync(); + + return int.Parse(response); + } + catch (Exception e) + { + _logger.LogError(e, "An error happened trying to get rate limit from Kavita+ API"); + } + + return 0; } - public static int? GetAniListId(Series seriesWithExternalMetadata) + private async Task PostScrobbleUpdate(ScrobbleDto data, string license, ScrobbleEvent evt) { - var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); - return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; + try + { + var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/update") + .WithKavitaPlusHeaders(license) + .PostJsonAsync(data) + .ReceiveJson(); + + if (!response.Successful) + { + // Might want to log this under ScrobbleError + if (response.ErrorMessage != null && response.ErrorMessage.Contains("Too Many Requests")) + { + _logger.LogInformation("Hit Too many requests, sleeping to regain requests and retrying"); + await Task.Delay(TimeSpan.FromMinutes(10)); + return await PostScrobbleUpdate(data, license, evt); + } + if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unauthorized")) + { + _logger.LogCritical("Kavita+ responded with Unauthorized. Please check your subscription"); + await _licenseService.HasActiveLicense(true); + evt.IsErrored = true; + evt.ErrorDetails = "Kavita+ subscription no longer active"; + throw new KavitaException("Kavita+ responded with Unauthorized. Please check your subscription"); + } + if (response.ErrorMessage != null && response.ErrorMessage.Contains("Access token is invalid")) + { + evt.IsErrored = true; + evt.ErrorDetails = AccessTokenErrorMessage; + throw new KavitaException("Access token is invalid"); + } + if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unknown Series")) + { + // Log the Series name and Id in ScrobbleErrors + _logger.LogInformation("Kavita+ was unable to match the series: {SeriesName}", evt.Series.Name); + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + // Create a new ExternalMetadata entry to indicate that this is not matchable + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(evt.SeriesId, SeriesIncludes.ExternalMetadata); + if (series == null) return 0; + + series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata() {SeriesId = evt.SeriesId}; + series.IsBlacklisted = true; + _unitOfWork.SeriesRepository.Update(series); + + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = UnknownSeriesErrorMessage, + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + + } + + evt.IsErrored = true; + evt.ErrorDetails = UnknownSeriesErrorMessage; + } else if (response.ErrorMessage != null && response.ErrorMessage.StartsWith("Review")) + { + // Log the Series name and Id in ScrobbleErrors + _logger.LogInformation("Kavita+ was unable to save the review"); + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = response.ErrorMessage, + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + evt.IsErrored = true; + evt.ErrorDetails = "Review was unable to be saved due to upstream requirements"; + } + } + + return response.RateLeft; + } + catch (FlurlHttpException ex) + { + var errorMessage = await ex.GetResponseStringAsync(); + // Trim quotes if the response is a JSON string + errorMessage = errorMessage.Trim('"'); + + if (errorMessage.Contains("Too Many Requests")) + { + _logger.LogInformation("Hit Too many requests, sleeping to regain requests and retrying"); + await Task.Delay(TimeSpan.FromMinutes(10)); + return await PostScrobbleUpdate(data, license, evt); + } + + _logger.LogError(ex, "Scrobbling to Kavita+ API failed due to error: {ErrorMessage}", ex.Message); + if (ex.Message.Contains("Call failed with status code 500 (Internal Server Error)")) + { + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = UnknownSeriesErrorMessage, + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + evt.IsErrored = true; + evt.ErrorDetails = "Bad payload from Scrobble Provider"; + throw new KavitaException("Bad payload from Scrobble Provider"); + } + throw; + } + } + + /// + /// This will backfill events from existing progress history, ratings, and want to read for users that have a valid license + /// + /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user + public async Task CreateEventsFromExistingHistory(int userId = 0) + { + if (!await _licenseService.HasActiveLicense()) return; + + if (userId != 0) + { + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return; + if (user.HasRunScrobbleEventGeneration) + { + _logger.LogWarning("User {UserName} has already run scrobble event generation, Kavita will not generate more events", user.UserName); + return; + } + } + + + + var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); + + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) + .Where(l => userId == 0 || userId == l.Id) + .Select(u => u.Id); + + foreach (var uId in userIds) + { + var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); + foreach (var wtr in wantToRead) + { + if (!libAllowsScrobbling[wtr.LibraryId]) continue; + await ScrobbleWantToReadUpdate(uId, wtr.Id, true); + } + + var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); + foreach (var rating in ratings) + { + if (!libAllowsScrobbling[rating.Series.LibraryId]) continue; + await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); + } + + var seriesWithProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, uId, + new UserParams(), new FilterDto() + { + ReadStatus = new ReadStatus() + { + Read = true, + InProgress = true, + NotRead = false + }, + Libraries = libAllowsScrobbling.Keys.Where(k => libAllowsScrobbling[k]).ToList() + }); + + foreach (var series in seriesWithProgress) + { + if (!libAllowsScrobbling[series.LibraryId]) continue; + if (series.PagesRead <= 0) continue; // Since we only scrobble when things are higher, we can + await ScrobbleReadingUpdate(uId, series.Id); + } + + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(uId); + if (user != null) + { + user.HasRunScrobbleEventGeneration = true; + user.ScrobbleEventGenerationRan = DateTime.UtcNow; + await _unitOfWork.CommitAsync(); + } + } + } + + public async Task CreateEventsFromExistingHistoryForSeries(int seriesId) + { + if (!await _licenseService.HasActiveLicense()) return; + + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + if (series == null || !series.Library.AllowScrobbling) return; + + _logger.LogInformation("Creating Scrobbling events for Series {SeriesName}", series.Name); + + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) + .Select(u => u.Id); + + foreach (var uId in userIds) + { + // Handle "Want to Read" updates specific to the series + var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); + foreach (var wtr in wantToRead.Where(wtr => wtr.Id == seriesId)) + { + await ScrobbleWantToReadUpdate(uId, wtr.Id, true); + } + + // Handle ratings specific to the series + var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); + foreach (var rating in ratings.Where(rating => rating.SeriesId == seriesId)) + { + await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); + } + + // Handle progress updates for the specific series + var seriesProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync( + series.LibraryId, + uId, + new UserParams(), + new FilterDto + { + ReadStatus = new ReadStatus + { + Read = true, + InProgress = true, + NotRead = false + }, + Libraries = new List { series.LibraryId }, + SeriesNameQuery = series.Name + }); + + foreach (var progress in seriesProgress.Where(progress => progress.Id == seriesId)) + { + if (progress.PagesRead > 0) + { + await ScrobbleReadingUpdate(uId, progress.Id); + } + } + } + } + + /// + /// Removes all events (active) that are tied to a now-on hold series + /// + /// + /// + public async Task ClearEventsForSeries(int userId, int seriesId) + { + _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {UserId} as Series is now on hold list", seriesId, userId); + var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); + foreach (var scrobble in events) + { + _unitOfWork.ScrobbleRepository.Remove(scrobble); + } + + await _unitOfWork.CommitAsync(); + } + + /// + /// Removes all events that have been processed that are 7 days old + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public async Task ClearProcessedEvents() + { + const int daysAgo = 7; + var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo); + _unitOfWork.ScrobbleRepository.Remove(events); + _logger.LogInformation("Removing {Count} scrobble events that have been processed {DaysAgo}+ days ago", events.Count, daysAgo); + await _unitOfWork.CommitAsync(); + } + + /// + /// This is a task that is ran on a fixed schedule (every few hours or every day) that clears out the scrobble event table + /// and offloads the data to the API server which performs the syncing to the providers. + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public async Task ProcessUpdatesSinceLastSync() + { + // Check how many scrobble events we have available then only do those. + var userRateLimits = new Dictionary(); + + var progressCounter = 0; + + var librariesWithScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + .AsEnumerable() + .Where(l => l.AllowScrobbling) + .Select(l => l.Id) + .ToImmutableHashSet(); + + var errors = (await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()) + .Where(e => e.Comment == "Unknown Series" || e.Comment == UnknownSeriesErrorMessage || e.Comment == AccessTokenErrorMessage) + .Select(e => e.SeriesId) + .ToList(); + + var readEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ChapterRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var addToWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.AddWantToRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var removeWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.RemoveWantToRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + var ratingEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ScoreUpdated)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !errors.Contains(e.SeriesId)) + .ToList(); + + var decisions = CalculateNetWantToReadDecisions(addToWantToRead, removeWantToRead); + + // Clear any events that are already on error table + var erroredEvents = await _unitOfWork.ScrobbleRepository.GetAllEventsWithSeriesIds(errors); + if (erroredEvents.Count > 0) + { + _unitOfWork.ScrobbleRepository.Remove(erroredEvents); + await _unitOfWork.CommitAsync(); + } + + var totalEvents = readEvents.Count + decisions.Count + ratingEvents.Count; + if (totalEvents == 0) return; + + // Get all the applicable users to scrobble and set their rate limits + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var usersToScrobble = await PrepareUsersToScrobble(readEvents, addToWantToRead, removeWantToRead, ratingEvents, userRateLimits, license); + + + _logger.LogInformation("Scrobble Processing Details:" + + "\n Read Events: {ReadEventsCount}" + + "\n Want to Read Events: {WantToReadEventsCount}" + + "\n Rating Events: {RatingEventsCount}" + + "\n Users to Scrobble: {UsersToScrobbleCount}" + + "\n Total Events to Process: {TotalEvents}", + readEvents.Count, + decisions.Count, + ratingEvents.Count, + usersToScrobble.Count, + totalEvents); + + try + { + progressCounter = await ProcessReadEvents(readEvents, userRateLimits, usersToScrobble, totalEvents, progressCounter); + + progressCounter = await ProcessRatingEvents(ratingEvents, userRateLimits, usersToScrobble, totalEvents, progressCounter); + + progressCounter = await ProcessWantToReadRatingEvents(decisions, userRateLimits, usersToScrobble, totalEvents, progressCounter); + } + catch (FlurlHttpException ex) + { + _logger.LogError(ex, "Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data"); + return; + } + + + await SaveToDb(progressCounter, true); + _logger.LogInformation("Scrobbling Events is complete"); + + // Cleanup any events that are due to bugs or legacy + try + { + var eventsWithoutAnilistToken = (await _unitOfWork.ScrobbleRepository.GetEvents()) + .Where(e => !e.IsProcessed && !e.IsErrored) + .Where(e => string.IsNullOrEmpty(e.AppUser.AniListAccessToken)); + + _unitOfWork.ScrobbleRepository.Remove(eventsWithoutAnilistToken); + await _unitOfWork.CommitAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception when trying to delete old scrobble events when the user has no active token"); + } + } + + /// + /// Calculates the net want-to-read decisions by considering all events. + /// Returns events that represent the final state for each user/series pair. + /// + /// List of events for adding to want-to-read + /// List of events for removing from want-to-read + /// List of events that represent the final state (add or remove) + private static List CalculateNetWantToReadDecisions(List addEvents, List removeEvents) + { + // Create a dictionary to track the latest event for each user/series combination + var latestEvents = new Dictionary<(int SeriesId, int AppUserId), ScrobbleEvent>(); + + // Process all add events + foreach (var addEvent in addEvents) + { + var key = (addEvent.SeriesId, addEvent.AppUserId); + + if (latestEvents.TryGetValue(key, out var value) && addEvent.CreatedUtc <= value.CreatedUtc) continue; + + value = addEvent; + latestEvents[key] = value; + } + + // Process all remove events + foreach (var removeEvent in removeEvents) + { + var key = (removeEvent.SeriesId, removeEvent.AppUserId); + + if (latestEvents.TryGetValue(key, out var value) && removeEvent.CreatedUtc <= value.CreatedUtc) continue; + + value = removeEvent; + latestEvents[key] = value; + } + + // Return all events that represent the final state + return latestEvents.Values.ToList(); + } + + private async Task ProcessWantToReadRatingEvents(List decisions, Dictionary userRateLimits, List usersToScrobble, int totalEvents, int progressCounter) + { + progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, + totalEvents, evt => Task.FromResult(new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + ChapterNumber = evt.ChapterNumber, + VolumeNumber = (int?) evt.VolumeNumber, + AniListToken = evt.AppUser.AniListAccessToken, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Year = evt.Series.Metadata.ReleaseYear + })); + + // After decisions, we need to mark all the want to read and remove from want to read as completed + if (decisions.Any(d => d.IsProcessed)) + { + foreach (var scrobbleEvent in decisions.Where(d => d.IsProcessed)) + { + scrobbleEvent.IsProcessed = true; + scrobbleEvent.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(scrobbleEvent); + } + await _unitOfWork.CommitAsync(); + } + + return progressCounter; + } + + private async Task ProcessRatingEvents(List ratingEvents, Dictionary userRateLimits, List usersToScrobble, + int totalEvents, int progressCounter) + { + return await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter, + totalEvents, evt => Task.FromResult(new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + AniListToken = evt.AppUser.AniListAccessToken, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Rating = evt.Rating, + Year = evt.Series.Metadata.ReleaseYear + })); + } + + + private async Task ProcessReadEvents(List readEvents, Dictionary userRateLimits, List usersToScrobble, int totalEvents, + int progressCounter) + { + // Recalculate the highest volume/chapter + foreach (var readEvt in readEvents) + { + // Note: this causes skewing in the scrobble history because it makes it look like there are duplicate events + readEvt.VolumeNumber = + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, + readEvt.AppUser.Id); + readEvt.ChapterNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, + readEvt.AppUser.Id); + _unitOfWork.ScrobbleRepository.Update(readEvt); + } + + return await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalEvents, + async evt => new ScrobbleDto() + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + ChapterNumber = evt.ChapterNumber, + VolumeNumber = (int?) evt.VolumeNumber, + AniListToken = evt.AppUser.AniListAccessToken!, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + ScrobbleDateUtc = evt.LastModifiedUtc, + Year = evt.Series.Metadata.ReleaseYear, + StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id), + LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id), + }); + } + + + private async Task> PrepareUsersToScrobble(List readEvents, List addToWantToRead, List removeWantToRead, List ratingEvents, + Dictionary userRateLimits, ServerSetting license) + { + // For all userIds, ensure that we can connect and have access + var usersToScrobble = readEvents.Select(r => r.AppUser) + .Concat(addToWantToRead.Select(r => r.AppUser)) + .Concat(removeWantToRead.Select(r => r.AppUser)) + .Concat(ratingEvents.Select(r => r.AppUser)) + .Where(user => !string.IsNullOrEmpty(user.AniListAccessToken)) + .Where(user => user.UserPreferences.AniListScrobblingEnabled) + .DistinctBy(u => u.Id) + .ToList(); + + foreach (var user in usersToScrobble) + { + await SetAndCheckRateLimit(userRateLimits, user, license.Value); + } + + return usersToScrobble; + } + + + private async Task ProcessEvents(IEnumerable events, Dictionary userRateLimits, + int usersToScrobble, int progressCounter, int totalProgress, Func> createEvent) + { + var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + foreach (var evt in events) + { + _logger.LogDebug("Processing Reading Events: {Count} / {Total}", progressCounter, totalProgress); + progressCounter++; + + // Check if this media item can even be processed for this user + if (!CanProcessScrobbleEvent(evt)) + { + continue; + } + + if (TokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = "AniList token has expired and needs rotating. Scrobbling wont work until then", + Details = $"User: {evt.AppUser.UserName}, Expired: {TokenService.GetTokenExpiry(evt.AppUser.AniListAccessToken)}", + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + await _unitOfWork.CommitAsync(); + continue; + } + + if (evt.Series.IsBlacklisted || evt.Series.DontMatch) + { + _logger.LogInformation("Series {SeriesName} ({SeriesId}) can't be matched and thus cannot scrobble this event", evt.Series.Name, evt.SeriesId); + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = UnknownSeriesErrorMessage, + Details = $"User: {evt.AppUser.UserName} Series: {evt.Series.Name}", + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + evt.IsErrored = true; + evt.ErrorDetails = UnknownSeriesErrorMessage; + evt.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(evt); + await _unitOfWork.CommitAsync(); + + continue; + } + + var count = await SetAndCheckRateLimit(userRateLimits, evt.AppUser, license.Value); + userRateLimits[evt.AppUserId] = count; + if (count == 0) + { + if (usersToScrobble == 1) break; + continue; + } + + try + { + var data = await createEvent(evt); + // We need to handle the encoding and changing it to the old one until we can update the API layer to handle these + // which could happen in v0.8.3 + if (data.VolumeNumber is Parser.SpecialVolumeNumber or Parser.DefaultChapterNumber) + { + data.VolumeNumber = 0; + } + + if (data.ChapterNumber is Parser.DefaultChapterNumber) + { + data.ChapterNumber = 0; + } + userRateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, license.Value, evt); + evt.IsProcessed = true; + evt.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(evt); + } + catch (FlurlHttpException) + { + // If a flurl exception occured, the API is likely down. Kill processing + throw; + } + catch (KavitaException ex) + { + if (ex.Message.Contains("Access token is invalid")) + { + _logger.LogCritical(ex, "Access Token for UserId: {UserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id); + evt.IsErrored = true; + evt.ErrorDetails = AccessTokenErrorMessage; + _unitOfWork.ScrobbleRepository.Update(evt); + } + } + catch (Exception ex) + { + /* Swallow as it's already been handled in PostScrobbleUpdate */ + _logger.LogError(ex, "Error processing event {EventId}", evt.Id); + } + await SaveToDb(progressCounter); + // We can use count to determine how long to sleep based on rate gain. It might be specific to AniList, but we can model others + var delay = count > 10 ? TimeSpan.FromMilliseconds(ScrobbleSleepTime) : TimeSpan.FromSeconds(60); + await Task.Delay(delay); + } + + await SaveToDb(progressCounter, true); + return progressCounter; + } + + private async Task SaveToDb(int progressCounter, bool force = false) + { + if ((force || progressCounter % 5 == 0) && _unitOfWork.HasChanges()) + { + _logger.LogDebug("Saving Scrobbling Event Processing Progress"); + await _unitOfWork.CommitAsync(); + } + } + + + private static bool CanProcessScrobbleEvent(ScrobbleEvent readEvent) + { + var userProviders = GetUserProviders(readEvent.AppUser); + switch (readEvent.Series.Library.Type) + { + case LibraryType.Manga when MangaProviders.Intersect(userProviders).Any(): + case LibraryType.Comic when + ComicProviders.Intersect(userProviders).Any(): + case LibraryType.Book when + BookProviders.Intersect(userProviders).Any(): + case LibraryType.LightNovel when + LightNovelProviders.Intersect(userProviders).Any(): + return true; + default: + return false; + } + } + + private static List GetUserProviders(AppUser appUser) + { + var providers = new List(); + if (!string.IsNullOrEmpty(appUser.AniListAccessToken)) providers.Add(ScrobbleProvider.AniList); + + return providers; } /// @@ -556,23 +1178,23 @@ public class ScrobblingService : IScrobblingService foreach (var webLink in webLinks.Split(',')) { if (!webLink.StartsWith(website)) continue; - var tokens = webLink.Split(website)[1].Split('/'); var value = tokens[index]; - if (typeof(T) == typeof(int?)) { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; + if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) + return (T)(object)intValue; } else if (typeof(T) == typeof(int)) { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; - + if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) + return (T)(object)intValue; return default; } else if (typeof(T) == typeof(long?)) { - if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue)) return (T)(object)longValue; + if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue)) + return (T)(object)longValue; } else if (typeof(T) == typeof(string)) { @@ -616,778 +1238,6 @@ public class ScrobblingService : IScrobblingService return id is null or 0 ? string.Empty : $"{url}{id}/"; } - #endregion - - /// - /// Returns false if, the series is on hold or Don't Match, or when the library has scrobbling disable or not eligible - /// - /// - /// - /// - /// - private async Task CheckIfCannotScrobble(int userId, int seriesId, Series series) - { - if (series.DontMatch) return true; - - if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId)) - { - _logger.LogInformation("Series {SeriesName} is on AppUserId {AppUserId}'s hold list. Not scrobbling", series.Name, userId); - return true; - } - - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId); - if (library is not {AllowScrobbling: true} || !ExternalMetadataService.IsPlusEligible(library.Type)) return true; - - return false; - } - - /// - /// Returns the rate limit from the K+ api - /// - /// - /// - /// - private async Task GetRateLimit(string license, string aniListToken) - { - if (string.IsNullOrWhiteSpace(aniListToken)) return 0; - - try - { - return await _kavitaPlusApiService.GetRateLimit(license, aniListToken); - } - catch (Exception e) - { - _logger.LogError(e, "An error happened trying to get rate limit from Kavita+ API"); - } - - return 0; - } - - #region Scrobble process (Requests to K+) - - /// - /// Retrieve all events for which the series has not errored, then delete all current errors - /// - private async Task PrepareScrobbleContext() - { - var librariesWithScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) - .AsEnumerable() - .Where(l => l.AllowScrobbling) - .Select(l => l.Id) - .ToImmutableHashSet(); - - var erroredSeries = (await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()) - .Where(e => e.Comment is "Unknown Series" or UnknownSeriesErrorMessage or AccessTokenErrorMessage) - .Select(e => e.SeriesId) - .ToList(); - - var readEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ChapterRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - var addToWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.AddWantToRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - var removeWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.RemoveWantToRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - var ratingEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ScoreUpdated)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !erroredSeries.Contains(e.SeriesId)) - .ToList(); - - return new ScrobbleSyncContext - { - ReadEvents = readEvents, - RatingEvents = ratingEvents, - AddToWantToRead = addToWantToRead, - RemoveWantToRead = removeWantToRead, - Decisions = CalculateNetWantToReadDecisions(addToWantToRead, removeWantToRead), - RateLimits = [], - License = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value, - }; - } - - /// - /// Filters users who can scrobble, sets their rate limit and updates the - /// - /// - /// - private async Task PrepareUsersToScrobble(ScrobbleSyncContext ctx) - { - // For all userIds, ensure that we can connect and have access - var usersToScrobble = ctx.ReadEvents.Select(r => r.AppUser) - .Concat(ctx.AddToWantToRead.Select(r => r.AppUser)) - .Concat(ctx.RemoveWantToRead.Select(r => r.AppUser)) - .Concat(ctx.RatingEvents.Select(r => r.AppUser)) - .Where(user => !string.IsNullOrEmpty(user.AniListAccessToken)) - .Where(user => user.UserPreferences.AniListScrobblingEnabled) - .DistinctBy(u => u.Id) - .ToList(); - - foreach (var user in usersToScrobble) - { - await SetAndCheckRateLimit(ctx.RateLimits, user, ctx.License); - } - - ctx.Users = usersToScrobble; - } - - /// - /// Cleans up any events that are due to bugs or legacy - /// - private async Task CleanupOldOrBuggedEvents() - { - try - { - var eventsWithoutAnilistToken = (await _unitOfWork.ScrobbleRepository.GetEvents()) - .Where(e => e is { IsProcessed: false, IsErrored: false }) - .Where(e => string.IsNullOrEmpty(e.AppUser.AniListAccessToken)); - - _unitOfWork.ScrobbleRepository.Remove(eventsWithoutAnilistToken); - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when trying to delete old scrobble events when the user has no active token"); - } - } - - /// - /// This is a task that is run on a fixed schedule (every few hours or every day) that clears out the scrobble event table - /// and offloads the data to the API server which performs the syncing to the providers. - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ProcessUpdatesSinceLastSync() - { - var ctx = await PrepareScrobbleContext(); - if (ctx.TotalCount == 0) return; - - // Get all the applicable users to scrobble and set their rate limits - await PrepareUsersToScrobble(ctx); - - _logger.LogInformation("Scrobble Processing Details:" + - "\n Read Events: {ReadEventsCount}" + - "\n Want to Read Events: {WantToReadEventsCount}" + - "\n Rating Events: {RatingEventsCount}" + - "\n Users to Scrobble: {UsersToScrobbleCount}" + - "\n Total Events to Process: {TotalEvents}", - ctx.ReadEvents.Count, - ctx.Decisions.Count, - ctx.RatingEvents.Count, - ctx.Users.Count, - ctx.TotalCount); - - try - { - await ProcessReadEvents(ctx); - await ProcessRatingEvents(ctx); - await ProcessWantToReadRatingEvents(ctx); - } - catch (FlurlHttpException ex) - { - _logger.LogError(ex, "Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data"); - return; - } - - - await SaveToDb(ctx.ProgressCounter, true); - _logger.LogInformation("Scrobbling Events is complete"); - - await CleanupOldOrBuggedEvents(); - } - - /// - /// Calculates the net want-to-read decisions by considering all events. - /// Returns events that represent the final state for each user/series pair. - /// - /// List of events for adding to want-to-read - /// List of events for removing from want-to-read - /// List of events that represent the final state (add or remove) - private static List CalculateNetWantToReadDecisions(List addEvents, List removeEvents) - { - // Create a dictionary to track the latest event for each user/series combination - var latestEvents = new Dictionary<(int SeriesId, int AppUserId), ScrobbleEvent>(); - - // Process all add events - foreach (var addEvent in addEvents) - { - var key = (addEvent.SeriesId, addEvent.AppUserId); - - if (latestEvents.TryGetValue(key, out var value) && addEvent.CreatedUtc <= value.CreatedUtc) continue; - - value = addEvent; - latestEvents[key] = value; - } - - // Process all remove events - foreach (var removeEvent in removeEvents) - { - var key = (removeEvent.SeriesId, removeEvent.AppUserId); - - if (latestEvents.TryGetValue(key, out var value) && removeEvent.CreatedUtc <= value.CreatedUtc) continue; - - value = removeEvent; - latestEvents[key] = value; - } - - // Return all events that represent the final state - return latestEvents.Values.ToList(); - } - - private async Task ProcessWantToReadRatingEvents(ScrobbleSyncContext ctx) - { - await ProcessEvents(ctx.Decisions, ctx, evt => Task.FromResult(new ScrobbleDto - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - ChapterNumber = evt.ChapterNumber, - VolumeNumber = (int?) evt.VolumeNumber, - AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Year = evt.Series.Metadata.ReleaseYear - })); - - // After decisions, we need to mark all the want to read and remove from want to read as completed - var processedDecisions = ctx.Decisions.Where(d => d.IsProcessed).ToList(); - if (processedDecisions.Count > 0) - { - foreach (var scrobbleEvent in processedDecisions) - { - scrobbleEvent.IsProcessed = true; - scrobbleEvent.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(scrobbleEvent); - } - await _unitOfWork.CommitAsync(); - } - } - - private async Task ProcessRatingEvents(ScrobbleSyncContext ctx) - { - await ProcessEvents(ctx.RatingEvents, ctx, evt => Task.FromResult(new ScrobbleDto - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Rating = evt.Rating, - Year = evt.Series.Metadata.ReleaseYear - })); - } - - private async Task ProcessReadEvents(ScrobbleSyncContext ctx) - { - // Recalculate the highest volume/chapter - foreach (var readEvt in ctx.ReadEvents) - { - // Note: this causes skewing in the scrobble history because it makes it look like there are duplicate events - readEvt.VolumeNumber = - (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, - readEvt.AppUser.Id); - readEvt.ChapterNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, - readEvt.AppUser.Id); - _unitOfWork.ScrobbleRepository.Update(readEvt); - } - - await ProcessEvents(ctx.ReadEvents, ctx, async evt => new ScrobbleDto - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - ChapterNumber = evt.ChapterNumber, - VolumeNumber = (int?) evt.VolumeNumber, - AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - ScrobbleDateUtc = evt.LastModifiedUtc, - Year = evt.Series.Metadata.ReleaseYear, - StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id), - LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id), - }); - } - - /// - /// Returns true if the user token is valid - /// - /// - /// - /// If the token is not, adds a scrobble error - private async Task ValidateUserToken(ScrobbleEvent evt) - { - if (!TokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) - return true; - - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = "AniList token has expired and needs rotating. Scrobbling wont work until then", - Details = $"User: {evt.AppUser.UserName}, Expired: {TokenService.GetTokenExpiry(evt.AppUser.AniListAccessToken)}", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - await _unitOfWork.CommitAsync(); - return false; - } - - /// - /// Returns true if the series can be scrobbled - /// - /// - /// - /// If the series cannot be scrobbled, adds a scrobble error - private async Task ValidateSeriesCanBeScrobbled(ScrobbleEvent evt) - { - if (evt.Series is { IsBlacklisted: false, DontMatch: false }) - return true; - - _logger.LogInformation("Series {SeriesName} ({SeriesId}) can't be matched and thus cannot scrobble this event", - evt.Series.Name, evt.SeriesId); - - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = UnknownSeriesErrorMessage, - Details = $"User: {evt.AppUser.UserName} Series: {evt.Series.Name}", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - - evt.SetErrorMessage(UnknownSeriesErrorMessage); - evt.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(evt); - await _unitOfWork.CommitAsync(); - return false; - } - - /// - /// Removed Special parses numbers from chatter and volume numbers - /// - /// - /// - private static ScrobbleDto NormalizeScrobbleData(ScrobbleDto data) - { - // We need to handle the encoding and changing it to the old one until we can update the API layer to handle these - // which could happen in v0.8.3 - if (data.VolumeNumber is Parser.SpecialVolumeNumber or Parser.DefaultChapterNumber) - { - data.VolumeNumber = 0; - } - - - if (data.ChapterNumber is Parser.DefaultChapterNumber) - { - data.ChapterNumber = 0; - } - - - return data; - } - - /// - /// Loops through all events, and post them to K+ - /// - /// - /// - /// - private async Task ProcessEvents(IEnumerable events, ScrobbleSyncContext ctx, Func> createEvent) - { - foreach (var evt in events.Where(CanProcessScrobbleEvent)) - { - _logger.LogDebug("Processing Scrobble Events: {Count} / {Total}", ctx.ProgressCounter, ctx.TotalCount); - ctx.ProgressCounter++; - - if (!await ValidateUserToken(evt)) continue; - if (!await ValidateSeriesCanBeScrobbled(evt)) continue; - - var count = await SetAndCheckRateLimit(ctx.RateLimits, evt.AppUser, ctx.License); - if (count == 0) - { - if (ctx.Users.Count == 1) break; - continue; - } - - try - { - var data = NormalizeScrobbleData(await createEvent(evt)); - - ctx.RateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, ctx.License, evt); - - evt.IsProcessed = true; - evt.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(evt); - } - catch (FlurlHttpException) - { - // If a flurl exception occured, the API is likely down. Kill processing - throw; - } - catch (KavitaException ex) - { - if (ex.Message.Contains("Access token is invalid")) - { - _logger.LogCritical(ex, "Access Token for AppUserId: {AppUserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id); - evt.SetErrorMessage(AccessTokenErrorMessage); - _unitOfWork.ScrobbleRepository.Update(evt); - - // Ensure series with this error do not get re-processed next sync - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = AccessTokenErrorMessage, - Details = $"{evt.AppUser.UserName} has an invalid access token (K+ Error)", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId, - }); - } - } - catch (Exception ex) - { - /* Swallow as it's already been handled in PostScrobbleUpdate */ - _logger.LogError(ex, "Error processing event {EventId}", evt.Id); - } - - await SaveToDb(ctx.ProgressCounter); - - // We can use count to determine how long to sleep based on rate gain. It might be specific to AniList, but we can model others - var delay = count > 10 ? TimeSpan.FromMilliseconds(ScrobbleSleepTime) : TimeSpan.FromSeconds(60); - await Task.Delay(delay); - } - - await SaveToDb(ctx.ProgressCounter, true); - } - - /// - /// Save changes every five updates - /// - /// - /// Ignore update count check - private async Task SaveToDb(int progressCounter, bool force = false) - { - if ((force || progressCounter % 5 == 0) && _unitOfWork.HasChanges()) - { - _logger.LogDebug("Saving Scrobbling Event Processing Progress"); - await _unitOfWork.CommitAsync(); - } - } - - /// - /// If no errors have been logged for the given series, creates a new Unknown series error, and blacklists the series - /// - /// - /// - private async Task MarkSeriesAsUnknown(ScrobbleDto data, ScrobbleEvent evt) - { - if (await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) return; - - // Create a new ExternalMetadata entry to indicate that this is not matchable - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(evt.SeriesId, SeriesIncludes.ExternalMetadata); - if (series == null) return; - - series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata {SeriesId = evt.SeriesId}; - series.IsBlacklisted = true; - _unitOfWork.SeriesRepository.Update(series); - - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError - { - Comment = UnknownSeriesErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - - /// - /// Makes the K+ request, and handles any exceptions that occur - /// - /// Data to send to K+ - /// K+ license key - /// Related scrobble event - /// - /// Exceptions may be rethrown as a KavitaException - /// Some FlurlHttpException are also rethrown - public async Task PostScrobbleUpdate(ScrobbleDto data, string license, ScrobbleEvent evt) - { - try - { - var response = await _kavitaPlusApiService.PostScrobbleUpdate(data, license); - - _logger.LogDebug("K+ API Scrobble response for series {SeriesName}: Successful {Successful}, ErrorMessage {ErrorMessage}, ExtraInformation: {ExtraInformation}, RateLeft: {RateLeft}", - data.SeriesName, response.Successful, response.ErrorMessage, response.ExtraInformation, response.RateLeft); - - if (response.Successful || response.ErrorMessage == null) return response.RateLeft; - - // Might want to log this under ScrobbleError - if (response.ErrorMessage.Contains("Too Many Requests")) - { - _logger.LogInformation("Hit Too many requests while posting scrobble updates, sleeping to regain requests and retrying"); - await Task.Delay(TimeSpan.FromMinutes(10)); - return await PostScrobbleUpdate(data, license, evt); - } - - if (response.ErrorMessage.Contains("Unauthorized")) - { - _logger.LogCritical("Kavita+ responded with Unauthorized. Please check your subscription"); - await _licenseService.HasActiveLicense(true); - evt.SetErrorMessage(InvalidKPlusLicenseErrorMessage); - throw new KavitaException("Kavita+ responded with Unauthorized. Please check your subscription"); - } - - if (response.ErrorMessage.Contains("Access token is invalid")) - { - evt.SetErrorMessage(AccessTokenErrorMessage); - throw new KavitaException("Access token is invalid"); - } - - if (response.ErrorMessage.Contains("Unknown Series")) - { - // Log the Series name and Id in ScrobbleErrors - _logger.LogInformation("Kavita+ was unable to match the series: {SeriesName}", evt.Series.Name); - await MarkSeriesAsUnknown(data, evt); - evt.SetErrorMessage(UnknownSeriesErrorMessage); - } else if (response.ErrorMessage.StartsWith("Review")) - { - // Log the Series name and Id in ScrobbleErrors - _logger.LogInformation("Kavita+ was unable to save the review"); - if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) - { - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = response.ErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - evt.SetErrorMessage(ReviewFailedErrorMessage); - } - - return response.RateLeft; - } - catch (FlurlHttpException ex) - { - var errorMessage = await ex.GetResponseStringAsync(); - // Trim quotes if the response is a JSON string - errorMessage = errorMessage.Trim('"'); - - if (errorMessage.Contains("Too Many Requests")) - { - _logger.LogInformation("Hit Too many requests while posting scrobble updates, sleeping to regain requests and retrying"); - await Task.Delay(TimeSpan.FromMinutes(10)); - return await PostScrobbleUpdate(data, license, evt); - } - - _logger.LogError(ex, "Scrobbling to Kavita+ API failed due to error: {ErrorMessage}", ex.Message); - if (ex.StatusCode == 500 || ex.Message.Contains("Call failed with status code 500 (Internal Server Error)")) - { - if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) - { - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = UnknownSeriesErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - evt.SetErrorMessage(BadPayLoadErrorMessage); - throw new KavitaException(BadPayLoadErrorMessage); - } - throw; - } - } - - #endregion - - #region BackFill - - - /// - /// This will backfill events from existing progress history, ratings, and want to read for users that have a valid license - /// - /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user - public async Task CreateEventsFromExistingHistory(int userId = 0) - { - if (!await _licenseService.HasActiveLicense()) return; - - if (userId != 0) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return; - if (user.HasRunScrobbleEventGeneration) - { - _logger.LogWarning("User {UserName} has already run scrobble event generation, Kavita will not generate more events", user.UserName); - return; - } - } - - var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) - .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); - - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) - .Where(l => userId == 0 || userId == l.Id) - .Where(u => !u.HasRunScrobbleEventGeneration) - .Select(u => u.Id); - - foreach (var uId in userIds) - { - await CreateEventsFromExistingHistoryForUser(uId, libAllowsScrobbling); - } - } - - /// - /// Creates wantToRead, rating, reviews, and series progress events for the suer - /// - /// - /// - private async Task CreateEventsFromExistingHistoryForUser(int userId, Dictionary libAllowsScrobbling) - { - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(userId); - foreach (var wtr in wantToRead) - { - if (!libAllowsScrobbling[wtr.LibraryId]) continue; - await ScrobbleWantToReadUpdate(userId, wtr.Id, true); - } - - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(userId); - foreach (var rating in ratings) - { - if (!libAllowsScrobbling[rating.Series.LibraryId]) continue; - await ScrobbleRatingUpdate(userId, rating.SeriesId, rating.Rating); - } - - var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(userId); - foreach (var review in reviews.Where(r => !string.IsNullOrEmpty(r.Review))) - { - if (!libAllowsScrobbling[review.Series.LibraryId]) continue; - await ScrobbleReviewUpdate(userId, review.SeriesId, string.Empty, review.Review!); - } - - var seriesWithProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, userId, - new UserParams(), new FilterDto - { - ReadStatus = new ReadStatus - { - Read = true, - InProgress = true, - NotRead = false - }, - Libraries = libAllowsScrobbling.Keys.Where(k => libAllowsScrobbling[k]).ToList() - }); - - foreach (var series in seriesWithProgress.Where(series => series.PagesRead > 0)) - { - if (!libAllowsScrobbling[series.LibraryId]) continue; - await ScrobbleReadingUpdate(userId, series.Id); - } - - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user != null) - { - user.HasRunScrobbleEventGeneration = true; - user.ScrobbleEventGenerationRan = DateTime.UtcNow; - await _unitOfWork.CommitAsync(); - } - } - - public async Task CreateEventsFromExistingHistoryForSeries(int seriesId) - { - if (!await _licenseService.HasActiveLicense()) return; - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); - if (series == null || !series.Library.AllowScrobbling) return; - - _logger.LogInformation("Creating Scrobbling events for Series {SeriesName}", series.Name); - - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.Id); - - foreach (var uId in userIds) - { - // Handle "Want to Read" updates specific to the series - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); - foreach (var wtr in wantToRead.Where(wtr => wtr.Id == seriesId)) - { - await ScrobbleWantToReadUpdate(uId, wtr.Id, true); - } - - // Handle ratings specific to the series - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); - foreach (var rating in ratings.Where(rating => rating.SeriesId == seriesId)) - { - await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); - } - - // Handle review specific to the series - var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId); - foreach (var review in reviews.Where(r => r.SeriesId == seriesId && !string.IsNullOrEmpty(r.Review))) - { - await ScrobbleReviewUpdate(uId, review.SeriesId, string.Empty, review.Review!); - } - - // Handle progress updates for the specific series - await ScrobbleReadingUpdate(uId, seriesId); - } - } - - #endregion - - /// - /// Removes all events (active) that are tied to a now-on hold series - /// - /// - /// - public async Task ClearEventsForSeries(int userId, int seriesId) - { - _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {AppUserId} as Series is now on hold list", seriesId, userId); - - var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); - _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); - } - - /// - /// Removes all events that have been processed that are 7 days old - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ClearProcessedEvents() - { - const int daysAgo = 7; - var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo); - _unitOfWork.ScrobbleRepository.Remove(events); - _logger.LogInformation("Removing {Count} scrobble events that have been processed {DaysAgo}+ days ago", events.Count, daysAgo); - await _unitOfWork.CommitAsync(); - } - - private static bool CanProcessScrobbleEvent(ScrobbleEvent readEvent) - { - var userProviders = GetUserProviders(readEvent.AppUser); - switch (readEvent.Series.Library.Type) - { - case LibraryType.Manga when MangaProviders.Intersect(userProviders).Any(): - case LibraryType.Comic when ComicProviders.Intersect(userProviders).Any(): - case LibraryType.Book when BookProviders.Intersect(userProviders).Any(): - case LibraryType.LightNovel when LightNovelProviders.Intersect(userProviders).Any(): - return true; - default: - return false; - } - } - - private static List GetUserProviders(AppUser appUser) - { - var providers = new List(); - if (!string.IsNullOrEmpty(appUser.AniListAccessToken)) providers.Add(ScrobbleProvider.AniList); - - return providers; - } private async Task SetAndCheckRateLimit(IDictionary userRateLimits, AppUser user, string license) { diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index 6ff8d19de..464c3b33c 100644 --- a/API/Services/ReadingItemService.cs +++ b/API/Services/ReadingItemService.cs @@ -12,7 +12,7 @@ public interface IReadingItemService int GetNumberOfPages(string filePath, MangaFormat format); string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); - ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); + ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type); } public class ReadingItemService : IReadingItemService @@ -27,6 +27,7 @@ public class ReadingItemService : IReadingItemService private readonly ImageParser _imageParser; private readonly BookParser _bookParser; private readonly PdfParser _pdfParser; + private readonly MagazineParser _magazineParser; public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService, IDirectoryService directoryService, ILogger logger) @@ -42,6 +43,7 @@ public class ReadingItemService : IReadingItemService _bookParser = new BookParser(directoryService, bookService, _basicParser); _comicVineParser = new ComicVineParser(directoryService); _pdfParser = new PdfParser(directoryService); + _magazineParser = new MagazineParser(directoryService); } @@ -71,12 +73,11 @@ public class ReadingItemService : IReadingItemService /// Path of a file /// /// Library type to determine parsing to perform - /// Enable Metadata parsing overriding filename parsing - public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) { try { - var info = Parse(path, rootPath, libraryRoot, type, enableMetadata); + var info = Parse(path, rootPath, libraryRoot, type); if (info == null) { _logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path); @@ -175,29 +176,32 @@ public class ReadingItemService : IReadingItemService /// /// /// - /// /// - private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) + private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type) { if (_comicVineParser.IsApplicable(path, type)) { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_imageParser.IsApplicable(path, type)) { - return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_bookParser.IsApplicable(path, type)) { - return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + } + if (_magazineParser.IsApplicable(path, type)) + { + return _magazineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_pdfParser.IsApplicable(path, type)) { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } if (_basicParser.IsApplicable(path, type)) { - return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); + return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); } return null; diff --git a/API/Services/ReadingProfileService.cs b/API/Services/ReadingProfileService.cs deleted file mode 100644 index 4c3dab006..000000000 --- a/API/Services/ReadingProfileService.cs +++ /dev/null @@ -1,454 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using AutoMapper; -using Kavita.Common; - -namespace API.Services; -#nullable enable - -public interface IReadingProfileService -{ - /// - /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. - /// Series (Implicit) -> Series (User) -> Library (User) -> Default - /// - /// - /// - /// - /// - Task GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false); - - /// - /// Creates a new reading profile for a user. Name must be unique per user - /// - /// - /// - /// - Task CreateReadingProfile(int userId, UserReadingProfileDto dto); - Task PromoteImplicitProfile(int userId, int profileId); - - /// - /// Updates the implicit reading profile for a series, creates one if none exists - /// - /// - /// - /// - /// - Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto); - - /// - /// Updates the non-implicit reading profile for the given series, and removes implicit profiles - /// - /// - /// - /// - /// - Task UpdateParent(int userId, int seriesId, UserReadingProfileDto dto); - - /// - /// Updates a given reading profile for a user - /// - /// - /// - /// - /// Does not update connected series and libraries - Task UpdateReadingProfile(int userId, UserReadingProfileDto dto); - - /// - /// Deletes a given profile for a user - /// - /// - /// - /// - /// - /// The default profile for the user cannot be deleted - Task DeleteReadingProfile(int userId, int profileId); - - /// - /// Binds the reading profile to the series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task AddProfileToSeries(int userId, int profileId, int seriesId); - /// - /// Binds the reading profile to many series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task BulkAddProfileToSeries(int userId, int profileId, IList seriesIds); - /// - /// Remove all reading profiles bound to the series - /// - /// - /// - /// - Task ClearSeriesProfile(int userId, int seriesId); - - /// - /// Bind the reading profile to the library - /// - /// - /// - /// - /// - Task AddProfileToLibrary(int userId, int profileId, int libraryId); - /// - /// Remove the reading profile bound to the library, if it exists - /// - /// - /// - /// - Task ClearLibraryProfile(int userId, int libraryId); - /// - /// Returns the bound Reading Profile to a Library - /// - /// - /// - /// - Task GetReadingProfileDtoForLibrary(int userId, int libraryId); -} - -public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper): IReadingProfileService -{ - /// - /// Tries to resolve the Reading Profile for a given Series. Will first check (optionally) Implicit profiles, then check for a bound Series profile, then a bound - /// Library profile, then default to the default profile. - /// - /// - /// - /// - /// - /// - public async Task GetReadingProfileForSeries(int userId, int seriesId, bool skipImplicit = false) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, skipImplicit); - - // If there is an implicit, send back - var implicitProfile = - profiles.FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind == ReadingProfileKind.Implicit); - if (implicitProfile != null) return implicitProfile; - - // Next check for a bound Series profile - var seriesProfile = profiles - .FirstOrDefault(p => p.SeriesIds.Contains(seriesId) && p.Kind != ReadingProfileKind.Implicit); - if (seriesProfile != null) return seriesProfile; - - // Check for a library bound profile - var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - if (series == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); - - var libraryProfile = profiles - .FirstOrDefault(p => p.LibraryIds.Contains(series.LibraryId) && p.Kind != ReadingProfileKind.Implicit); - if (libraryProfile != null) return libraryProfile; - - // Fallback to the default profile - return profiles.First(p => p.Kind == ReadingProfileKind.Default); - } - - public async Task GetReadingProfileDtoForSeries(int userId, int seriesId, bool skipImplicit = false) - { - return mapper.Map(await GetReadingProfileForSeries(userId, seriesId, skipImplicit)); - } - - public async Task UpdateParent(int userId, int seriesId, UserReadingProfileDto dto) - { - var parentProfile = await GetReadingProfileForSeries(userId, seriesId, true); - - UpdateReaderProfileFields(parentProfile, dto, false); - unitOfWork.AppUserReadingProfileRepository.Update(parentProfile); - - // Remove the implicit profile when we UpdateParent (from reader) as it is implied that we are already bound with a non-implicit profile - await DeleteImplicateReadingProfilesForSeries(userId, [seriesId]); - - await unitOfWork.CommitAsync(); - return mapper.Map(parentProfile); - } - - public async Task UpdateReadingProfile(int userId, UserReadingProfileDto dto) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, dto.Id); - if (profile == null) throw new KavitaException("profile-does-not-exist"); - - UpdateReaderProfileFields(profile, dto); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - - await unitOfWork.CommitAsync(); - return mapper.Map(profile); - } - - public async Task CreateReadingProfile(int userId, UserReadingProfileDto dto) - { - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null) throw new UnauthorizedAccessException(); - - if (await unitOfWork.AppUserReadingProfileRepository.IsProfileNameInUse(userId, dto.Name)) throw new KavitaException("name-already-in-use"); - - var newProfile = new AppUserReadingProfileBuilder(user.Id).Build(); - UpdateReaderProfileFields(newProfile, dto); - - unitOfWork.AppUserReadingProfileRepository.Add(newProfile); - user.ReadingProfiles.Add(newProfile); - - await unitOfWork.CommitAsync(); - - return mapper.Map(newProfile); - } - - /// - /// Promotes the implicit profile to a user profile. Removes the series from other profiles. - /// - /// - /// - /// - public async Task PromoteImplicitProfile(int userId, int profileId) - { - // Get all the user's profiles including the implicit - var allUserProfiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, false); - var profileToPromote = allUserProfiles.First(r => r.Id == profileId); - var seriesId = profileToPromote.SeriesIds[0]; // An Implicit series can only be bound to 1 Series - - // Check if there are any reading profiles (Series) already bound to the series - var existingSeriesProfile = allUserProfiles.FirstOrDefault(r => r.SeriesIds.Contains(seriesId) && r.Kind == ReadingProfileKind.User); - if (existingSeriesProfile != null) - { - existingSeriesProfile.SeriesIds.Remove(seriesId); - unitOfWork.AppUserReadingProfileRepository.Update(existingSeriesProfile); - } - - // Convert the implicit profile into a proper Series - var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - if (series == null) throw new KavitaException("series-doesnt-exist"); // Shouldn't happen - - profileToPromote.Kind = ReadingProfileKind.User; - profileToPromote.Name = await localizationService.Translate(userId, "generated-reading-profile-name", series.Name); - profileToPromote.Name = EnsureUniqueProfileName(allUserProfiles, profileToPromote.Name); - profileToPromote.NormalizedName = profileToPromote.Name.ToNormalized(); - unitOfWork.AppUserReadingProfileRepository.Update(profileToPromote); - - await unitOfWork.CommitAsync(); - - return mapper.Map(profileToPromote); - } - - private static string EnsureUniqueProfileName(IList allUserProfiles, string name) - { - var counter = 1; - var newName = name; - while (allUserProfiles.Any(p => p.Name == newName)) - { - newName = $"{name} ({counter})"; - counter++; - } - - return newName; - } - - public async Task UpdateImplicitReadingProfile(int userId, int seriesId, UserReadingProfileDto dto) - { - var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); - if (user == null) throw new UnauthorizedAccessException(); - - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var existingProfile = profiles.FirstOrDefault(rp => rp.Kind == ReadingProfileKind.Implicit && rp.SeriesIds.Contains(seriesId)); - - // Series already had an implicit profile, update it - if (existingProfile is {Kind: ReadingProfileKind.Implicit}) - { - UpdateReaderProfileFields(existingProfile, dto, false); - unitOfWork.AppUserReadingProfileRepository.Update(existingProfile); - await unitOfWork.CommitAsync(); - - return mapper.Map(existingProfile); - } - - var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId) ?? throw new KeyNotFoundException(); - var newProfile = new AppUserReadingProfileBuilder(userId) - .WithSeries(series) - .WithKind(ReadingProfileKind.Implicit) - .Build(); - - // Set name to something fitting for debugging if needed - UpdateReaderProfileFields(newProfile, dto, false); - newProfile.Name = $"Implicit Profile for {seriesId}"; - newProfile.NormalizedName = newProfile.Name.ToNormalized(); - - user.ReadingProfiles.Add(newProfile); - await unitOfWork.CommitAsync(); - - return mapper.Map(newProfile); - } - - public async Task DeleteReadingProfile(int userId, int profileId) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - if (profile.Kind == ReadingProfileKind.Default) throw new KavitaException("cant-delete-default-profile"); - - unitOfWork.AppUserReadingProfileRepository.Remove(profile); - await unitOfWork.CommitAsync(); - } - - public async Task AddProfileToSeries(int userId, int profileId, int seriesId) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - await DeleteImplicitAndRemoveFromUserProfiles(userId, [seriesId], []); - - profile.SeriesIds.Add(seriesId); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - - await unitOfWork.CommitAsync(); - } - - public async Task BulkAddProfileToSeries(int userId, int profileId, IList seriesIds) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - await DeleteImplicitAndRemoveFromUserProfiles(userId, seriesIds, []); - - profile.SeriesIds.AddRange(seriesIds.Except(profile.SeriesIds)); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - - await unitOfWork.CommitAsync(); - } - - public async Task ClearSeriesProfile(int userId, int seriesId) - { - await DeleteImplicitAndRemoveFromUserProfiles(userId, [seriesId], []); - await unitOfWork.CommitAsync(); - } - - public async Task AddProfileToLibrary(int userId, int profileId, int libraryId) - { - var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId); - if (profile == null) throw new KavitaException("profile-doesnt-exist"); - - await DeleteImplicitAndRemoveFromUserProfiles(userId, [], [libraryId]); - - profile.LibraryIds.Add(libraryId); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - await unitOfWork.CommitAsync(); - } - - public async Task ClearLibraryProfile(int userId, int libraryId) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var libraryProfile = profiles.FirstOrDefault(p => p.LibraryIds.Contains(libraryId)); - if (libraryProfile != null) - { - libraryProfile.LibraryIds.Remove(libraryId); - unitOfWork.AppUserReadingProfileRepository.Update(libraryProfile); - } - - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - } - - public async Task GetReadingProfileDtoForLibrary(int userId, int libraryId) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId, true); - return mapper.Map(profiles.FirstOrDefault(p => p.LibraryIds.Contains(libraryId))); - } - - private async Task DeleteImplicitAndRemoveFromUserProfiles(int userId, IList seriesIds, IList libraryIds) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var implicitProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) - .Where(rp => rp.Kind == ReadingProfileKind.Implicit) - .ToList(); - unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); - - var nonImplicitProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any() || rp.LibraryIds.Intersect(libraryIds).Any()) - .Where(rp => rp.Kind != ReadingProfileKind.Implicit); - - foreach (var profile in nonImplicitProfiles) - { - profile.SeriesIds.RemoveAll(seriesIds.Contains); - profile.LibraryIds.RemoveAll(libraryIds.Contains); - unitOfWork.AppUserReadingProfileRepository.Update(profile); - } - } - - private async Task DeleteImplicateReadingProfilesForSeries(int userId, IList seriesIds) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var implicitProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) - .Where(rp => rp.Kind == ReadingProfileKind.Implicit) - .ToList(); - unitOfWork.AppUserReadingProfileRepository.RemoveRange(implicitProfiles); - } - - private async Task RemoveSeriesFromUserProfiles(int userId, IList seriesIds) - { - var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId); - var userProfiles = profiles - .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any()) - .Where(rp => rp.Kind == ReadingProfileKind.User) - .ToList(); - - unitOfWork.AppUserReadingProfileRepository.RemoveRange(userProfiles); - } - - public static void UpdateReaderProfileFields(AppUserReadingProfile existingProfile, UserReadingProfileDto dto, bool updateName = true) - { - if (updateName && !string.IsNullOrEmpty(dto.Name) && existingProfile.NormalizedName != dto.Name.ToNormalized()) - { - existingProfile.Name = dto.Name; - existingProfile.NormalizedName = dto.Name.ToNormalized(); - } - - // Manga Reader - existingProfile.ReadingDirection = dto.ReadingDirection; - existingProfile.ScalingOption = dto.ScalingOption; - existingProfile.PageSplitOption = dto.PageSplitOption; - existingProfile.ReaderMode = dto.ReaderMode; - existingProfile.AutoCloseMenu = dto.AutoCloseMenu; - existingProfile.ShowScreenHints = dto.ShowScreenHints; - existingProfile.EmulateBook = dto.EmulateBook; - existingProfile.LayoutMode = dto.LayoutMode; - existingProfile.BackgroundColor = string.IsNullOrEmpty(dto.BackgroundColor) ? "#000000" : dto.BackgroundColor; - existingProfile.SwipeToPaginate = dto.SwipeToPaginate; - existingProfile.AllowAutomaticWebtoonReaderDetection = dto.AllowAutomaticWebtoonReaderDetection; - existingProfile.WidthOverride = dto.WidthOverride; - existingProfile.DisableWidthOverride = dto.DisableWidthOverride; - - // Book Reader - existingProfile.BookReaderMargin = dto.BookReaderMargin; - existingProfile.BookReaderLineSpacing = dto.BookReaderLineSpacing; - existingProfile.BookReaderFontSize = dto.BookReaderFontSize; - existingProfile.BookReaderFontFamily = dto.BookReaderFontFamily; - existingProfile.BookReaderTapToPaginate = dto.BookReaderTapToPaginate; - existingProfile.BookReaderReadingDirection = dto.BookReaderReadingDirection; - existingProfile.BookReaderWritingStyle = dto.BookReaderWritingStyle; - existingProfile.BookThemeName = dto.BookReaderThemeName; - existingProfile.BookReaderLayoutMode = dto.BookReaderLayoutMode; - existingProfile.BookReaderImmersiveMode = dto.BookReaderImmersiveMode; - - // PDF Reading - existingProfile.PdfTheme = dto.PdfTheme; - existingProfile.PdfScrollMode = dto.PdfScrollMode; - existingProfile.PdfSpreadMode = dto.PdfSpreadMode; - } -} diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 78e3c41f1..9b06f7e51 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -7,13 +7,10 @@ using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs; -using API.DTOs.Person; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.Interfaces; using API.Entities.Metadata; -using API.Entities.MetadataMatching; using API.Entities.Person; using API.Extensions; using API.Helpers; @@ -36,7 +33,6 @@ public interface ISeriesService Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto); Task GetRelatedSeries(int userId, int seriesId); Task FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true); - Task FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true); Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash); Task FormatChapterName(int userId, LibraryType libraryType, bool withHash = false); @@ -122,27 +118,23 @@ public class SeriesService : ISeriesService { series.Metadata.ReleaseYear = updateSeriesMetadataDto.SeriesMetadata.ReleaseYear; series.Metadata.ReleaseYearLocked = true; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.StartDate); } if (series.Metadata.PublicationStatus != updateSeriesMetadataDto.SeriesMetadata.PublicationStatus) { series.Metadata.PublicationStatus = updateSeriesMetadataDto.SeriesMetadata.PublicationStatus; series.Metadata.PublicationStatusLocked = true; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.PublicationStatus); } if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary)) { updateSeriesMetadataDto.SeriesMetadata.Summary = string.Empty; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Summary); } if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim()) { series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim() ?? string.Empty; series.Metadata.SummaryLocked = true; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Summary); } if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language) @@ -201,7 +193,6 @@ public class SeriesService : ISeriesService series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata?.AgeRating ?? AgeRating.Unknown; series.Metadata.AgeRatingLocked = true; await _readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating); - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating); } else { @@ -213,7 +204,6 @@ public class SeriesService : ISeriesService if (updatedRating > series.Metadata.AgeRating) { series.Metadata.AgeRating = updatedRating; - series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating); } } } @@ -327,7 +317,6 @@ public class SeriesService : ISeriesService return true; } - _unitOfWork.SeriesRepository.Update(series.Metadata); await _unitOfWork.CommitAsync(); // Trigger code to clean up tags, collections, people, etc @@ -371,7 +360,8 @@ public class SeriesService : ISeriesService var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedNames); // Use a dictionary for quick lookups - var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople); + var existingPeopleDictionary = existingPeople.DistinctBy(p => p.NormalizedName) + .ToDictionary(p => p.NormalizedName, p => p); // List to track people that will be added to the metadata var peopleToAdd = new List(); @@ -642,7 +632,7 @@ public class SeriesService : ISeriesService public async Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash) { - if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || libraryType == LibraryType.Book)) throw new ArgumentException("Chapter Title cannot be null"); + if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || (libraryType == LibraryType.Book || libraryType == LibraryType.Magazine))) throw new ArgumentException("Chapter Title cannot be null"); if (isSpecial) { @@ -652,9 +642,10 @@ public class SeriesService : ISeriesService var hashSpot = withHash ? "#" : string.Empty; var baseChapter = libraryType switch { - LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle!), + LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle ?? string.Empty), LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterRange), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterRange), + LibraryType.Magazine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle ?? string.Empty), LibraryType.ComicVine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterRange), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterRange), LibraryType.Image => await _localizationService.Translate(userId, "chapter-num", chapterRange), @@ -675,10 +666,6 @@ public class SeriesService : ISeriesService return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Range, chapter.Title, withHash); } - public async Task FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true) - { - return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Range, chapter.Title, withHash); - } // TODO: Refactor this out and use FormatChapterTitle instead across library public async Task FormatChapterName(int userId, LibraryType libraryType, bool withHash = false) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 575f89b3b..e73d82b1f 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -215,9 +215,9 @@ public class TaskScheduler : ITaskScheduler RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false), LicenseService.Cron, RecurringJobOptions); - // KavitaPlus Scrobbling (every hour) - randomise minutes to spread requests out for K+ + // KavitaPlus Scrobbling (every hour) RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(), - Cron.Hourly(Rnd.Next(0, 60)), RecurringJobOptions); + "0 */1 * * *", RecurringJobOptions); RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(), Cron.Daily, RecurringJobOptions); diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index 015613965..cebf08b97 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -206,12 +206,17 @@ public class CoverDbService : ICoverDbService throw new KavitaException($"Could not grab publisher image for {publisherName}"); } + _logger.LogTrace("Fetching publisher image from {Url}", publisherLink.Sanitize()); + // Download the publisher file using Flurl + var publisherStream = await publisherLink + .AllowHttpStatus("2xx,304") + .GetStreamAsync(); + // Create the destination file path + using var image = Image.NewFromStream(publisherStream); var filename = ImageService.GetPublisherFormat(publisherName, encodeFormat); - _logger.LogTrace("Fetching publisher image from {Url}", publisherLink.Sanitize()); - await DownloadImageFromUrl(publisherName, encodeFormat, publisherLink, _directoryService.PublisherDirectory); - + image.WriteToFile(Path.Combine(_directoryService.PublisherDirectory, filename)); _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName.Sanitize()); return filename; @@ -297,27 +302,7 @@ public class CoverDbService : ICoverDbService .GetStreamAsync(); using var image = Image.NewFromStream(imageStream); - try - { - image.WriteToFile(targetFile); - } - catch (Exception ex) - { - switch (encodeFormat) - { - case EncodeFormat.PNG: - image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - case EncodeFormat.WEBP: - image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - case EncodeFormat.AVIF: - image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); - } - } + image.WriteToFile(targetFile); return filename; } @@ -400,13 +385,14 @@ public class CoverDbService : ICoverDbService private async Task FallbackToKavitaReaderPublisher(string publisherName) { const string publisherFileName = "publishers.txt"; + var externalLink = string.Empty; var allOverrides = await GetCachedData(publisherFileName) ?? await $"{NewHost}publishers/{publisherFileName}".GetStringAsync(); // Cache immediately await CacheDataAsync(publisherFileName, allOverrides); - if (string.IsNullOrEmpty(allOverrides)) return string.Empty; + if (string.IsNullOrEmpty(allOverrides)) return externalLink; var externalFile = allOverrides .Split("\n") @@ -429,7 +415,7 @@ public class CoverDbService : ICoverDbService throw new KavitaException($"Could not grab publisher image for {publisherName}"); } - return $"{NewHost}publishers/{externalFile}"; + return $"{NewHost}publishers/{externalLink}"; } private async Task CacheDataAsync(string fileName, string? content) @@ -515,7 +501,7 @@ public class CoverDbService : ICoverDbService else { _directoryService.DeleteFiles([tempFullPath]); - return; + person.CoverImage = Path.GetFileName(existingPath); } } else @@ -586,19 +572,14 @@ public class CoverDbService : ICoverDbService var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); if (choseNewImage) { - // Don't delete the Series cover unless it is an override, otherwise the first chapter will be null - if (existingPath.Contains(ImageService.GetSeriesFormat(series.Id))) - { - _directoryService.DeleteFiles([existingPath]); - } - + _directoryService.DeleteFiles([existingPath]); _directoryService.CopyFile(tempFullPath, finalFullPath); series.CoverImage = finalFileName; } else { _directoryService.DeleteFiles([tempFullPath]); - return; + series.CoverImage = Path.GetFileName(existingPath); } } catch (Exception ex) @@ -637,7 +618,6 @@ public class CoverDbService : ICoverDbService } } - // TODO: Refactor this to IHasCoverImage instead of a hard entity type public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false) { if (!string.IsNullOrEmpty(url)) @@ -671,7 +651,6 @@ public class CoverDbService : ICoverDbService else { _directoryService.DeleteFiles([tempFullPath]); - return; } chapter.CoverImage = finalFileName; diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index fec0304a8..d2e6437a3 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -310,7 +310,7 @@ public class LibraryWatcher : ILibraryWatcher if (rootFolder.Count == 0) return string.Empty; // Select the first folder and join with library folder, this should give us the folder to scan. - return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[^1])); + return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1])); } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 83558eaa0..c3f36ef2e 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -804,7 +804,7 @@ public class ParseScannedFiles { // Process files sequentially result.ParserInfos = files - .Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata)) + .Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type)) .Where(info => info != null) .ToList()!; } @@ -812,7 +812,7 @@ public class ParseScannedFiles { // Process files in parallel var tasks = files.Select(file => Task.Run(() => - _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata))); + _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type))); var infos = await Task.WhenAll(tasks); result.ParserInfos = infos.Where(info => info != null).ToList()!; diff --git a/API/Services/Tasks/Scanner/Parser/BasicParser.cs b/API/Services/Tasks/Scanner/Parser/BasicParser.cs index 168ca7f01..1462ab3d3 100644 --- a/API/Services/Tasks/Scanner/Parser/BasicParser.cs +++ b/API/Services/Tasks/Scanner/Parser/BasicParser.cs @@ -12,7 +12,7 @@ namespace API.Services.Tasks.Scanner.Parser; /// public class BasicParser(IDirectoryService directoryService, IDefaultParser imageParser) : DefaultParser(directoryService) { - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) { var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. @@ -20,7 +20,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag if (Parser.IsImage(filePath)) { - return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo); + return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, comicInfo); } var ret = new ParserInfo() @@ -101,12 +101,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag } // Patch in other information from ComicInfo - if (enableMetadata) - { - UpdateFromComicInfo(ret); - } - - + UpdateFromComicInfo(ret); if (ret.Volumes == Parser.LooseLeafVolume && ret.Chapters == Parser.DefaultChapter) { diff --git a/API/Services/Tasks/Scanner/Parser/BookParser.cs b/API/Services/Tasks/Scanner/Parser/BookParser.cs index 14f42c989..499e554ef 100644 --- a/API/Services/Tasks/Scanner/Parser/BookParser.cs +++ b/API/Services/Tasks/Scanner/Parser/BookParser.cs @@ -5,7 +5,7 @@ namespace API.Services.Tasks.Scanner.Parser; public class BookParser(IDirectoryService directoryService, IBookService bookService, BasicParser basicParser) : DefaultParser(directoryService) { - public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null) + public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null) { var info = bookService.ParseInfo(filePath); if (info == null) return null; @@ -35,7 +35,7 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer } else { - var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, enableMetadata, comicInfo); + var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, comicInfo); info.Merge(info2); if (hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series, type) .Equals(Parser.LooseLeafVolume)) diff --git a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs index b60f28aee..f632bcd59 100644 --- a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs +++ b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs @@ -19,7 +19,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser /// /// /// - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) { if (type != LibraryType.ComicVine) return null; @@ -57,7 +57,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser { if (!Parser.IsSeriesAndYear(directory)) continue; info.Series = directory; - info.Volumes = Parser.ParseYear(directory); + info.Volumes = Parser.ParseYearFromSeries(directory); break; } @@ -72,7 +72,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (Parser.IsSeriesAndYear(directoryName)) { info.Series = directoryName; - info.Volumes = Parser.ParseYear(directoryName); + info.Volumes = Parser.ParseYearFromSeries(directoryName); } } } @@ -81,10 +81,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser info.IsSpecial = Parser.IsSpecial(info.Filename, type) || Parser.IsSpecial(info.ComicInfo?.Format, type); // Patch in other information from ComicInfo - if (enableMetadata) - { - UpdateFromComicInfo(info); - } + UpdateFromComicInfo(info); if (string.IsNullOrEmpty(info.Series)) { diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 687617fd7..679d6a031 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -8,7 +8,7 @@ namespace API.Services.Tasks.Scanner.Parser; public interface IDefaultParser { - ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null); + ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null); void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret); bool IsApplicable(string filePath, LibraryType type); } @@ -26,9 +26,8 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau /// /// Root folder /// Allows different Regex to be used for parsing. - /// Allows overriding data from metadata (ComicInfo/pdf/epub) /// or null if Series was empty - public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null); + public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null); /// /// Fills out by trying to parse volume, chapters, and series from folders diff --git a/API/Services/Tasks/Scanner/Parser/ImageParser.cs b/API/Services/Tasks/Scanner/Parser/ImageParser.cs index 12f9f4d50..415533631 100644 --- a/API/Services/Tasks/Scanner/Parser/ImageParser.cs +++ b/API/Services/Tasks/Scanner/Parser/ImageParser.cs @@ -7,7 +7,7 @@ namespace API.Services.Tasks.Scanner.Parser; public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService) { - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) { if (!IsApplicable(filePath, type)) return null; diff --git a/API/Services/Tasks/Scanner/Parser/MagazineParser.cs b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs new file mode 100644 index 000000000..1aa0ff19d --- /dev/null +++ b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs @@ -0,0 +1,86 @@ +using System.IO; +using System.Linq; +using API.Data.Metadata; +using API.Entities.Enums; + +namespace API.Services.Tasks.Scanner.Parser; +#nullable enable + +public class MagazineParser(IDirectoryService directoryService) : DefaultParser(directoryService) +{ + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, + ComicInfo? comicInfo = null) + { + if (!IsApplicable(filePath, type)) return null; + + var ret = new ParserInfo + { + Volumes = Parser.LooseLeafVolume, + Chapters = Parser.DefaultChapter, + ComicInfo = comicInfo, + Format = Parser.ParseFormat(filePath), + Filename = Path.GetFileName(filePath), + FullFilePath = Parser.NormalizePath(filePath), + Series = string.Empty, + }; + + // Try to parse Series from the filename + var libraryPath = directoryService.FileSystem.DirectoryInfo.New(rootPath).Parent?.FullName ?? rootPath; + var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); + ret.Series = Parser.ParseMagazineSeries(fileName); + ret.Volumes = Parser.ParseMagazineVolume(fileName); + ret.Chapters = Parser.ParseMagazineChapter(fileName); + + if (string.IsNullOrEmpty(ret.Series) || (string.IsNullOrEmpty(ret.Chapters) && string.IsNullOrEmpty(ret.Volumes))) + { + // Fallback to the parent folder. We can also likely grab Volume (year) from here + var folders = directoryService.GetFoldersTillRoot(libraryPath, filePath).ToList(); + // Usually the LAST folder is the Series and everything up to can have Volume + + + if (string.IsNullOrEmpty(ret.Series)) + { + ret.Series = Parser.CleanTitle(folders[^1]); + } + + var hasGeoCode = !string.IsNullOrEmpty(Parser.ParseGeoCode(ret.Series)); + foreach (var folder in folders[..^1]) + { + if (ret.Volumes == Parser.LooseLeafVolume) + { + var vol = Parser.ParseYear(folder); // TODO: This might be better as YearFromSeries + if (!string.IsNullOrEmpty(vol) && vol != folder) + { + ret.Volumes = vol; + } + } + + // If folder has a language code in it, then we add that to the Series (Wired (UK)) + if (!hasGeoCode) + { + var geoCode = Parser.ParseGeoCode(folder); + if (!string.IsNullOrEmpty(geoCode)) + { + ret.Series = $"{ret.Series} ({geoCode})"; + hasGeoCode = true; + } + } + + } + } + + return ret; + } + + /// + /// Only applicable for PDF Files and Magazine library type + /// + /// + /// + /// + public override bool IsApplicable(string filePath, LibraryType type) + { + return type == LibraryType.Magazine && Parser.IsPdf(filePath); + } + +} diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index c0b130f91..84b723fa9 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -64,6 +66,8 @@ public static partial class Parser /// private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(? /// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data @@ -165,9 +169,9 @@ public static partial class Parser new Regex( @"(卷|册)(?\d+)", MatchOptions, RegexTimeout), - // Korean Volume: 제n화|회|장 -> Volume n, n화|권|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) + // Korean Volume: 제n화|권|회|장 -> Volume n, n화|권|회|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) new Regex( - @"제?(?\d+(\.\d+)?)(권|화|장)", + @"제?(?\d+(\.\d+)?)(권|회|화|장)", MatchOptions, RegexTimeout), // Korean Season: 시즌n -> Season n, new Regex( @@ -667,6 +671,69 @@ public static partial class Parser MatchOptions, RegexTimeout ); + #region Magazine + + private static readonly HashSet GeoCodes = new(CreateCountryCodes()); + private static readonly Dictionary MonthMappings = CreateMonthMappings(); + private static readonly Regex[] MagazineSeriesRegex = + [ + // 3D World - 2018 UK, 3D World - 022014 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*\d{4,6}.*", + MatchOptions, RegexTimeout), + // AIR International - April 2018 UK + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*.*", + MatchOptions, RegexTimeout), + // AIR International #1 // This breaks the way the code works + // new Regex( + // @"^(?.+?)(_|\s)+?#", + // MatchOptions, RegexTimeout) + // The New Yorker - April 2, 2018 USA + // AIR International Magazine 2006 + // AIR International Vol. 14 No. 3 (ISSN 1011-3250) + ]; + + private static readonly Regex[] MagazineVolumeRegex = new[] + { + // 3D World - 2018 UK, 3D World - 022014 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*\d{2}?(?\d{4}).*", + MatchOptions, RegexTimeout), + // 3D World - Sept 2018 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*\D+(?\d{4}).*", + MatchOptions, RegexTimeout), + // 3D World - Sept 2018 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*\D+(?\d{4}).*", + MatchOptions, RegexTimeout), + + }; + + private static readonly Regex[] MagazineChapterRegex = new[] + { + // 3D World - September 2023 #2 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*.*#(?\d+).*", + MatchOptions, RegexTimeout), + // Computer Weekly - September 2023 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*(?January|February|March|April|May|June|July|August|September|October|November|December).*", + MatchOptions, RegexTimeout), + // Computer Weekly - Sept 2023 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*(?Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Oct|Nov|Dec).*", + MatchOptions, RegexTimeout), + }; + + private static readonly Regex YearRegex = new( + @"(\b|\s|_)[1-9]{1}\d{3}(\b|\s|_)", + MatchOptions, RegexTimeout + ); + + #endregion + public static MangaFormat ParseFormat(string filePath) @@ -739,6 +806,20 @@ public static partial class Parser return string.Empty; } + public static string ParseMagazineSeries(string filename) + { + foreach (var regex in MagazineSeriesRegex) + { + var matches = regex.Matches(filename); + var group = matches + .Select(match => match.Groups["Series"]) + .FirstOrDefault(group => group.Success && group != Match.Empty); + if (group != null) return CleanTitle(group.Value, true); + } + + return string.Empty; + } + public static string ParseMangaVolume(string filename) { foreach (var regex in MangaVolumeRegex) @@ -776,6 +857,137 @@ public static partial class Parser } + public static string ParseMagazineVolume(string filename) + { + foreach (var regex in MagazineVolumeRegex) + { + var matches = regex.Matches(filename); + foreach (var group in matches.Select(match => match.Groups)) + { + if (!group["Volume"].Success || group["Volume"] == Match.Empty) continue; + + var value = group["Volume"].Value; + return FormatValue(value, false); + } + } + + return LooseLeafVolume; + } + + private static string[] CreateCountryCodes() + { + var codes = CultureInfo.GetCultures(CultureTypes.SpecificCultures) + .Select(culture => new RegionInfo(culture.Name).TwoLetterISORegionName) + .Distinct() + .OrderBy(code => code) + .ToList(); + codes.Add("UK"); + return codes.ToArray(); + } + + + private static Dictionary CreateMonthMappings() + { + Dictionary mappings = new(StringComparer.OrdinalIgnoreCase); + + // Add English month names and shorthands + for (var i = 1; i <= 12; i++) + { + var month = new DateTime(2022, i, 1); + var monthName = month.ToString("MMMM", CultureInfo.InvariantCulture); + var monthAbbreviation = month.ToString("MMM", CultureInfo.InvariantCulture); + + mappings[monthName] = i; + mappings[monthAbbreviation] = i; + } + + // Add mappings for other languages if needed + + return mappings; + } + + public static string ParseMagazineChapter(string filename) + { + foreach (var regex in MagazineChapterRegex) + { + var matches = regex.Matches(filename); + foreach (var groups in matches.Select(match => match.Groups)) + { + if (!groups["Chapter"].Success || groups["Chapter"] == Match.Empty) continue; + + var value = groups["Chapter"].Value; + // If value has non-digits, we need to convert to a digit + if (IsNumberRegex().IsMatch(value)) return FormatValue(value, false); + if (MonthMappings.TryGetValue(value, out var parsedMonth)) + { + return FormatValue($"{parsedMonth}", false); + } + } + } + + return DefaultChapter; + } + + /// + /// Tries to parse a GeoCode (UK, US) out of a string + /// + /// + /// + public static string? ParseGeoCode(string? value) + { + if (string.IsNullOrEmpty(value)) return value; + const string pattern = @"\b(?:\(|\[|\{)([A-Z]{2})(?:\)|\]|\})\b|^([A-Z]{2})$"; + + // Match the pattern in the input string + var match = Regex.Match(value, pattern, RegexOptions.IgnoreCase); + + if (match.Success) + { + // Extract the GeoCode from the first capturing group if it exists, + // otherwise, extract the GeoCode from the second capturing group + var extractedCode = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + + // Validate the extracted GeoCode against the list of valid GeoCodes + if (GeoCodes.Contains(extractedCode)) + { + return extractedCode; + } + } + + return null; + } + + + // /// + // /// Tries to parse a GTIN/ISBN out of a string + // /// + // /// + // /// + // public static string? ParseGTIN(string? value) + // { + // if (string.IsNullOrEmpty(value)) return value; + // const string pattern = @"\b(?:\(|\[|\{)([A-Z]{2})(?:\)|\]|\})\b|^([A-Z]{2})$"; + // + // // Match the pattern in the input string + // var match = Regex.Match(value, pattern, RegexOptions.IgnoreCase); + // + // if (match.Success) + // { + // // Extract the GeoCode from the first capturing group if it exists, + // // otherwise, extract the GeoCode from the second capturing group + // var extractedCode = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + // + // // Validate the extracted GeoCode against the list of valid GeoCodes + // if (GeoCodes.Contains(extractedCode)) + // { + // return extractedCode; + // } + // } + // + // return null; + // } + + private static string FormatValue(string value, bool hasPart) { if (!value.Contains('-')) @@ -1160,18 +1372,20 @@ public static partial class Parser } /// - /// Parse a Year from a Comic Series: Series Name (YEAR) + /// Extracts year from Series (Year) /// - /// Harley Quinn (2024) returns 2024 /// /// - public static string ParseYear(string? name) + public static string ParseYearFromSeries(string? name) { if (string.IsNullOrEmpty(name)) return string.Empty; var match = SeriesAndYearRegex.Match(name); - if (!match.Success) return string.Empty; + return !match.Success ? string.Empty : match.Groups["Year"].Value; + } - return match.Groups["Year"].Value; + public static string ParseYear(string? value) + { + return string.IsNullOrEmpty(value) ? string.Empty : YearRegex.Match(value).Value; } public static string? RemoveExtensionIfSupported(string? filename) diff --git a/API/Services/Tasks/Scanner/Parser/PdfParser.cs b/API/Services/Tasks/Scanner/Parser/PdfParser.cs index 80bfa9a48..bc12e2c77 100644 --- a/API/Services/Tasks/Scanner/Parser/PdfParser.cs +++ b/API/Services/Tasks/Scanner/Parser/PdfParser.cs @@ -6,7 +6,7 @@ namespace API.Services.Tasks.Scanner.Parser; public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService) { - public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null) + public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null) { var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); var ret = new ParserInfo @@ -68,18 +68,14 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); } - if (enableMetadata) + // Patch in other information from ComicInfo + UpdateFromComicInfo(ret); + + if (comicInfo != null && !string.IsNullOrEmpty(comicInfo.Title)) { - // Patch in other information from ComicInfo - UpdateFromComicInfo(ret); - - if (comicInfo != null && !string.IsNullOrEmpty(comicInfo.Title)) - { - ret.Title = comicInfo.Title.Trim(); - } + ret.Title = comicInfo.Title.Trim(); } - if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && type == LibraryType.Book) { ret.IsSpecial = true; diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 307408adb..454c72733 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -126,17 +126,13 @@ public class ProcessSeries : IProcessSeries series.Format = firstParsedInfo.Format; } - var removePrefix = library.RemovePrefixForSortName; - var sortName = removePrefix ? BookSortTitlePrefixHelper.GetSortTitle(series.Name) : series.Name; - if (string.IsNullOrEmpty(series.SortName)) { - series.SortName = sortName; + series.SortName = series.Name; } - if (!series.SortNameLocked) { - series.SortName = sortName; + series.SortName = series.Name; if (!string.IsNullOrEmpty(firstParsedInfo.SeriesSort)) { series.SortName = firstParsedInfo.SeriesSort; @@ -884,8 +880,6 @@ public class ProcessSeries : IProcessSeries existingFile.FileName = Parser.Parser.RemoveExtensionIfSupported(existingFile.FilePath); existingFile.FilePath = Parser.Parser.NormalizePath(existingFile.FilePath); existingFile.Bytes = fileInfo.Length; - existingFile.KoreaderHash = KoreaderHelper.HashContents(existingFile.FilePath); - // We skip updating DB here with last modified time so that metadata refresh can do it } else @@ -894,7 +888,6 @@ public class ProcessSeries : IProcessSeries var file = new MangaFileBuilder(info.FullFilePath, info.Format, _readingItemService.GetNumberOfPages(info.FullFilePath, info.Format)) .WithExtension(fileInfo.Extension) .WithBytes(fileInfo.Length) - .WithHash() .Build(); chapter.Files.Add(file); } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index cb5f4302f..e22ee4bb6 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -521,11 +521,6 @@ public class ScannerService : IScannerService // Validations are done, now we can start actual scan _logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); - if (!library.EnableMetadata) - { - _logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name); - } - // This doesn't work for something like M:/Manga/ and a series has library folder as root var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); if (!shouldUseLibraryScan) diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 4ccf79abb..123b610ff 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -52,7 +52,6 @@ public interface IVersionUpdaterService Task PushUpdate(UpdateNotificationDto update); Task> GetAllReleases(int count = 0); Task GetNumberOfReleasesBehind(bool stableOnly = false); - void BustGithubCache(); } @@ -385,7 +384,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc <= CacheDuration) { var cachedData = await File.ReadAllTextAsync(_cacheLatestReleaseFilePath); - return JsonSerializer.Deserialize(cachedData); + return System.Text.Json.JsonSerializer.Deserialize(cachedData); } return null; @@ -408,7 +407,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService { try { - var json = JsonSerializer.Serialize(update, JsonOptions); + var json = System.Text.Json.JsonSerializer.Serialize(update, JsonOptions); await File.WriteAllTextAsync(_cacheLatestReleaseFilePath, json); } catch (Exception ex) @@ -447,21 +446,6 @@ public partial class VersionUpdaterService : IVersionUpdaterService .Count(u => u.IsReleaseNewer); } - /// - /// Clears the Github cache - /// - public void BustGithubCache() - { - try - { - File.Delete(_cacheFilePath); - File.Delete(_cacheLatestReleaseFilePath); - } catch (Exception ex) - { - _logger.LogError(ex, "Failed to clear Github cache"); - } - } - private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update) { if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null; diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 87a464e6a..de9818b79 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -1,6 +1,5 @@ using System; using API.DTOs.Update; -using API.Entities.Person; using API.Extensions; using API.Services.Plus; @@ -148,14 +147,6 @@ public static class MessageFactory /// Volume is removed from server /// public const string VolumeRemoved = "VolumeRemoved"; - /// - /// A Person merged has been merged into another - /// - public const string PersonMerged = "PersonMerged"; - /// - /// A Rate limit error was hit when matching a series with Kavita+ - /// - public const string ExternalMatchRateLimitError = "ExternalMatchRateLimitError"; public static SignalRMessage DashboardUpdateEvent(int userId) { @@ -670,29 +661,4 @@ public static class MessageFactory EventType = ProgressEventType.Single, }; } - - public static SignalRMessage PersonMergedMessage(Person dst, Person src) - { - return new SignalRMessage() - { - Name = PersonMerged, - Body = new - { - srcId = src.Id, - dstName = dst.Name, - }, - }; - } - public static SignalRMessage ExternalMatchRateLimitErrorEvent(int seriesId, string seriesName) - { - return new SignalRMessage() - { - Name = ExternalMatchRateLimitError, - Body = new - { - seriesId = seriesId, - seriesName = seriesName, - }, - }; - } } diff --git a/API/Startup.cs b/API/Startup.cs index f57cb7d01..34af22154 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -55,9 +55,6 @@ public class Startup { _config = config; _env = env; - - // Disable Hangfire Automatic Retry - GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute { Attempts = 0 }); } // This method gets called by the runtime. Use this method to add services to the container. @@ -226,7 +223,7 @@ public class Startup // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ICacheService cacheService, - IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService, IVersionUpdaterService versionService) + IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService) { var logger = serviceProvider.GetRequiredService>(); @@ -238,9 +235,8 @@ public class Startup // Apply all migrations on startup var dataContext = serviceProvider.GetRequiredService(); - logger.LogInformation("Running Migrations"); - #region Migrations + logger.LogInformation("Running Migrations"); // v0.7.9 await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger); @@ -293,26 +289,13 @@ public class Startup await ManualMigrateScrobbleSpecials.Migrate(dataContext, logger); await ManualMigrateScrobbleEventGen.Migrate(dataContext, logger); - // v0.8.7 - await ManualMigrateReadingProfiles.Migrate(dataContext, logger); - - #endregion - // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); - var isVersionDifferent = installVersion.Value != BuildInfo.Version.ToString(); installVersion.Value = BuildInfo.Version.ToString(); unitOfWork.SettingsRepository.Update(installVersion); await unitOfWork.CommitAsync(); logger.LogInformation("Running Migrations - complete"); - - if (isVersionDifferent) - { - // Clear the Github cache so update stuff shows correctly - versionService.BustGithubCache(); - } - }).GetAwaiter() .GetResult(); } diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index ba4fd09b7..f2d64cde6 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -17,7 +17,7 @@ public static class Configuration private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); public static readonly string KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development - ? "https://plus.kavitareader.com" : "https://plus.kavitareader.com"; // http://localhost:5020 + ? "http://localhost:5020" : "https://plus.kavitareader.com"; public static readonly string StatsApiUrl = "https://stats.kavitareader.com"; public static int Port diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index c7dd0ab94..2c9ab6dc0 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,18 +3,18 @@ net9.0 kavitareader.com Kavita - 0.8.7.1 + 0.8.6.8 en true - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/README.md b/README.md index ffff8d831..bff8f0f5c 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,13 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## Mega Sponsors -## Powered By -[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource) +## JetBrains +Thank you to [ JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. + +* [ Rider](http://www.jetbrains.com/rider/) ### License + * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * Copyright 2020-2024 diff --git a/UI/Web/src/_series-detail-common.scss b/UI/Web/src/_series-detail-common.scss index efb54f860..f043dec17 100644 --- a/UI/Web/src/_series-detail-common.scss +++ b/UI/Web/src/_series-detail-common.scss @@ -13,7 +13,7 @@ } .subtitle { - color: var(--detail-subtitle-color); + color: lightgrey; font-weight: bold; font-size: 0.8rem; } diff --git a/UI/Web/src/_tag-card-common.scss b/UI/Web/src/_tag-card-common.scss deleted file mode 100644 index 39a1e87fd..000000000 --- a/UI/Web/src/_tag-card-common.scss +++ /dev/null @@ -1,35 +0,0 @@ -.tag-card { - background-color: var(--bs-card-color, #2c2c2c); - padding: 1rem; - border-radius: 12px; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - transition: transform 0.2s ease, background 0.3s ease; - cursor: pointer; - - &.not-selectable:hover { - cursor: not-allowed; - background-color: var(--bs-card-color, #2c2c2c) !important; - } -} - -.tag-card:hover { - background-color: #3a3a3a; - //transform: translateY(-3px); // Cool effect but has a weird background issue. ROBBIE: Fix this -} - -.tag-name { - font-size: 1.1rem; - font-weight: 600; - margin-bottom: 0.5rem; - max-height: 8rem; - height: 8rem; - overflow: hidden; - text-overflow: ellipsis; -} - -.tag-meta { - font-size: 0.85rem; - display: flex; - justify-content: space-between; - color: var(--text-muted-color, #bbb); -} diff --git a/UI/Web/src/app/_helpers/form-debug.ts b/UI/Web/src/app/_helpers/form-debug.ts deleted file mode 100644 index 4ad70ac87..000000000 --- a/UI/Web/src/app/_helpers/form-debug.ts +++ /dev/null @@ -1,120 +0,0 @@ -import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms'; - -interface ValidationIssue { - path: string; - controlType: string; - value: any; - errors: { [key: string]: any } | null; - status: string; - disabled: boolean; -} - -export function analyzeFormGroupValidation(formGroup: FormGroup, basePath: string = ''): ValidationIssue[] { - const issues: ValidationIssue[] = []; - - function analyzeControl(control: AbstractControl, path: string): void { - // Determine control type for better debugging - let controlType = 'AbstractControl'; - if (control instanceof FormGroup) { - controlType = 'FormGroup'; - } else if (control instanceof FormArray) { - controlType = 'FormArray'; - } else if (control instanceof FormControl) { - controlType = 'FormControl'; - } - - // Add issue if control has validation errors or is invalid - if (control.invalid || control.errors || control.disabled) { - issues.push({ - path: path || 'root', - controlType, - value: control.value, - errors: control.errors, - status: control.status, - disabled: control.disabled - }); - } - - // Recursively check nested controls - if (control instanceof FormGroup) { - Object.keys(control.controls).forEach(key => { - const childPath = path ? `${path}.${key}` : key; - analyzeControl(control.controls[key], childPath); - }); - } else if (control instanceof FormArray) { - control.controls.forEach((childControl, index) => { - const childPath = path ? `${path}[${index}]` : `[${index}]`; - analyzeControl(childControl, childPath); - }); - } - } - - analyzeControl(formGroup, basePath); - return issues; -} - -export function printFormGroupValidation(formGroup: FormGroup, basePath: string = ''): void { - const issues = analyzeFormGroupValidation(formGroup, basePath); - - console.group(`🔍 FormGroup Validation Analysis (${basePath || 'root'})`); - console.log(`Overall Status: ${formGroup.status}`); - console.log(`Overall Valid: ${formGroup.valid}`); - console.log(`Total Issues Found: ${issues.length}`); - - if (issues.length === 0) { - console.log('✅ No validation issues found!'); - } else { - console.log('\n📋 Detailed Issues:'); - issues.forEach((issue, index) => { - console.group(`${index + 1}. ${issue.path} (${issue.controlType})`); - console.log(`Status: ${issue.status}`); - console.log(`Value:`, issue.value); - console.log(`Disabled: ${issue.disabled}`); - - if (issue.errors) { - console.log('Validation Errors:'); - Object.entries(issue.errors).forEach(([errorKey, errorValue]) => { - console.log(` • ${errorKey}:`, errorValue); - }); - } else { - console.log('No specific validation errors (but control is invalid)'); - } - console.groupEnd(); - }); - } - - console.groupEnd(); -} - -// Alternative function that returns a formatted string instead of console logging -export function getFormGroupValidationReport(formGroup: FormGroup, basePath: string = ''): string { - const issues = analyzeFormGroupValidation(formGroup, basePath); - - let report = `FormGroup Validation Report (${basePath || 'root'})\n`; - report += `Overall Status: ${formGroup.status}\n`; - report += `Overall Valid: ${formGroup.valid}\n`; - report += `Total Issues Found: ${issues.length}\n\n`; - - if (issues.length === 0) { - report += '✅ No validation issues found!'; - } else { - report += 'Detailed Issues:\n'; - issues.forEach((issue, index) => { - report += `\n${index + 1}. ${issue.path} (${issue.controlType})\n`; - report += ` Status: ${issue.status}\n`; - report += ` Value: ${JSON.stringify(issue.value)}\n`; - report += ` Disabled: ${issue.disabled}\n`; - - if (issue.errors) { - report += ' Validation Errors:\n'; - Object.entries(issue.errors).forEach(([errorKey, errorValue]) => { - report += ` • ${errorKey}: ${JSON.stringify(errorValue)}\n`; - }); - } else { - report += ' No specific validation errors (but control is invalid)\n'; - } - }); - } - - return report; -} diff --git a/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts b/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts deleted file mode 100644 index 3695651d6..000000000 --- a/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ExternalMatchRateLimitErrorEvent { - seriesId: number; - seriesName: string; -} diff --git a/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts b/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts index 05a4041c8..a8dc1ce06 100644 --- a/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts +++ b/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts @@ -1,8 +1,6 @@ import {MatchStateOption} from "./match-state-option"; -import {LibraryType} from "../library/library"; export interface ManageMatchFilter { matchStateOption: MatchStateOption; - libraryType: LibraryType | -1; searchTerm: string; } diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index bcbf9b447..000723568 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -6,15 +6,11 @@ export enum LibraryType { Book = 2, Images = 3, LightNovel = 4, - /** - * Comic (Legacy) - */ - ComicVine = 5 + ComicVine = 5, + Magazine = 6 } -export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images]; -export const allKavitaPlusMetadataApplicableTypes = [LibraryType.Manga, LibraryType.LightNovel, LibraryType.ComicVine, LibraryType.Comic]; -export const allKavitaPlusScrobbleEligibleTypes = [LibraryType.Manga, LibraryType.LightNovel]; +export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images, LibraryType.Magazine]; export interface Library { id: number; @@ -31,8 +27,6 @@ export interface Library { manageReadingLists: boolean; allowScrobbling: boolean; allowMetadataMatching: boolean; - enableMetadata: boolean; - removePrefixForSortName: boolean; collapseSeriesRelationships: boolean; libraryFileTypes: Array; excludePatterns: Array; diff --git a/UI/Web/src/app/_models/metadata/browse/browse-genre.ts b/UI/Web/src/app/_models/metadata/browse/browse-genre.ts deleted file mode 100644 index e7bb0d915..000000000 --- a/UI/Web/src/app/_models/metadata/browse/browse-genre.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Genre} from "../genre"; - -export interface BrowseGenre extends Genre { - seriesCount: number; - chapterCount: number; -} diff --git a/UI/Web/src/app/_models/metadata/browse/browse-tag.ts b/UI/Web/src/app/_models/metadata/browse/browse-tag.ts deleted file mode 100644 index 4d87370ee..000000000 --- a/UI/Web/src/app/_models/metadata/browse/browse-tag.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Tag} from "../../tag"; - -export interface BrowseTag extends Tag { - seriesCount: number; - chapterCount: number; -} diff --git a/UI/Web/src/app/_models/metadata/language.ts b/UI/Web/src/app/_models/metadata/language.ts index 28ab2b598..8b68c7233 100644 --- a/UI/Web/src/app/_models/metadata/language.ts +++ b/UI/Web/src/app/_models/metadata/language.ts @@ -4,10 +4,7 @@ export interface Language { } export interface KavitaLocale { - /** - * isoCode aka what maps to the file on disk and what transloco loads - */ - fileName: string; + fileName: string; // isoCode aka what maps to the file on disk and what transloco loads renderName: string; translationCompletion: number; isRtL: boolean; diff --git a/UI/Web/src/app/_models/metadata/person.ts b/UI/Web/src/app/_models/metadata/person.ts index efc8df914..c8a4c566e 100644 --- a/UI/Web/src/app/_models/metadata/person.ts +++ b/UI/Web/src/app/_models/metadata/person.ts @@ -2,6 +2,7 @@ import {IHasCover} from "../common/i-has-cover"; export enum PersonRole { Other = 1, + Artist = 2, Writer = 3, Penciller = 4, Inker = 5, @@ -21,7 +22,6 @@ export interface Person extends IHasCover { id: number; name: string; description: string; - aliases: Array; coverImage?: string; coverImageLocked: boolean; malId?: number; @@ -31,22 +31,3 @@ export interface Person extends IHasCover { primaryColor: string; secondaryColor: string; } - -/** - * Excludes Other as it's not in use - */ -export const allPeopleRoles = [ - PersonRole.Writer, - PersonRole.Penciller, - PersonRole.Inker, - PersonRole.Colorist, - PersonRole.Letterer, - PersonRole.CoverArtist, - PersonRole.Editor, - PersonRole.Publisher, - PersonRole.Character, - PersonRole.Translator, - PersonRole.Imprint, - PersonRole.Team, - PersonRole.Location -] diff --git a/UI/Web/src/app/_models/metadata/series-filter.ts b/UI/Web/src/app/_models/metadata/series-filter.ts index 7875732b7..7d043aa3c 100644 --- a/UI/Web/src/app/_models/metadata/series-filter.ts +++ b/UI/Web/src/app/_models/metadata/series-filter.ts @@ -1,5 +1,5 @@ import {MangaFormat} from "../manga-format"; -import {FilterV2} from "./v2/filter-v2"; +import {SeriesFilterV2} from "./v2/series-filter-v2"; export interface FilterItem { title: string; @@ -7,6 +7,10 @@ export interface FilterItem { selected: boolean; } +export interface SortOptions { + sortField: SortField; + isAscending: boolean; +} export enum SortField { SortName = 1, @@ -23,7 +27,7 @@ export enum SortField { Random = 9 } -export const allSeriesSortFields = Object.keys(SortField) +export const allSortFields = Object.keys(SortField) .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) .map(key => parseInt(key, 10)) as SortField[]; @@ -50,8 +54,8 @@ export const mangaFormatFilters = [ } ]; -export interface FilterEvent { - filterV2: FilterV2; +export interface FilterEvent { + filterV2: SeriesFilterV2; isFirst: boolean; } diff --git a/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts b/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts deleted file mode 100644 index bb5edc9ce..000000000 --- a/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {PersonRole} from "../person"; -import {PersonSortOptions} from "./sort-options"; - -export interface BrowsePersonFilter { - roles: Array; - query?: string; - sortOptions?: PersonSortOptions; -} diff --git a/UI/Web/src/app/_models/metadata/v2/filter-field.ts b/UI/Web/src/app/_models/metadata/v2/filter-field.ts index eeb8c7853..08005d5c8 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-field.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-field.ts @@ -48,7 +48,7 @@ const enumArray = Object.keys(FilterField) enumArray.sort((a, b) => a.value.localeCompare(b.value)); -export const allSeriesFilterFields = enumArray +export const allFields = enumArray .map(key => parseInt(key.key, 10))as FilterField[]; export const allPeople = [ @@ -66,6 +66,7 @@ export const allPeople = [ export const personRoleForFilterField = (role: PersonRole) => { switch (role) { + case PersonRole.Artist: return FilterField.CoverArtist; case PersonRole.Character: return FilterField.Characters; case PersonRole.Colorist: return FilterField.Colorist; case PersonRole.CoverArtist: return FilterField.CoverArtist; diff --git a/UI/Web/src/app/_models/metadata/v2/filter-statement.ts b/UI/Web/src/app/_models/metadata/v2/filter-statement.ts index b14fe564d..d031927a2 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-statement.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-statement.ts @@ -1,7 +1,8 @@ -import {FilterComparison} from "./filter-comparison"; +import { FilterComparison } from "./filter-comparison"; +import { FilterField } from "./filter-field"; -export interface FilterStatement { +export interface FilterStatement { comparison: FilterComparison; - field: T; + field: FilterField; value: string; -} +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/metadata/v2/filter-v2.ts b/UI/Web/src/app/_models/metadata/v2/filter-v2.ts deleted file mode 100644 index 77c064450..000000000 --- a/UI/Web/src/app/_models/metadata/v2/filter-v2.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {FilterStatement} from "./filter-statement"; -import {FilterCombination} from "./filter-combination"; -import {SortOptions} from "./sort-options"; - -export interface FilterV2 { - name?: string; - statements: Array>; - combination: FilterCombination; - sortOptions?: SortOptions; - limitTo: number; -} diff --git a/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts b/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts deleted file mode 100644 index 6bfb5a0c1..000000000 --- a/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum PersonFilterField { - Role = 1, - Name = 2, - SeriesCount = 3, - ChapterCount = 4, -} - - -export const allPersonFilterFields = Object.keys(PersonFilterField) - .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) - .map(key => parseInt(key, 10)) as PersonFilterField[]; - diff --git a/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts b/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts deleted file mode 100644 index 6bcb66925..000000000 --- a/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum PersonSortField { - Name = 1, - SeriesCount = 2, - ChapterCount = 3 -} - -export const allPersonSortFields = Object.keys(PersonSortField) - .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) - .map(key => parseInt(key, 10)) as PersonSortField[]; diff --git a/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts b/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts new file mode 100644 index 000000000..c13244644 --- /dev/null +++ b/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts @@ -0,0 +1,11 @@ +import { SortOptions } from "../series-filter"; +import {FilterStatement} from "./filter-statement"; +import {FilterCombination} from "./filter-combination"; + +export interface SeriesFilterV2 { + name?: string; + statements: Array; + combination: FilterCombination; + sortOptions?: SortOptions; + limitTo: number; +} diff --git a/UI/Web/src/app/_models/metadata/v2/sort-options.ts b/UI/Web/src/app/_models/metadata/v2/sort-options.ts deleted file mode 100644 index ed68d6b9d..000000000 --- a/UI/Web/src/app/_models/metadata/v2/sort-options.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {PersonSortField} from "./person-sort-field"; - -/** - * Series-based Sort options - */ -export interface SortOptions { - sortField: TSort; - isAscending: boolean; -} - -/** - * Person-based Sort Options - */ -export interface PersonSortOptions { - sortField: PersonSortField; - isAscending: boolean; -} diff --git a/UI/Web/src/app/_models/metadata/browse/browse-person.ts b/UI/Web/src/app/_models/person/browse-person.ts similarity index 52% rename from UI/Web/src/app/_models/metadata/browse/browse-person.ts rename to UI/Web/src/app/_models/person/browse-person.ts index 886f9455b..aeddac7cd 100644 --- a/UI/Web/src/app/_models/metadata/browse/browse-person.ts +++ b/UI/Web/src/app/_models/person/browse-person.ts @@ -1,6 +1,6 @@ -import {Person} from "../person"; +import {Person} from "../metadata/person"; export interface BrowsePerson extends Person { seriesCount: number; - chapterCount: number; + issueCount: number; } diff --git a/UI/Web/src/app/_models/preferences/book-theme.ts b/UI/Web/src/app/_models/preferences/book-theme.ts index cb321c110..b6e37f6e4 100644 --- a/UI/Web/src/app/_models/preferences/book-theme.ts +++ b/UI/Web/src/app/_models/preferences/book-theme.ts @@ -1,7 +1,7 @@ -import {ThemeProvider} from "./site-theme"; +import { ThemeProvider } from "./site-theme"; /** - * Theme for the book reader contents + * Theme for the the book reader contents */ export interface BookTheme { name: string; diff --git a/UI/Web/src/app/_models/preferences/preferences.ts b/UI/Web/src/app/_models/preferences/preferences.ts index 886c570e2..1dd5731e5 100644 --- a/UI/Web/src/app/_models/preferences/preferences.ts +++ b/UI/Web/src/app/_models/preferences/preferences.ts @@ -1,7 +1,47 @@ +import {LayoutMode} from 'src/app/manga-reader/_models/layout-mode'; +import {BookPageLayoutMode} from '../readers/book-page-layout-mode'; import {PageLayoutMode} from '../page-layout-mode'; +import {PageSplitOption} from './page-split-option'; +import {ReaderMode} from './reader-mode'; +import {ReadingDirection} from './reading-direction'; +import {ScalingOption} from './scaling-option'; import {SiteTheme} from './site-theme'; +import {WritingStyle} from "./writing-style"; +import {PdfTheme} from "./pdf-theme"; +import {PdfScrollMode} from "./pdf-scroll-mode"; +import {PdfLayoutMode} from "./pdf-layout-mode"; +import {PdfSpreadMode} from "./pdf-spread-mode"; export interface Preferences { + // Manga Reader + readingDirection: ReadingDirection; + scalingOption: ScalingOption; + pageSplitOption: PageSplitOption; + readerMode: ReaderMode; + autoCloseMenu: boolean; + layoutMode: LayoutMode; + backgroundColor: string; + showScreenHints: boolean; + emulateBook: boolean; + swipeToPaginate: boolean; + allowAutomaticWebtoonReaderDetection: boolean; + + // Book Reader + bookReaderMargin: number; + bookReaderLineSpacing: number; + bookReaderFontSize: number; + bookReaderFontFamily: string; + bookReaderTapToPaginate: boolean; + bookReaderReadingDirection: ReadingDirection; + bookReaderWritingStyle: WritingStyle; + bookReaderThemeName: string; + bookReaderLayoutMode: BookPageLayoutMode; + bookReaderImmersiveMode: boolean; + + // PDF Reader + pdfTheme: PdfTheme; + pdfScrollMode: PdfScrollMode; + pdfSpreadMode: PdfSpreadMode; // Global theme: SiteTheme; @@ -18,3 +58,15 @@ export interface Preferences { wantToReadSync: boolean; } +export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}]; +export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}]; +export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}]; +export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}]; +export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}]; +export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} +export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}]; +export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}]; +export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}]; +export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}]; +export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}]; +export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}]; diff --git a/UI/Web/src/app/_models/preferences/reading-profiles.ts b/UI/Web/src/app/_models/preferences/reading-profiles.ts deleted file mode 100644 index dad02946f..000000000 --- a/UI/Web/src/app/_models/preferences/reading-profiles.ts +++ /dev/null @@ -1,80 +0,0 @@ -import {LayoutMode} from 'src/app/manga-reader/_models/layout-mode'; -import {BookPageLayoutMode} from '../readers/book-page-layout-mode'; -import {PageLayoutMode} from '../page-layout-mode'; -import {PageSplitOption} from './page-split-option'; -import {ReaderMode} from './reader-mode'; -import {ReadingDirection} from './reading-direction'; -import {ScalingOption} from './scaling-option'; -import {WritingStyle} from "./writing-style"; -import {PdfTheme} from "./pdf-theme"; -import {PdfScrollMode} from "./pdf-scroll-mode"; -import {PdfLayoutMode} from "./pdf-layout-mode"; -import {PdfSpreadMode} from "./pdf-spread-mode"; -import {Series} from "../series"; -import {Library} from "../library/library"; -import {UserBreakpoint} from "../../shared/_services/utility.service"; - -export enum ReadingProfileKind { - Default = 0, - User = 1, - Implicit = 2, -} - -export interface ReadingProfile { - - id: number; - name: string; - normalizedName: string; - kind: ReadingProfileKind; - - // Manga Reader - readingDirection: ReadingDirection; - scalingOption: ScalingOption; - pageSplitOption: PageSplitOption; - readerMode: ReaderMode; - autoCloseMenu: boolean; - layoutMode: LayoutMode; - backgroundColor: string; - showScreenHints: boolean; - emulateBook: boolean; - swipeToPaginate: boolean; - allowAutomaticWebtoonReaderDetection: boolean; - widthOverride?: number; - disableWidthOverride: UserBreakpoint; - - // Book Reader - bookReaderMargin: number; - bookReaderLineSpacing: number; - bookReaderFontSize: number; - bookReaderFontFamily: string; - bookReaderTapToPaginate: boolean; - bookReaderReadingDirection: ReadingDirection; - bookReaderWritingStyle: WritingStyle; - bookReaderThemeName: string; - bookReaderLayoutMode: BookPageLayoutMode; - bookReaderImmersiveMode: boolean; - - // PDF Reader - pdfTheme: PdfTheme; - pdfScrollMode: PdfScrollMode; - pdfSpreadMode: PdfSpreadMode; - - // relations - seriesIds: number[]; - libraryIds: number[]; - -} - -export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}]; -export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}]; -export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}]; -export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}]; -export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}]; -export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover} -export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}]; -export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}]; -export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}]; -export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}]; -export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}]; -export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}]; -export const breakPoints = [UserBreakpoint.Never, UserBreakpoint.Mobile, UserBreakpoint.Tablet, UserBreakpoint.Desktop] diff --git a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts index 7db1ceeaa..48a75afda 100644 --- a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts +++ b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts @@ -7,7 +7,6 @@ export enum ScrobbleEventType { } export interface ScrobbleEvent { - id: number; seriesName: string; seriesId: number; libraryId: number; diff --git a/UI/Web/src/app/_models/wiki.ts b/UI/Web/src/app/_models/wiki.ts index a01267cf3..21b669f0c 100644 --- a/UI/Web/src/app/_models/wiki.ts +++ b/UI/Web/src/app/_models/wiki.ts @@ -20,6 +20,5 @@ export enum WikiLink { UpdateNative = 'https://wiki.kavitareader.com/guides/updating/updating-native', UpdateDocker = 'https://wiki.kavitareader.com/guides/updating/updating-docker', OpdsClients = 'https://wiki.kavitareader.com/guides/features/opds/#opds-capable-clients', - Guides = 'https://wiki.kavitareader.com/guides', - ReadingProfiles = "https://wiki.kavitareader.com/guides/user-settings/reading-profiles/", + Guides = 'https://wiki.kavitareader.com/guides' } diff --git a/UI/Web/src/app/_pipes/breakpoint.pipe.ts b/UI/Web/src/app/_pipes/breakpoint.pipe.ts deleted file mode 100644 index 1897b773c..000000000 --- a/UI/Web/src/app/_pipes/breakpoint.pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {translate} from "@jsverse/transloco"; -import {UserBreakpoint} from "../shared/_services/utility.service"; - -@Pipe({ - name: 'breakpoint' -}) -export class BreakpointPipe implements PipeTransform { - - transform(value: UserBreakpoint): string { - const v = parseInt(value + '', 10) as UserBreakpoint; - switch (v) { - case UserBreakpoint.Never: - return translate('breakpoint-pipe.never'); - case UserBreakpoint.Mobile: - return translate('breakpoint-pipe.mobile'); - case UserBreakpoint.Tablet: - return translate('breakpoint-pipe.tablet'); - case UserBreakpoint.Desktop: - return translate('breakpoint-pipe.desktop'); - } - throw new Error("unknown breakpoint value: " + value); - } - -} diff --git a/UI/Web/src/app/_pipes/browse-title.pipe.ts b/UI/Web/src/app/_pipes/browse-title.pipe.ts deleted file mode 100644 index 0495e8b8a..000000000 --- a/UI/Web/src/app/_pipes/browse-title.pipe.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {FilterField} from "../_models/metadata/v2/filter-field"; -import {translate} from "@jsverse/transloco"; - -/** - * Responsible for taking a filter field and value (as a string) and translating into a "Browse X" heading for All Series page - * Example: Genre & "Action" -> Browse Action - * Example: Artist & "Joe Shmo" -> Browse Joe Shmo Works - */ -@Pipe({ - name: 'browseTitle' -}) -export class BrowseTitlePipe implements PipeTransform { - - transform(field: FilterField, value: string): string { - switch (field) { - case FilterField.PublicationStatus: - return translate('browse-title-pipe.publication-status', {value}); - case FilterField.AgeRating: - return translate('browse-title-pipe.age-rating', {value}); - case FilterField.UserRating: - return translate('browse-title-pipe.user-rating', {value}); - case FilterField.Tags: - return translate('browse-title-pipe.tag', {value}); - case FilterField.Translators: - return translate('browse-title-pipe.translator', {value}); - case FilterField.Characters: - return translate('browse-title-pipe.character', {value}); - case FilterField.Publisher: - return translate('browse-title-pipe.publisher', {value}); - case FilterField.Editor: - return translate('browse-title-pipe.editor', {value}); - case FilterField.CoverArtist: - return translate('browse-title-pipe.artist', {value}); - case FilterField.Letterer: - return translate('browse-title-pipe.letterer', {value}); - case FilterField.Colorist: - return translate('browse-title-pipe.colorist', {value}); - case FilterField.Inker: - return translate('browse-title-pipe.inker', {value}); - case FilterField.Penciller: - return translate('browse-title-pipe.penciller', {value}); - case FilterField.Writers: - return translate('browse-title-pipe.writer', {value}); - case FilterField.Genres: - return translate('browse-title-pipe.genre', {value}); - case FilterField.Libraries: - return translate('browse-title-pipe.library', {value}); - case FilterField.Formats: - return translate('browse-title-pipe.format', {value}); - case FilterField.ReleaseYear: - return translate('browse-title-pipe.release-year', {value}); - case FilterField.Imprint: - return translate('browse-title-pipe.imprint', {value}); - case FilterField.Team: - return translate('browse-title-pipe.team', {value}); - case FilterField.Location: - return translate('browse-title-pipe.location', {value}); - - // These have no natural links in the app to demand a richer title experience - case FilterField.Languages: - case FilterField.CollectionTags: - case FilterField.ReadProgress: - case FilterField.ReadTime: - case FilterField.Path: - case FilterField.FilePath: - case FilterField.WantToRead: - case FilterField.ReadingDate: - case FilterField.AverageRating: - case FilterField.ReadLast: - case FilterField.Summary: - case FilterField.SeriesName: - default: - return ''; - } - } - -} diff --git a/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts b/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts deleted file mode 100644 index f342c0034..000000000 --- a/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts +++ /dev/null @@ -1,108 +0,0 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {FilterField} from "../_models/metadata/v2/filter-field"; -import {translate} from "@jsverse/transloco"; -import {ValidFilterEntity} from "../metadata-filter/filter-settings"; -import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; - -@Pipe({ - name: 'genericFilterField' -}) -export class GenericFilterFieldPipe implements PipeTransform { - - transform(value: T, entityType: ValidFilterEntity): string { - - switch (entityType) { - case "series": - return this.translateFilterField(value as FilterField); - case "person": - return this.translatePersonFilterField(value as PersonFilterField); - } - } - - private translatePersonFilterField(value: PersonFilterField) { - switch (value) { - case PersonFilterField.Role: - return translate('generic-filter-field-pipe.person-role'); - case PersonFilterField.Name: - return translate('generic-filter-field-pipe.person-name'); - case PersonFilterField.SeriesCount: - return translate('generic-filter-field-pipe.person-series-count'); - case PersonFilterField.ChapterCount: - return translate('generic-filter-field-pipe.person-chapter-count'); - } - } - - private translateFilterField(value: FilterField) { - switch (value) { - case FilterField.AgeRating: - return translate('filter-field-pipe.age-rating'); - case FilterField.Characters: - return translate('filter-field-pipe.characters'); - case FilterField.CollectionTags: - return translate('filter-field-pipe.collection-tags'); - case FilterField.Colorist: - return translate('filter-field-pipe.colorist'); - case FilterField.CoverArtist: - return translate('filter-field-pipe.cover-artist'); - case FilterField.Editor: - return translate('filter-field-pipe.editor'); - case FilterField.Formats: - return translate('filter-field-pipe.formats'); - case FilterField.Genres: - return translate('filter-field-pipe.genres'); - case FilterField.Inker: - return translate('filter-field-pipe.inker'); - case FilterField.Imprint: - return translate('filter-field-pipe.imprint'); - case FilterField.Team: - return translate('filter-field-pipe.team'); - case FilterField.Location: - return translate('filter-field-pipe.location'); - case FilterField.Languages: - return translate('filter-field-pipe.languages'); - case FilterField.Libraries: - return translate('filter-field-pipe.libraries'); - case FilterField.Letterer: - return translate('filter-field-pipe.letterer'); - case FilterField.PublicationStatus: - return translate('filter-field-pipe.publication-status'); - case FilterField.Penciller: - return translate('filter-field-pipe.penciller'); - case FilterField.Publisher: - return translate('filter-field-pipe.publisher'); - case FilterField.ReadProgress: - return translate('filter-field-pipe.read-progress'); - case FilterField.ReadTime: - return translate('filter-field-pipe.read-time'); - case FilterField.ReleaseYear: - return translate('filter-field-pipe.release-year'); - case FilterField.SeriesName: - return translate('filter-field-pipe.series-name'); - case FilterField.Summary: - return translate('filter-field-pipe.summary'); - case FilterField.Tags: - return translate('filter-field-pipe.tags'); - case FilterField.Translators: - return translate('filter-field-pipe.translators'); - case FilterField.UserRating: - return translate('filter-field-pipe.user-rating'); - case FilterField.Writers: - return translate('filter-field-pipe.writers'); - case FilterField.Path: - return translate('filter-field-pipe.path'); - case FilterField.FilePath: - return translate('filter-field-pipe.file-path'); - case FilterField.WantToRead: - return translate('filter-field-pipe.want-to-read'); - case FilterField.ReadingDate: - return translate('filter-field-pipe.read-date'); - case FilterField.ReadLast: - return translate('filter-field-pipe.read-last'); - case FilterField.AverageRating: - return translate('filter-field-pipe.average-rating'); - default: - throw new Error(`Invalid FilterField value: ${value}`); - } - } - -} diff --git a/UI/Web/src/app/_pipes/library-type.pipe.ts b/UI/Web/src/app/_pipes/library-type.pipe.ts index 1881b64d5..784089e6e 100644 --- a/UI/Web/src/app/_pipes/library-type.pipe.ts +++ b/UI/Web/src/app/_pipes/library-type.pipe.ts @@ -26,6 +26,8 @@ export class LibraryTypePipe implements PipeTransform { return this.translocoService.translate('library-type-pipe.manga'); case LibraryType.LightNovel: return this.translocoService.translate('library-type-pipe.lightNovel'); + case LibraryType.Magazine: + return this.translocoService.translate('library-type-pipe.magazine'); default: return ''; } diff --git a/UI/Web/src/app/_pipes/person-role.pipe.ts b/UI/Web/src/app/_pipes/person-role.pipe.ts index 1b9ee2163..c1395ae5b 100644 --- a/UI/Web/src/app/_pipes/person-role.pipe.ts +++ b/UI/Web/src/app/_pipes/person-role.pipe.ts @@ -1,6 +1,6 @@ -import {Pipe, PipeTransform} from '@angular/core'; -import {PersonRole} from '../_models/metadata/person'; -import {translate} from "@jsverse/transloco"; +import {inject, Pipe, PipeTransform} from '@angular/core'; +import { PersonRole } from '../_models/metadata/person'; +import {translate, TranslocoService} from "@jsverse/transloco"; @Pipe({ name: 'personRole', @@ -10,6 +10,8 @@ export class PersonRolePipe implements PipeTransform { transform(value: PersonRole): string { switch (value) { + case PersonRole.Artist: + return translate('person-role-pipe.artist'); case PersonRole.Character: return translate('person-role-pipe.character'); case PersonRole.Colorist: diff --git a/UI/Web/src/app/_pipes/sort-field.pipe.ts b/UI/Web/src/app/_pipes/sort-field.pipe.ts index d032de9c8..13ff4f758 100644 --- a/UI/Web/src/app/_pipes/sort-field.pipe.ts +++ b/UI/Web/src/app/_pipes/sort-field.pipe.ts @@ -1,8 +1,6 @@ -import {Pipe, PipeTransform} from '@angular/core'; +import { Pipe, PipeTransform } from '@angular/core'; import {SortField} from "../_models/metadata/series-filter"; import {TranslocoService} from "@jsverse/transloco"; -import {ValidFilterEntity} from "../metadata-filter/filter-settings"; -import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; @Pipe({ name: 'sortField', @@ -13,30 +11,7 @@ export class SortFieldPipe implements PipeTransform { constructor(private translocoService: TranslocoService) { } - transform(value: T, entityType: ValidFilterEntity): string { - - switch (entityType) { - case 'series': - return this.seriesSortFields(value as SortField); - case 'person': - return this.personSortFields(value as PersonSortField); - - } - } - - private personSortFields(value: PersonSortField) { - switch (value) { - case PersonSortField.Name: - return this.translocoService.translate('sort-field-pipe.person-name'); - case PersonSortField.SeriesCount: - return this.translocoService.translate('sort-field-pipe.person-series-count'); - case PersonSortField.ChapterCount: - return this.translocoService.translate('sort-field-pipe.person-chapter-count'); - - } - } - - private seriesSortFields(value: SortField) { + transform(value: SortField): string { switch (value) { case SortField.SortName: return this.translocoService.translate('sort-field-pipe.sort-name'); @@ -57,6 +32,7 @@ export class SortFieldPipe implements PipeTransform { case SortField.Random: return this.translocoService.translate('sort-field-pipe.random'); } + } } diff --git a/UI/Web/src/app/_resolvers/reading-profile.resolver.ts b/UI/Web/src/app/_resolvers/reading-profile.resolver.ts deleted file mode 100644 index 1d28adf95..000000000 --- a/UI/Web/src/app/_resolvers/reading-profile.resolver.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Injectable} from '@angular/core'; -import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; -import {Observable} from 'rxjs'; -import {ReadingProfileService} from "../_services/reading-profile.service"; - -@Injectable({ - providedIn: 'root' -}) -export class ReadingProfileResolver implements Resolve { - - constructor(private readingProfileService: ReadingProfileService) {} - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - // Extract seriesId from route params or parent route - const seriesId = route.params['seriesId'] || route.parent?.params['seriesId']; - return this.readingProfileService.getForSeries(seriesId); - } -} diff --git a/UI/Web/src/app/_resolvers/url-filter.resolver.ts b/UI/Web/src/app/_resolvers/url-filter.resolver.ts deleted file mode 100644 index 16bc5c752..000000000 --- a/UI/Web/src/app/_resolvers/url-filter.resolver.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Injectable} from "@angular/core"; -import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router"; -import {Observable, of} from "rxjs"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; -import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; - -/** - * Checks the url for a filter and resolves one if applicable, otherwise returns null. - * It is up to the consumer to cast appropriately. - */ -@Injectable({ - providedIn: 'root' -}) -export class UrlFilterResolver implements Resolve { - - constructor(private filterUtilitiesService: FilterUtilitiesService) {} - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - if (!state.url.includes('?')) return of(null); - return this.filterUtilitiesService.decodeFilter(state.url.split('?')[1]); - } -} diff --git a/UI/Web/src/app/_routes/all-series-routing.module.ts b/UI/Web/src/app/_routes/all-series-routing.module.ts index 5c4804251..d9dfaaf96 100644 --- a/UI/Web/src/app/_routes/all-series-routing.module.ts +++ b/UI/Web/src/app/_routes/all-series-routing.module.ts @@ -1,13 +1,7 @@ -import {Routes} from "@angular/router"; -import {AllSeriesComponent} from "../all-series/_components/all-series/all-series.component"; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; +import { Routes } from "@angular/router"; +import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component"; export const routes: Routes = [ - {path: '', component: AllSeriesComponent, pathMatch: 'full', - runGuardsAndResolvers: 'always', - resolve: { - filter: UrlFilterResolver - } - }, + {path: '', component: AllSeriesComponent, pathMatch: 'full'}, ]; diff --git a/UI/Web/src/app/_routes/book-reader.router.module.ts b/UI/Web/src/app/_routes/book-reader.router.module.ts index c9d6262ad..5083c2d4a 100644 --- a/UI/Web/src/app/_routes/book-reader.router.module.ts +++ b/UI/Web/src/app/_routes/book-reader.router.module.ts @@ -1,14 +1,10 @@ -import {Routes} from '@angular/router'; -import {BookReaderComponent} from '../book-reader/_components/book-reader/book-reader.component'; -import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver"; +import { Routes } from '@angular/router'; +import { BookReaderComponent } from '../book-reader/_components/book-reader/book-reader.component'; export const routes: Routes = [ { path: ':chapterId', component: BookReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } } ]; diff --git a/UI/Web/src/app/_routes/bookmark-routing.module.ts b/UI/Web/src/app/_routes/bookmark-routing.module.ts index 2c7c52036..6da971e08 100644 --- a/UI/Web/src/app/_routes/bookmark-routing.module.ts +++ b/UI/Web/src/app/_routes/bookmark-routing.module.ts @@ -1,12 +1,6 @@ -import {Routes} from "@angular/router"; -import {BookmarksComponent} from "../bookmark/_components/bookmarks/bookmarks.component"; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; +import { Routes } from "@angular/router"; +import { BookmarksComponent } from "../bookmark/_components/bookmarks/bookmarks.component"; export const routes: Routes = [ - {path: '', component: BookmarksComponent, pathMatch: 'full', - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, + {path: '', component: BookmarksComponent, pathMatch: 'full'}, ]; diff --git a/UI/Web/src/app/_routes/browse-authors-routing.module.ts b/UI/Web/src/app/_routes/browse-authors-routing.module.ts new file mode 100644 index 000000000..e7aab1b57 --- /dev/null +++ b/UI/Web/src/app/_routes/browse-authors-routing.module.ts @@ -0,0 +1,8 @@ +import { Routes } from "@angular/router"; +import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component"; +import {BrowseAuthorsComponent} from "../browse-people/browse-authors.component"; + + +export const routes: Routes = [ + {path: '', component: BrowseAuthorsComponent, pathMatch: 'full'}, +]; diff --git a/UI/Web/src/app/_routes/browse-routing.module.ts b/UI/Web/src/app/_routes/browse-routing.module.ts deleted file mode 100644 index be96e8193..000000000 --- a/UI/Web/src/app/_routes/browse-routing.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {Routes} from "@angular/router"; -import {BrowsePeopleComponent} from "../browse/browse-people/browse-people.component"; -import {BrowseGenresComponent} from "../browse/browse-genres/browse-genres.component"; -import {BrowseTagsComponent} from "../browse/browse-tags/browse-tags.component"; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; - - -export const routes: Routes = [ - // Legacy route - {path: 'authors', component: BrowsePeopleComponent, pathMatch: 'full', - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, - {path: 'people', component: BrowsePeopleComponent, pathMatch: 'full', - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, - {path: 'genres', component: BrowseGenresComponent, pathMatch: 'full'}, - {path: 'tags', component: BrowseTagsComponent, pathMatch: 'full'}, -]; diff --git a/UI/Web/src/app/_routes/collections-routing.module.ts b/UI/Web/src/app/_routes/collections-routing.module.ts index 2b3b0ffd7..80510c8f6 100644 --- a/UI/Web/src/app/_routes/collections-routing.module.ts +++ b/UI/Web/src/app/_routes/collections-routing.module.ts @@ -1,15 +1,9 @@ -import {Routes} from '@angular/router'; -import {AllCollectionsComponent} from '../collections/_components/all-collections/all-collections.component'; -import {CollectionDetailComponent} from '../collections/_components/collection-detail/collection-detail.component'; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; +import { Routes } from '@angular/router'; +import { AllCollectionsComponent } from '../collections/_components/all-collections/all-collections.component'; +import { CollectionDetailComponent } from '../collections/_components/collection-detail/collection-detail.component'; export const routes: Routes = [ {path: '', component: AllCollectionsComponent, pathMatch: 'full'}, - {path: ':id', component: CollectionDetailComponent, - resolve: { - filter: UrlFilterResolver - }, - runGuardsAndResolvers: 'always', - }, + {path: ':id', component: CollectionDetailComponent}, ]; diff --git a/UI/Web/src/app/_routes/library-detail-routing.module.ts b/UI/Web/src/app/_routes/library-detail-routing.module.ts index 3c09a71ee..04cb3c9dd 100644 --- a/UI/Web/src/app/_routes/library-detail-routing.module.ts +++ b/UI/Web/src/app/_routes/library-detail-routing.module.ts @@ -1,8 +1,7 @@ -import {Routes} from '@angular/router'; -import {AuthGuard} from '../_guards/auth.guard'; -import {LibraryAccessGuard} from '../_guards/library-access.guard'; -import {LibraryDetailComponent} from '../library-detail/library-detail.component'; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; +import { Routes } from '@angular/router'; +import { AuthGuard } from '../_guards/auth.guard'; +import { LibraryAccessGuard } from '../_guards/library-access.guard'; +import { LibraryDetailComponent } from '../library-detail/library-detail.component'; export const routes: Routes = [ @@ -10,18 +9,12 @@ export const routes: Routes = [ path: ':libraryId', runGuardsAndResolvers: 'always', canActivate: [AuthGuard, LibraryAccessGuard], - component: LibraryDetailComponent, - resolve: { - filter: UrlFilterResolver - }, + component: LibraryDetailComponent }, { path: '', runGuardsAndResolvers: 'always', canActivate: [AuthGuard, LibraryAccessGuard], - component: LibraryDetailComponent, - resolve: { - filter: UrlFilterResolver - }, - }, + component: LibraryDetailComponent + } ]; diff --git a/UI/Web/src/app/_routes/manga-reader.router.module.ts b/UI/Web/src/app/_routes/manga-reader.router.module.ts index e479e8ae6..04ff77b3c 100644 --- a/UI/Web/src/app/_routes/manga-reader.router.module.ts +++ b/UI/Web/src/app/_routes/manga-reader.router.module.ts @@ -1,22 +1,15 @@ -import {Routes} from '@angular/router'; -import {MangaReaderComponent} from '../manga-reader/_components/manga-reader/manga-reader.component'; -import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver"; +import { Routes } from '@angular/router'; +import { MangaReaderComponent } from '../manga-reader/_components/manga-reader/manga-reader.component'; export const routes: Routes = [ { path: ':chapterId', - component: MangaReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } + component: MangaReaderComponent }, { // This will allow the MangaReader to have a list to use for next/prev chapters rather than natural sort order path: ':chapterId/list/:listId', - component: MangaReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } + component: MangaReaderComponent } ]; diff --git a/UI/Web/src/app/_routes/pdf-reader.router.module.ts b/UI/Web/src/app/_routes/pdf-reader.router.module.ts index 7cb9f68e2..a55699280 100644 --- a/UI/Web/src/app/_routes/pdf-reader.router.module.ts +++ b/UI/Web/src/app/_routes/pdf-reader.router.module.ts @@ -1,13 +1,9 @@ -import {Routes} from '@angular/router'; -import {PdfReaderComponent} from '../pdf-reader/_components/pdf-reader/pdf-reader.component'; -import {ReadingProfileResolver} from "../_resolvers/reading-profile.resolver"; +import { Routes } from '@angular/router'; +import { PdfReaderComponent } from '../pdf-reader/_components/pdf-reader/pdf-reader.component'; export const routes: Routes = [ { path: ':chapterId', component: PdfReaderComponent, - resolve: { - readingProfile: ReadingProfileResolver - } } ]; diff --git a/UI/Web/src/app/_routes/want-to-read-routing.module.ts b/UI/Web/src/app/_routes/want-to-read-routing.module.ts index b593172c0..b3301d9f9 100644 --- a/UI/Web/src/app/_routes/want-to-read-routing.module.ts +++ b/UI/Web/src/app/_routes/want-to-read-routing.module.ts @@ -1,10 +1,6 @@ -import {Routes} from '@angular/router'; -import {WantToReadComponent} from '../want-to-read/_components/want-to-read/want-to-read.component'; -import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; +import { Routes } from '@angular/router'; +import { WantToReadComponent } from '../want-to-read/_components/want-to-read/want-to-read.component'; export const routes: Routes = [ - {path: '', component: WantToReadComponent, pathMatch: 'full', runGuardsAndResolvers: 'always', resolve: { - filter: UrlFilterResolver - } - }, + {path: '', component: WantToReadComponent, pathMatch: 'full'}, ]; diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index f1f91143f..6b8cdc243 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -102,22 +102,11 @@ export class AccountService { return true; } - /** - * If the user has any role in the restricted roles array or is an Admin - * @param user - * @param roles - * @param restrictedRoles - */ hasAnyRole(user: User, roles: Array, restrictedRoles: Array = []) { if (!user || !user.roles) { return false; } - // If the user is an admin, they have the role - if (this.hasAdminRole(user)) { - return true; - } - // If restricted roles are provided and the user has any of them, deny access if (restrictedRoles.length > 0 && restrictedRoles.some(role => user.roles.includes(role))) { return false; @@ -132,33 +121,6 @@ export class AccountService { return roles.some(role => user.roles.includes(role)); } - /** - * If User or Admin, will return false - * @param user - * @param restrictedRoles - */ - hasAnyRestrictedRole(user: User, restrictedRoles: Array = []) { - if (!user || !user.roles) { - return true; - } - - if (restrictedRoles.length === 0) { - return false; - } - - // If the user is an admin, they have the role - if (this.hasAdminRole(user)) { - return false; - } - - - if (restrictedRoles.length > 0 && restrictedRoles.some(role => user.roles.includes(role))) { - return true; - } - - return false; - } - hasAdminRole(user: User) { return user && user.roles.includes(Role.Admin); } diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index e5967bf24..6d2f7053e 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -7,13 +7,12 @@ import {Library} from '../_models/library/library'; import {ReadingList} from '../_models/reading-list'; import {Series} from '../_models/series'; import {Volume} from '../_models/volume'; -import {AccountService, Role} from './account.service'; +import {AccountService} from './account.service'; import {DeviceService} from './device.service'; import {SideNavStream} from "../_models/sidenav/sidenav-stream"; import {SmartFilter} from "../_models/metadata/v2/smart-filter"; import {translate} from "@jsverse/transloco"; import {Person} from "../_models/metadata/person"; -import {User} from '../_models/user'; export enum Action { Submenu = -1, @@ -107,7 +106,7 @@ export enum Action { Promote = 24, UnPromote = 25, /** - * Invoke refresh covers as false to generate colorscapes + * Invoke a refresh covers as false to generate colorscapes */ GenerateColorScape = 26, /** @@ -117,39 +116,20 @@ export enum Action { /** * Match an entity with an upstream system */ - Match = 28, - /** - * Merge two (or more?) entities - */ - Merge = 29, - /** - * Add to a reading profile - */ - SetReadingProfile = 30, - /** - * Remove the reading profile from the entity - */ - ClearReadingProfile = 31, + Match = 28 } /** * Callback for an action */ -export type ActionCallback = (action: ActionItem, entity: T) => void; -export type ActionShouldRenderFunc = (action: ActionItem, entity: T, user: User) => boolean; +export type ActionCallback = (action: ActionItem, data: T) => void; +export type ActionAllowedCallback = (action: ActionItem) => boolean; export interface ActionItem { title: string; description: string; action: Action; callback: ActionCallback; - /** - * Roles required to be present for ActionItem to show. If empty, assumes anyone can see. At least one needs to apply. - */ - requiredRoles: Role[]; - /** - * @deprecated Use required Roles instead - */ requiresAdmin: boolean; children: Array>; /** @@ -165,98 +145,94 @@ export interface ActionItem { * Extra data that needs to be sent back from the card item. Used mainly for dynamicList. This will be the item from dyanamicList return */ _extra?: {title: string, data: any}; - /** - * Will call on each action to determine if it should show for the appropriate entity based on state and user - */ - shouldRender: ActionShouldRenderFunc; } -/** - * Entities that can be actioned upon - */ -export type ActionableEntity = Volume | Series | Chapter | ReadingList | UserCollection | Person | Library | SideNavStream | SmartFilter | null; - @Injectable({ providedIn: 'root', }) export class ActionFactoryService { - private libraryActions: Array> = []; - private seriesActions: Array> = []; - private volumeActions: Array> = []; - private chapterActions: Array> = []; - private collectionTagActions: Array> = []; - private readingListActions: Array> = []; - private bookmarkActions: Array> = []; + libraryActions: Array> = []; + + seriesActions: Array> = []; + + volumeActions: Array> = []; + + chapterActions: Array> = []; + + collectionTagActions: Array> = []; + + readingListActions: Array> = []; + + bookmarkActions: Array> = []; + private personActions: Array> = []; - private sideNavStreamActions: Array> = []; - private smartFilterActions: Array> = []; - private sideNavHomeActions: Array> = []; + + sideNavStreamActions: Array> = []; + smartFilterActions: Array> = []; + + sideNavHomeActions: Array> = []; + + isAdmin = false; + constructor(private accountService: AccountService, private deviceService: DeviceService) { - this.accountService.currentUser$.subscribe((_) => { + this.accountService.currentUser$.subscribe((user) => { + if (user) { + this.isAdmin = this.accountService.hasAdminRole(user); + } else { + this._resetActions(); + return; // If user is logged out, we don't need to do anything + } + this._resetActions(); }); } - getLibraryActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.libraryActions, callback, shouldRenderFunc) as ActionItem[]; + getLibraryActions(callback: ActionCallback) { + return this.applyCallbackToList(this.libraryActions, callback); } - getSeriesActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { - return this.applyCallbackToList(this.seriesActions, callback, shouldRenderFunc); + getSeriesActions(callback: ActionCallback) { + return this.applyCallbackToList(this.seriesActions, callback); } - getSideNavStreamActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.sideNavStreamActions, callback, shouldRenderFunc); + getSideNavStreamActions(callback: ActionCallback) { + return this.applyCallbackToList(this.sideNavStreamActions, callback); } - getSmartFilterActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.smartFilterActions, callback, shouldRenderFunc); + getSmartFilterActions(callback: ActionCallback) { + return this.applyCallbackToList(this.smartFilterActions, callback); } - getVolumeActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { - return this.applyCallbackToList(this.volumeActions, callback, shouldRenderFunc); + getVolumeActions(callback: ActionCallback) { + return this.applyCallbackToList(this.volumeActions, callback); } - getChapterActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { - return this.applyCallbackToList(this.chapterActions, callback, shouldRenderFunc); + getChapterActions(callback: ActionCallback) { + return this.applyCallbackToList(this.chapterActions, callback); } - getCollectionTagActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.collectionTagActions, callback, shouldRenderFunc); + getCollectionTagActions(callback: ActionCallback) { + return this.applyCallbackToList(this.collectionTagActions, callback); } - getReadingListActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.readingListActions, callback, shouldRenderFunc); + getReadingListActions(callback: ActionCallback) { + return this.applyCallbackToList(this.readingListActions, callback); } - getBookmarkActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.bookmarkActions, callback, shouldRenderFunc); + getBookmarkActions(callback: ActionCallback) { + return this.applyCallbackToList(this.bookmarkActions, callback); } - getPersonActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.personActions, callback, shouldRenderFunc); + getPersonActions(callback: ActionCallback) { + return this.applyCallbackToList(this.personActions, callback); } - getSideNavHomeActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { - return this.applyCallbackToList(this.sideNavHomeActions, callback, shouldRenderFunc); + getSideNavHomeActions(callback: ActionCallback) { + return this.applyCallbackToList(this.sideNavHomeActions, callback); } - dummyCallback(action: ActionItem, entity: any) {} - dummyShouldRender(action: ActionItem, entity: any, user: User) {return true;} - basicReadRender(action: ActionItem, entity: any, user: User) { - if (entity === null || entity === undefined) return true; - if (!entity.hasOwnProperty('pagesRead') && !entity.hasOwnProperty('pages')) return true; - - switch (action.action) { - case(Action.MarkAsRead): - return entity.pagesRead < entity.pages; - case(Action.MarkAsUnread): - return entity.pagesRead !== 0; - default: - return true; - } - } + dummyCallback(action: ActionItem, data: any) {} filterSendToAction(actions: Array>, chapter: Chapter) { // if (chapter.files.filter(f => f.format === MangaFormat.EPUB || f.format === MangaFormat.PDF).length !== chapter.files.length) { @@ -299,7 +275,7 @@ export class ActionFactoryService { return tasks.filter(t => !blacklist.includes(t.action)); } - getBulkLibraryActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + getBulkLibraryActions(callback: ActionCallback) { // Scan is currently not supported due to the backend not being able to handle it yet const actions = this.flattenActions(this.libraryActions).filter(a => { @@ -313,13 +289,11 @@ export class ActionFactoryService { dynamicList: undefined, action: Action.CopySettings, callback: this.dummyCallback, - shouldRender: shouldRenderFunc, children: [], - requiredRoles: [Role.Admin], requiresAdmin: true, title: 'copy-settings' }) - return this.applyCallbackToList(actions, callback, shouldRenderFunc) as ActionItem[]; + return this.applyCallbackToList(actions, callback); } flattenActions(actions: Array>): Array> { @@ -345,59 +319,22 @@ export class ActionFactoryService { title: 'scan-library', description: 'scan-library-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, - { - action: Action.Submenu, - title: 'reading-profiles', - description: '', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.SetReadingProfile, - title: 'set-reading-profile', - description: 'set-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - { - action: Action.ClearReadingProfile, - title: 'clear-reading-profile', - description: 'clear-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - ], - }, { action: Action.Submenu, title: 'others', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [ { action: Action.RefreshMetadata, title: 'refresh-covers', description: 'refresh-covers-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -405,9 +342,7 @@ export class ActionFactoryService { title: 'generate-colorscape', description: 'generate-colorscape-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -415,9 +350,7 @@ export class ActionFactoryService { title: 'analyze-files', description: 'analyze-files-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -425,9 +358,7 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, ], @@ -437,9 +368,7 @@ export class ActionFactoryService { title: 'settings', description: 'settings-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, ]; @@ -450,9 +379,7 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -460,9 +387,7 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], class: 'danger', children: [], }, @@ -471,9 +396,7 @@ export class ActionFactoryService { title: 'promote', description: 'promote-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -481,9 +404,7 @@ export class ActionFactoryService { title: 'unpromote', description: 'unpromote-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -494,9 +415,7 @@ export class ActionFactoryService { title: 'mark-as-read', description: 'mark-as-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -504,9 +423,7 @@ export class ActionFactoryService { title: 'mark-as-unread', description: 'mark-as-unread-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -514,9 +431,7 @@ export class ActionFactoryService { title: 'scan-series', description: 'scan-series-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -524,18 +439,14 @@ export class ActionFactoryService { title: 'add-to', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.AddToWantToReadList, title: 'add-to-want-to-read', description: 'add-to-want-to-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -543,9 +454,7 @@ export class ActionFactoryService { title: 'remove-from-want-to-read', description: 'remove-to-want-to-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -553,9 +462,7 @@ export class ActionFactoryService { title: 'add-to-reading-list', description: 'add-to-reading-list-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -563,11 +470,26 @@ export class ActionFactoryService { title: 'add-to-collection', description: 'add-to-collection-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], - } + }, + + // { + // action: Action.AddToScrobbleHold, + // title: 'add-to-scrobble-hold', + // description: 'add-to-scrobble-hold-tooltip', + // callback: this.dummyCallback, + // requiresAdmin: true, + // children: [], + // }, + // { + // action: Action.RemoveFromScrobbleHold, + // title: 'remove-from-scrobble-hold', + // description: 'remove-from-scrobble-hold-tooltip', + // callback: this.dummyCallback, + // requiresAdmin: true, + // children: [], + // }, ], }, { @@ -575,18 +497,14 @@ export class ActionFactoryService { title: 'send-to', description: 'send-to-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.SendTo, title: '', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -594,54 +512,19 @@ export class ActionFactoryService { } ], }, - { - action: Action.Submenu, - title: 'reading-profiles', - description: '', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [ - { - action: Action.SetReadingProfile, - title: 'set-reading-profile', - description: 'set-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - { - action: Action.ClearReadingProfile, - title: 'clear-reading-profile', - description: 'clear-reading-profile-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: false, - requiredRoles: [], - children: [], - }, - ], - }, { action: Action.Submenu, title: 'others', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [], children: [ { action: Action.RefreshMetadata, title: 'refresh-covers', description: 'refresh-covers-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -649,9 +532,7 @@ export class ActionFactoryService { title: 'generate-colorscape', description: 'generate-colorscape-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -659,9 +540,7 @@ export class ActionFactoryService { title: 'analyze-files', description: 'analyze-files-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -669,9 +548,7 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], class: 'danger', children: [], }, @@ -682,9 +559,7 @@ export class ActionFactoryService { title: 'match', description: 'match-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -692,9 +567,7 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [Role.Download], children: [], }, { @@ -702,9 +575,7 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, ]; @@ -715,9 +586,7 @@ export class ActionFactoryService { title: 'read-incognito', description: 'read-incognito-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -725,9 +594,7 @@ export class ActionFactoryService { title: 'mark-as-read', description: 'mark-as-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -735,9 +602,7 @@ export class ActionFactoryService { title: 'mark-as-unread', description: 'mark-as-unread-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -745,18 +610,14 @@ export class ActionFactoryService { title: 'add-to', description: '=', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.AddToReadingList, title: 'add-to-reading-list', description: 'add-to-reading-list-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], } ] @@ -766,18 +627,14 @@ export class ActionFactoryService { title: 'send-to', description: 'send-to-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.SendTo, title: '', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -790,18 +647,14 @@ export class ActionFactoryService { title: 'others', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.Delete, title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -809,9 +662,7 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ] @@ -821,9 +672,7 @@ export class ActionFactoryService { title: 'details', description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -834,9 +683,7 @@ export class ActionFactoryService { title: 'read-incognito', description: 'read-incognito-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -844,9 +691,7 @@ export class ActionFactoryService { title: 'mark-as-read', description: 'mark-as-read-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -854,9 +699,7 @@ export class ActionFactoryService { title: 'mark-as-unread', description: 'mark-as-unread-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -864,18 +707,14 @@ export class ActionFactoryService { title: 'add-to', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.AddToReadingList, title: 'add-to-reading-list', description: 'add-to-reading-list-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], } ] @@ -885,18 +724,14 @@ export class ActionFactoryService { title: 'send-to', description: 'send-to-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.SendTo, title: '', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -910,18 +745,14 @@ export class ActionFactoryService { title: 'others', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [ { action: Action.Delete, title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], }, { @@ -929,9 +760,7 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [Role.Download], children: [], }, ] @@ -941,9 +770,7 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -954,9 +781,7 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -964,9 +789,7 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], class: 'danger', children: [], }, @@ -975,9 +798,7 @@ export class ActionFactoryService { title: 'promote', description: 'promote-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -985,9 +806,7 @@ export class ActionFactoryService { title: 'unpromote', description: 'unpromote-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -998,19 +817,7 @@ export class ActionFactoryService { title: 'edit', description: 'edit-person-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: true, - requiredRoles: [Role.Admin], - children: [], - }, - { - action: Action.Merge, - title: 'merge', - description: 'merge-person-tooltip', - callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, - requiresAdmin: true, - requiredRoles: [Role.Admin], children: [], } ]; @@ -1021,9 +828,7 @@ export class ActionFactoryService { title: 'view-series', description: 'view-series-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -1031,9 +836,7 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -1041,10 +844,8 @@ export class ActionFactoryService { title: 'clear', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, class: 'danger', requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -1055,9 +856,7 @@ export class ActionFactoryService { title: 'mark-visible', description: 'mark-visible-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -1065,9 +864,7 @@ export class ActionFactoryService { title: 'mark-invisible', description: 'mark-invisible-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -1078,9 +875,7 @@ export class ActionFactoryService { title: 'rename', description: 'rename-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, { @@ -1088,9 +883,7 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], }, ]; @@ -1101,9 +894,7 @@ export class ActionFactoryService { title: 'reorder', description: '', callback: this.dummyCallback, - shouldRender: this.dummyShouldRender, requiresAdmin: false, - requiredRoles: [], children: [], } ] @@ -1111,30 +902,21 @@ export class ActionFactoryService { } - private applyCallback(action: ActionItem, callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc) { + private applyCallback(action: ActionItem, callback: (action: ActionItem, data: any) => void) { action.callback = callback; - action.shouldRender = shouldRenderFunc; if (action.children === null || action.children?.length === 0) return; - // Ensure action children are a copy of the parent (since parent does a shallow mapping) - action.children = action.children.map(d => { return {...d}; }); - - action.children.forEach((childAction) => { - this.applyCallback(childAction, callback, shouldRenderFunc); + action.children?.forEach((childAction) => { + this.applyCallback(childAction, callback); }); } - public applyCallbackToList(list: Array>, - callback: ActionCallback, - shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender): Array> { - // Create a clone of the list to ensure we aren't affecting the default state + public applyCallbackToList(list: Array>, callback: (action: ActionItem, data: any) => void): Array> { const actions = list.map((a) => { return { ...a }; }); - - actions.forEach((action) => this.applyCallback(action, callback, shouldRenderFunc)); - + actions.forEach((action) => this.applyCallback(action, callback)); return actions; } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 2328bf72e..fd24bd9ff 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -31,9 +31,6 @@ import {ChapterService} from "./chapter.service"; import {VolumeService} from "./volume.service"; import {DefaultModalOptions} from "../_models/default-modal-options"; import {MatchSeriesModalComponent} from "../_single-module/match-series-modal/match-series-modal.component"; -import { - BulkSetReadingProfileModalComponent -} from "../cards/_modals/bulk-set-reading-profile-modal/bulk-set-reading-profile-modal.component"; export type LibraryActionCallback = (library: Partial) => void; @@ -476,7 +473,8 @@ export class ActionService { } async deleteMultipleVolumes(volumes: Array, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-volumes', {count: volumes.length}))) return; + // TODO: Change translation key back to "toasts.confirm-delete-multiple-volumes" + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: volumes.length}))) return; this.volumeService.deleteMultipleVolumes(volumes.map(v => v.id)).subscribe((success) => { if (callback) { @@ -816,56 +814,4 @@ export class ActionService { }); } - /** - * Sets the reading profile for multiple series - * @param series - * @param callback - */ - setReadingProfileForMultiple(series: Array, callback?: BooleanActionCallback) { - if (this.readingListModalRef != null) { return; } - - this.readingListModalRef = this.modalService.open(BulkSetReadingProfileModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); - this.readingListModalRef.componentInstance.seriesIds = series.map(s => s.id) - this.readingListModalRef.componentInstance.title = "" - - this.readingListModalRef.closed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(true); - } - }); - this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(false); - } - }); - } - - /** - * Sets the reading profile for multiple series - * @param library - * @param callback - */ - setReadingProfileForLibrary(library: Library, callback?: BooleanActionCallback) { - if (this.readingListModalRef != null) { return; } - - this.readingListModalRef = this.modalService.open(BulkSetReadingProfileModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' }); - this.readingListModalRef.componentInstance.libraryId = library.id; - this.readingListModalRef.componentInstance.title = "" - - this.readingListModalRef.closed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(true); - } - }); - this.readingListModalRef.dismissed.pipe(take(1)).subscribe(() => { - this.readingListModalRef = null; - if (callback) { - callback(false); - } - }); - } - } diff --git a/UI/Web/src/app/_services/filter.service.ts b/UI/Web/src/app/_services/filter.service.ts index 2b9681e90..e76c1926f 100644 --- a/UI/Web/src/app/_services/filter.service.ts +++ b/UI/Web/src/app/_services/filter.service.ts @@ -1,7 +1,8 @@ -import {Injectable} from '@angular/core'; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import { Injectable } from '@angular/core'; +import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {environment} from "../../environments/environment"; -import {HttpClient} from "@angular/common/http"; +import { HttpClient } from "@angular/common/http"; +import {JumpKey} from "../_models/jumpbar/jump-key"; import {SmartFilter} from "../_models/metadata/v2/smart-filter"; @Injectable({ @@ -12,7 +13,7 @@ export class FilterService { baseUrl = environment.apiUrl; constructor(private httpClient: HttpClient) { } - saveFilter(filter: FilterV2) { + saveFilter(filter: SeriesFilterV2) { return this.httpClient.post(this.baseUrl + 'filter/update', filter); } getAllFilters() { @@ -25,4 +26,5 @@ export class FilterService { renameSmartFilter(filter: SmartFilter) { return this.httpClient.post(this.baseUrl + `filter/rename?filterId=${filter.id}&name=${filter.name.trim()}`, {}); } + } diff --git a/UI/Web/src/app/_services/jumpbar.service.ts b/UI/Web/src/app/_services/jumpbar.service.ts index 48ca08705..d9919ff57 100644 --- a/UI/Web/src/app/_services/jumpbar.service.ts +++ b/UI/Web/src/app/_services/jumpbar.service.ts @@ -1,5 +1,5 @@ -import {Injectable} from '@angular/core'; -import {JumpKey} from '../_models/jumpbar/jump-key'; +import { Injectable } from '@angular/core'; +import { JumpKey } from '../_models/jumpbar/jump-key'; const keySize = 25; // Height of the JumpBar button @@ -105,18 +105,14 @@ export class JumpbarService { getJumpKeys(data :Array, keySelector: (data: any) => string) { const keys: {[key: string]: number} = {}; data.forEach(obj => { - try { - let ch = keySelector(obj).charAt(0).toUpperCase(); - if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { - ch = '#'; - } - if (!keys.hasOwnProperty(ch)) { - keys[ch] = 0; - } - keys[ch] += 1; - } catch (e) { - console.error('Failed to calculate jump key for ', obj, e); + let ch = keySelector(obj).charAt(0).toUpperCase(); + if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { + ch = '#'; } + if (!keys.hasOwnProperty(ch)) { + keys[ch] = 0; + } + keys[ch] += 1; }); return Object.keys(keys).map(k => { k = k.toUpperCase(); diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 8c851dd80..9178cd137 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -136,6 +136,20 @@ export class LibraryService { return this.httpClient.post(this.baseUrl + 'library/update', model); } + getLibraryTypes() { + if (this.libraryTypes) return of(this.libraryTypes); + return this.httpClient.get>(this.baseUrl + 'library/types').pipe(map(types => { + if (this.libraryTypes === undefined) { + this.libraryTypes = {}; + } + types.forEach(t => { + this.libraryTypes![t.libraryId] = t.libraryType; + }); + + return this.libraryTypes; + })); + } + getLibraryType(libraryId: number) { if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(libraryId)) { return of(this.libraryTypes[libraryId]); diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index f870d1449..ea1819bd7 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -1,16 +1,15 @@ -import {Injectable} from '@angular/core'; -import {HubConnection, HubConnectionBuilder} from '@microsoft/signalr'; -import {BehaviorSubject, ReplaySubject} from 'rxjs'; -import {environment} from 'src/environments/environment'; -import {LibraryModifiedEvent} from '../_models/events/library-modified-event'; -import {NotificationProgressEvent} from '../_models/events/notification-progress-event'; -import {ThemeProgressEvent} from '../_models/events/theme-progress-event'; -import {UserUpdateEvent} from '../_models/events/user-update-event'; -import {User} from '../_models/user'; +import { Injectable } from '@angular/core'; +import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; +import { BehaviorSubject, ReplaySubject } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { LibraryModifiedEvent } from '../_models/events/library-modified-event'; +import { NotificationProgressEvent } from '../_models/events/notification-progress-event'; +import { ThemeProgressEvent } from '../_models/events/theme-progress-event'; +import { UserUpdateEvent } from '../_models/events/user-update-event'; +import { User } from '../_models/user'; import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event"; import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event"; import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"; -import {ExternalMatchRateLimitErrorEvent} from "../_models/events/external-match-rate-limit-error-event"; export enum EVENTS { UpdateAvailable = 'UpdateAvailable', @@ -110,15 +109,7 @@ export enum EVENTS { /** * A Progress event when a smart collection is synchronizing */ - SmartCollectionSync = 'SmartCollectionSync', - /** - * A Person merged has been merged into another - */ - PersonMerged = 'PersonMerged', - /** - * A Rate limit error was hit when matching a series with Kavita+ - */ - ExternalMatchRateLimitError = 'ExternalMatchRateLimitError' + SmartCollectionSync = 'SmartCollectionSync' } export interface Message { @@ -241,13 +232,6 @@ export class MessageHubService { }); }); - this.hubConnection.on(EVENTS.ExternalMatchRateLimitError, resp => { - this.messagesSource.next({ - event: EVENTS.ExternalMatchRateLimitError, - payload: resp.body as ExternalMatchRateLimitErrorEvent - }); - }); - this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => { this.messagesSource.next({ event: EVENTS.NotificationProgress, @@ -352,13 +336,6 @@ export class MessageHubService { payload: resp.body }); }); - - this.hubConnection.on(EVENTS.PersonMerged, resp => { - this.messagesSource.next({ - event: EVENTS.PersonMerged, - payload: resp.body - }); - }) } stopHubConnection() { diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index fe0702219..314e5c37b 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -1,54 +1,33 @@ -import {HttpClient, HttpParams} from '@angular/common/http'; -import {inject, Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import {tap} from 'rxjs/operators'; -import {map, of} from 'rxjs'; +import {of} from 'rxjs'; import {environment} from 'src/environments/environment'; import {Genre} from '../_models/metadata/genre'; import {AgeRatingDto} from '../_models/metadata/age-rating-dto'; import {Language} from '../_models/metadata/language'; import {PublicationStatusDto} from '../_models/metadata/publication-status-dto'; -import {allPeopleRoles, Person, PersonRole} from '../_models/metadata/person'; +import {Person, PersonRole} from '../_models/metadata/person'; import {Tag} from '../_models/tag'; import {FilterComparison} from '../_models/metadata/v2/filter-comparison'; import {FilterField} from '../_models/metadata/v2/filter-field'; -import {mangaFormatFilters, SortField} from "../_models/metadata/series-filter"; +import {SortField} from "../_models/metadata/series-filter"; import {FilterCombination} from "../_models/metadata/v2/filter-combination"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {FilterStatement} from "../_models/metadata/v2/filter-statement"; import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus"; import {LibraryType} from "../_models/library/library"; import {IHasCast} from "../_models/common/i-has-cast"; import {TextResonse} from "../_types/text-response"; import {QueryContext} from "../_models/metadata/v2/query-context"; -import {AgeRatingPipe} from "../_pipes/age-rating.pipe"; -import {MangaFormatPipe} from "../_pipes/manga-format.pipe"; -import {TranslocoService} from "@jsverse/transloco"; -import {LibraryService} from './library.service'; -import {CollectionTagService} from "./collection-tag.service"; -import {PaginatedResult} from "../_models/pagination"; -import {UtilityService} from "../shared/_services/utility.service"; -import {BrowseGenre} from "../_models/metadata/browse/browse-genre"; -import {BrowseTag} from "../_models/metadata/browse/browse-tag"; -import {ValidFilterEntity} from "../metadata-filter/filter-settings"; -import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; -import {PersonRolePipe} from "../_pipes/person-role.pipe"; -import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; @Injectable({ providedIn: 'root' }) export class MetadataService { - private readonly translocoService = inject(TranslocoService); - private readonly libraryService = inject(LibraryService); - private readonly collectionTagService = inject(CollectionTagService); - private readonly utilityService = inject(UtilityService); - baseUrl = environment.apiUrl; private validLanguages: Array = []; - private ageRatingPipe = new AgeRatingPipe(); - private mangaFormatPipe = new MangaFormatPipe(this.translocoService); - private personRolePipe = new PersonRolePipe(); constructor(private httpClient: HttpClient) { } @@ -95,28 +74,6 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + method); } - getGenreWithCounts(pageNum?: number, itemsPerPage?: number) { - let params = new HttpParams(); - params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - - return this.httpClient.post>(this.baseUrl + 'metadata/genres-with-counts', {}, {observe: 'response', params}).pipe( - map((response: any) => { - return this.utilityService.createPaginatedResult(response) as PaginatedResult; - }) - ); - } - - getTagWithCounts(pageNum?: number, itemsPerPage?: number) { - let params = new HttpParams(); - params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - - return this.httpClient.post>(this.baseUrl + 'metadata/tags-with-counts', {}, {observe: 'response', params}).pipe( - map((response: any) => { - return this.utilityService.createPaginatedResult(response) as PaginatedResult; - }) - ); - } - getAllLanguages(libraries?: Array) { let method = 'metadata/languages' if (libraries != undefined && libraries.length > 0) { @@ -153,28 +110,19 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + 'metadata/people-by-role?role=' + role); } - createDefaultFilterDto(entityType: ValidFilterEntity): FilterV2 { + createDefaultFilterDto(): SeriesFilterV2 { return { - statements: [] as FilterStatement[], + statements: [] as FilterStatement[], combination: FilterCombination.And, limitTo: 0, sortOptions: { isAscending: true, - sortField: (entityType === 'series' ? SortField.SortName : PersonSortField.Name) as TSort + sortField: SortField.SortName } }; } - createDefaultFilterStatement(entityType: ValidFilterEntity) { - switch (entityType) { - case 'series': - return this.createFilterStatement(FilterField.SeriesName); - case 'person': - return this.createFilterStatement(PersonFilterField.Role, FilterComparison.Contains, `${PersonRole.CoverArtist},${PersonRole.Writer}`); - } - } - - createFilterStatement(field: T, comparison = FilterComparison.Equal, value = '') { + createDefaultFilterStatement(field: FilterField = FilterField.SeriesName, comparison = FilterComparison.Equal, value = '') { return { comparison: comparison, field: field, @@ -182,7 +130,7 @@ export class MetadataService { }; } - updateFilter(arr: Array>, index: number, filterStmt: FilterStatement) { + updateFilter(arr: Array, index: number, filterStmt: FilterStatement) { arr[index].comparison = filterStmt.comparison; arr[index].field = filterStmt.field; arr[index].value = filterStmt.value ? filterStmt.value + '' : ''; @@ -192,6 +140,8 @@ export class MetadataService { switch (role) { case PersonRole.Other: break; + case PersonRole.Artist: + break; case PersonRole.CoverArtist: entity.coverArtists = persons; break; @@ -233,85 +183,4 @@ export class MetadataService { break; } } - - /** - * Used to get the underlying Options (for Metadata Filter Dropdowns) - * @param filterField - * @param entityType - */ - getOptionsForFilterField(filterField: T, entityType: ValidFilterEntity) { - - switch (entityType) { - case 'series': - return this.getSeriesOptionsForFilterField(filterField as FilterField); - case 'person': - return this.getPersonOptionsForFilterField(filterField as PersonFilterField); - } - } - - private getPersonOptionsForFilterField(field: PersonFilterField) { - switch (field) { - case PersonFilterField.Role: - return of(allPeopleRoles.map(r => {return {value: r, label: this.personRolePipe.transform(r)}})); - } - return of([]) - } - - private getSeriesOptionsForFilterField(field: FilterField) { - switch (field) { - case FilterField.PublicationStatus: - return this.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => { - return {value: pub.value, label: pub.title} - }))); - case FilterField.AgeRating: - return this.getAllAgeRatings().pipe(map(ratings => ratings.map(rating => { - return {value: rating.value, label: this.ageRatingPipe.transform(rating.value)} - }))); - case FilterField.Genres: - return this.getAllGenres().pipe(map(genres => genres.map(genre => { - return {value: genre.id, label: genre.title} - }))); - case FilterField.Languages: - return this.getAllLanguages().pipe(map(statuses => statuses.map(status => { - return {value: status.isoCode, label: status.title + ` (${status.isoCode})`} - }))); - case FilterField.Formats: - return of(mangaFormatFilters).pipe(map(statuses => statuses.map(status => { - return {value: status.value, label: this.mangaFormatPipe.transform(status.value)} - }))); - case FilterField.Libraries: - return this.libraryService.getLibraries().pipe(map(libs => libs.map(lib => { - return {value: lib.id, label: lib.name} - }))); - case FilterField.Tags: - return this.getAllTags().pipe(map(statuses => statuses.map(status => { - return {value: status.id, label: status.title} - }))); - case FilterField.CollectionTags: - return this.collectionTagService.allCollections().pipe(map(statuses => statuses.map(status => { - return {value: status.id, label: status.title} - }))); - case FilterField.Characters: return this.getPersonOptions(PersonRole.Character); - case FilterField.Colorist: return this.getPersonOptions(PersonRole.Colorist); - case FilterField.CoverArtist: return this.getPersonOptions(PersonRole.CoverArtist); - case FilterField.Editor: return this.getPersonOptions(PersonRole.Editor); - case FilterField.Inker: return this.getPersonOptions(PersonRole.Inker); - case FilterField.Letterer: return this.getPersonOptions(PersonRole.Letterer); - case FilterField.Penciller: return this.getPersonOptions(PersonRole.Penciller); - case FilterField.Publisher: return this.getPersonOptions(PersonRole.Publisher); - case FilterField.Imprint: return this.getPersonOptions(PersonRole.Imprint); - case FilterField.Team: return this.getPersonOptions(PersonRole.Team); - case FilterField.Location: return this.getPersonOptions(PersonRole.Location); - case FilterField.Translators: return this.getPersonOptions(PersonRole.Translator); - case FilterField.Writers: return this.getPersonOptions(PersonRole.Writer); - } - - return of([]); - } - - private getPersonOptions(role: PersonRole) { - return this.getAllPeopleByRole(role).pipe(map(people => people.map(person => { - return {value: person.id, label: person.name} - }))); - } } diff --git a/UI/Web/src/app/_services/nav.service.ts b/UI/Web/src/app/_services/nav.service.ts index 0aad76ef7..65d9fca17 100644 --- a/UI/Web/src/app/_services/nav.service.ts +++ b/UI/Web/src/app/_services/nav.service.ts @@ -9,24 +9,6 @@ import {AccountService} from "./account.service"; import {map} from "rxjs/operators"; import {NavigationEnd, Router} from "@angular/router"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {SettingsTabId} from "../sidenav/preference-nav/preference-nav.component"; -import {WikiLink} from "../_models/wiki"; - -/** - * NavItem used to construct the dropdown or NavLinkModal on mobile - * Priority construction - * @param routerLink A link to a page on the web app, takes priority - * @param fragment Optional fragment for routerLink - * @param href A link to an external page, must set noopener noreferrer - * @param click Callback, lowest priority. Should only be used if routerLink and href or not set - */ -interface NavItem { - transLocoKey: string; - href?: string; - fragment?: string; - routerLink?: string; - click?: () => void; -} @Injectable({ providedIn: 'root' @@ -39,33 +21,6 @@ export class NavService { public localStorageSideNavKey = 'kavita--sidenav--expanded'; - public navItems: NavItem[] = [ - { - transLocoKey: 'all-filters', - routerLink: '/all-filters/', - }, - { - transLocoKey: 'browse-genres', - routerLink: '/browse/genres', - }, - { - transLocoKey: 'browse-tags', - routerLink: '/browse/tags', - }, - { - transLocoKey: 'announcements', - routerLink: '/announcements/', - }, - { - transLocoKey: 'help', - href: WikiLink.Guides, - }, - { - transLocoKey: 'logout', - click: () => this.logout(), - } - ] - private navbarVisibleSource = new ReplaySubject(1); /** * If the top Nav bar is rendered or not @@ -172,13 +127,6 @@ export class NavService { }, 10); } - logout() { - this.accountService.logout(); - this.hideNavBar(); - this.hideSideNav(); - this.router.navigateByUrl('/login'); - } - /** * Shows the side nav. When being visible, the side nav will automatically return to previous collapsed state. */ diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts index fc9148135..676aa6e71 100644 --- a/UI/Web/src/app/_services/person.service.ts +++ b/UI/Web/src/app/_services/person.service.ts @@ -1,17 +1,16 @@ -import {Injectable} from '@angular/core'; -import {HttpClient, HttpParams} from "@angular/common/http"; +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from "@angular/common/http"; import {environment} from "../../environments/environment"; import {Person, PersonRole} from "../_models/metadata/person"; +import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {PaginatedResult} from "../_models/pagination"; import {Series} from "../_models/series"; import {map} from "rxjs/operators"; import {UtilityService} from "../shared/_services/utility.service"; -import {BrowsePerson} from "../_models/metadata/browse/browse-person"; +import {BrowsePerson} from "../_models/person/browse-person"; +import {Chapter} from "../_models/chapter"; import {StandaloneChapter} from "../_models/standalone-chapter"; import {TextResonse} from "../_types/text-response"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; -import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; -import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; @Injectable({ providedIn: 'root' @@ -30,10 +29,6 @@ export class PersonService { return this.httpClient.get(this.baseUrl + `person?name=${name}`); } - searchPerson(name: string) { - return this.httpClient.get>(this.baseUrl + `person/search?queryString=${encodeURIComponent(name)}`); - } - getRolesForPerson(personId: number) { return this.httpClient.get>(this.baseUrl + `person/roles?personId=${personId}`); } @@ -46,40 +41,18 @@ export class PersonService { return this.httpClient.get>(this.baseUrl + `person/chapters-by-role?personId=${personId}&role=${role}`); } - getAuthorsToBrowse(filter: FilterV2, pageNum?: number, itemsPerPage?: number) { + getAuthorsToBrowse(pageNum?: number, itemsPerPage?: number) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - return this.httpClient.post>(this.baseUrl + `person/all`, filter, {observe: 'response', params}).pipe( + return this.httpClient.post>(this.baseUrl + 'person/all', {}, {observe: 'response', params}).pipe( map((response: any) => { return this.utilityService.createPaginatedResult(response) as PaginatedResult; }) ); } - // getAuthorsToBrowse(filter: BrowsePersonFilter, pageNum?: number, itemsPerPage?: number) { - // let params = new HttpParams(); - // params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - // - // return this.httpClient.post>(this.baseUrl + `person/all`, filter, {observe: 'response', params}).pipe( - // map((response: any) => { - // return this.utilityService.createPaginatedResult(response) as PaginatedResult; - // }) - // ); - // } - downloadCover(personId: number) { return this.httpClient.post(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse); } - - isValidAlias(personId: number, alias: string) { - return this.httpClient.get(this.baseUrl + `person/valid-alias?personId=${personId}&alias=${alias}`, TextResonse).pipe( - map(valid => valid + '' === 'true') - ); - } - - mergePerson(destId: number, srcId: number) { - return this.httpClient.post(this.baseUrl + 'person/merge', {destId, srcId}); - } - } diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 52aef2a4a..9941cd005 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -16,14 +16,13 @@ import {TextResonse} from '../_types/text-response'; import {AccountService} from './account.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {PersonalToC} from "../_models/readers/personal-toc"; -import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import NoSleep from 'nosleep.js'; import {FullProgress} from "../_models/readers/full-progress"; import {Volume} from "../_models/volume"; import {UtilityService} from "../shared/_services/utility.service"; import {translate} from "@jsverse/transloco"; import {ToastrService} from "ngx-toastr"; -import {FilterField} from "../_models/metadata/v2/filter-field"; export const CHAPTER_ID_DOESNT_EXIST = -1; @@ -108,7 +107,7 @@ export class ReaderService { return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page}); } - getAllBookmarks(filter: FilterV2 | undefined) { + getAllBookmarks(filter: SeriesFilterV2 | undefined) { return this.httpClient.post(this.baseUrl + 'reader/all-bookmarks', filter); } @@ -266,13 +265,13 @@ export class ReaderService { getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) { - const params: {[key: string]: any} = {}; - params['incognitoMode'] = incognitoMode; - + let params: {[key: string]: any} = {}; + if (incognitoMode) { + params['incognitoMode'] = true; + } if (readingListMode) { params['readingListId'] = readingListId; } - return params; } diff --git a/UI/Web/src/app/_services/reading-profile.service.ts b/UI/Web/src/app/_services/reading-profile.service.ts deleted file mode 100644 index e8be8b6ab..000000000 --- a/UI/Web/src/app/_services/reading-profile.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {inject, Injectable} from '@angular/core'; -import {HttpClient} from "@angular/common/http"; -import {environment} from "../../environments/environment"; -import {ReadingProfile} from "../_models/preferences/reading-profiles"; - -@Injectable({ - providedIn: 'root' -}) -export class ReadingProfileService { - - private readonly httpClient = inject(HttpClient); - baseUrl = environment.apiUrl; - - getForSeries(seriesId: number, skipImplicit: boolean = false) { - return this.httpClient.get(this.baseUrl + `reading-profile/${seriesId}?skipImplicit=${skipImplicit}`); - } - - getForLibrary(libraryId: number) { - return this.httpClient.get(this.baseUrl + `reading-profile/library?libraryId=${libraryId}`); - } - - updateProfile(profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + 'reading-profile', profile); - } - - updateParentProfile(seriesId: number, profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + `reading-profile/update-parent?seriesId=${seriesId}`, profile); - } - - createProfile(profile: ReadingProfile) { - return this.httpClient.post(this.baseUrl + 'reading-profile/create', profile); - } - - promoteProfile(profileId: number) { - return this.httpClient.post(this.baseUrl + "reading-profile/promote?profileId=" + profileId, {}); - } - - updateImplicit(profile: ReadingProfile, seriesId: number) { - return this.httpClient.post(this.baseUrl + "reading-profile/series?seriesId="+seriesId, profile); - } - - getAllProfiles() { - return this.httpClient.get(this.baseUrl + 'reading-profile/all'); - } - - delete(id: number) { - return this.httpClient.delete(this.baseUrl + `reading-profile?profileId=${id}`); - } - - addToSeries(id: number, seriesId: number) { - return this.httpClient.post(this.baseUrl + `reading-profile/series/${seriesId}?profileId=${id}`, {}); - } - - clearSeriesProfiles(seriesId: number) { - return this.httpClient.delete(this.baseUrl + `reading-profile/series/${seriesId}`, {}); - } - - addToLibrary(id: number, libraryId: number) { - return this.httpClient.post(this.baseUrl + `reading-profile/library/${libraryId}?profileId=${id}`, {}); - } - - clearLibraryProfiles(libraryId: number) { - return this.httpClient.delete(this.baseUrl + `reading-profile/library/${libraryId}`, {}); - } - - bulkAddToSeries(id: number, seriesIds: number[]) { - return this.httpClient.post(this.baseUrl + `reading-profile/bulk?profileId=${id}`, seriesIds); - } - -} diff --git a/UI/Web/src/app/_services/scrobbling.service.ts b/UI/Web/src/app/_services/scrobbling.service.ts index cfc7b34ac..76b9212f4 100644 --- a/UI/Web/src/app/_services/scrobbling.service.ts +++ b/UI/Web/src/app/_services/scrobbling.service.ts @@ -104,10 +104,6 @@ export class ScrobblingService { triggerScrobbleEventGeneration() { return this.httpClient.post(this.baseUrl + 'scrobbling/generate-scrobble-events', TextResonse); - } - bulkRemoveEvents(eventIds: number[]) { - return this.httpClient.post(this.baseUrl + "scrobbling/bulk-remove-events", eventIds) } - } diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 9c436e636..b440b1eb7 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -1,26 +1,28 @@ -import {HttpClient, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; -import {environment} from 'src/environments/environment'; -import {UtilityService} from '../shared/_services/utility.service'; -import {Chapter} from '../_models/chapter'; -import {PaginatedResult} from '../_models/pagination'; -import {Series} from '../_models/series'; -import {RelatedSeries} from '../_models/series-detail/related-series'; -import {SeriesDetail} from '../_models/series-detail/series-detail'; -import {SeriesGroup} from '../_models/series-group'; -import {SeriesMetadata} from '../_models/metadata/series-metadata'; -import {Volume} from '../_models/volume'; -import {TextResonse} from '../_types/text-response'; -import {FilterV2} from '../_models/metadata/v2/filter-v2'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { UtilityService } from '../shared/_services/utility.service'; +import { Chapter } from '../_models/chapter'; +import { PaginatedResult } from '../_models/pagination'; +import { Series } from '../_models/series'; +import { RelatedSeries } from '../_models/series-detail/related-series'; +import { SeriesDetail } from '../_models/series-detail/series-detail'; +import { SeriesGroup } from '../_models/series-group'; +import { SeriesMetadata } from '../_models/metadata/series-metadata'; +import { Volume } from '../_models/volume'; +import { ImageService } from './image.service'; +import { TextResonse } from '../_types/text-response'; +import { SeriesFilterV2 } from '../_models/metadata/v2/series-filter-v2'; +import {UserReview} from "../_single-module/review-card/user-review"; import {Rating} from "../_models/rating"; import {Recommendation} from "../_models/series-detail/recommendation"; import {ExternalSeriesDetail} from "../_models/series-detail/external-series-detail"; import {NextExpectedChapter} from "../_models/series-detail/next-expected-chapter"; import {QueryContext} from "../_models/metadata/v2/query-context"; +import {ExternalSeries} from "../_models/series-detail/external-series"; import {ExternalSeriesMatch} from "../_models/series-detail/external-series-match"; -import {FilterField} from "../_models/metadata/v2/filter-field"; @Injectable({ providedIn: 'root' @@ -31,9 +33,10 @@ export class SeriesService { paginatedResults: PaginatedResult = new PaginatedResult(); paginatedSeriesForTagsResults: PaginatedResult = new PaginatedResult(); - constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } + constructor(private httpClient: HttpClient, private imageService: ImageService, + private utilityService: UtilityService) { } - getAllSeriesV2(pageNum?: number, itemsPerPage?: number, filter?: FilterV2, context: QueryContext = QueryContext.None) { + getAllSeriesV2(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2, context: QueryContext = QueryContext.None) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -45,7 +48,7 @@ export class SeriesService { ); } - getSeriesForLibraryV2(pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { + getSeriesForLibraryV2(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -81,6 +84,10 @@ export class SeriesService { return this.httpClient.post(this.baseUrl + 'series/delete-multiple', {seriesIds}, TextResonse).pipe(map(s => s === "true")); } + updateRating(seriesId: number, userRating: number) { + return this.httpClient.post(this.baseUrl + 'series/update-rating', {seriesId, userRating}); + } + updateSeries(model: any) { return this.httpClient.post(this.baseUrl + 'series/update', model); } @@ -93,7 +100,7 @@ export class SeriesService { return this.httpClient.post(this.baseUrl + 'reader/mark-unread', {seriesId}); } - getRecentlyAdded(pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { + getRecentlyAdded(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); @@ -109,7 +116,7 @@ export class SeriesService { return this.httpClient.post(this.baseUrl + 'series/recently-updated-series', {}); } - getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: FilterV2): Observable> { + getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2): Observable> { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -127,7 +134,7 @@ export class SeriesService { })); } - getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { + getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -223,4 +230,5 @@ export class SeriesService { updateDontMatch(seriesId: number, dontMatch: boolean) { return this.httpClient.post(this.baseUrl + `series/dont-match?seriesId=${seriesId}&dontMatch=${dontMatch}`, {}, TextResonse); } + } diff --git a/UI/Web/src/app/_services/statistics.service.ts b/UI/Web/src/app/_services/statistics.service.ts index cf80765f2..f13b29c87 100644 --- a/UI/Web/src/app/_services/statistics.service.ts +++ b/UI/Web/src/app/_services/statistics.service.ts @@ -1,19 +1,20 @@ -import {HttpClient, HttpParams} from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import {Inject, inject, Injectable} from '@angular/core'; -import {environment} from 'src/environments/environment'; -import {UserReadStatistics} from '../statistics/_models/user-read-statistics'; -import {PublicationStatusPipe} from '../_pipes/publication-status.pipe'; -import {asyncScheduler, map} from 'rxjs'; -import {MangaFormatPipe} from '../_pipes/manga-format.pipe'; -import {FileExtensionBreakdown} from '../statistics/_models/file-breakdown'; -import {TopUserRead} from '../statistics/_models/top-reads'; -import {ReadHistoryEvent} from '../statistics/_models/read-history-event'; -import {ServerStatistics} from '../statistics/_models/server-statistics'; -import {StatCount} from '../statistics/_models/stat-count'; -import {PublicationStatus} from '../_models/metadata/publication-status'; -import {MangaFormat} from '../_models/manga-format'; -import {TextResonse} from '../_types/text-response'; +import { environment } from 'src/environments/environment'; +import { UserReadStatistics } from '../statistics/_models/user-read-statistics'; +import { PublicationStatusPipe } from '../_pipes/publication-status.pipe'; +import {asyncScheduler, finalize, map, tap} from 'rxjs'; +import { MangaFormatPipe } from '../_pipes/manga-format.pipe'; +import { FileExtensionBreakdown } from '../statistics/_models/file-breakdown'; +import { TopUserRead } from '../statistics/_models/top-reads'; +import { ReadHistoryEvent } from '../statistics/_models/read-history-event'; +import { ServerStatistics } from '../statistics/_models/server-statistics'; +import { StatCount } from '../statistics/_models/stat-count'; +import { PublicationStatus } from '../_models/metadata/publication-status'; +import { MangaFormat } from '../_models/manga-format'; +import { TextResonse } from '../_types/text-response'; import {TranslocoService} from "@jsverse/transloco"; +import {KavitaPlusMetadataBreakdown} from "../statistics/_models/kavitaplus-metadata-breakdown"; import {throttleTime} from "rxjs/operators"; import {DEBOUNCE_TIME} from "../shared/_services/download.service"; import {download} from "../shared/_models/download"; @@ -43,14 +44,11 @@ export class StatisticsService { constructor(private httpClient: HttpClient, @Inject(SAVER) private save: Saver) { } getUserStatistics(userId: number, libraryIds: Array = []) { - const url = `${this.baseUrl}stats/user/${userId}/read`; + // TODO: Convert to httpParams object + let url = 'stats/user/' + userId + '/read'; + if (libraryIds.length > 0) url += '?libraryIds=' + libraryIds.join(','); - let params = new HttpParams(); - if (libraryIds.length > 0) { - params = params.set('libraryIds', libraryIds.join(',')); - } - - return this.httpClient.get(url, { params }); + return this.httpClient.get(this.baseUrl + url); } getServerStatistics() { @@ -61,7 +59,7 @@ export class StatisticsService { return this.httpClient.get[]>(this.baseUrl + 'stats/server/count/year').pipe( map(spreads => spreads.map(spread => { return {name: spread.value + '', value: spread.count}; - }))); + }))); } getTopYears() { diff --git a/UI/Web/src/app/_services/toggle.service.ts b/UI/Web/src/app/_services/toggle.service.ts index 0ad9813e3..8b335394a 100644 --- a/UI/Web/src/app/_services/toggle.service.ts +++ b/UI/Web/src/app/_services/toggle.service.ts @@ -1,6 +1,6 @@ -import {Injectable} from '@angular/core'; -import {NavigationStart, Router} from '@angular/router'; -import {filter, ReplaySubject, take} from 'rxjs'; +import { Injectable } from '@angular/core'; +import { NavigationStart, Router } from '@angular/router'; +import { filter, ReplaySubject, take } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -29,7 +29,7 @@ export class ToggleService { this.toggleState = !state; this.toggleStateSource.next(this.toggleState); }); - + } set(state: boolean) { diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html index 7573c554a..067dc5fb2 100644 --- a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html +++ b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html @@ -1,9 +1,7 @@