diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index d6fd4eb9f..ec9c1884f 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 73b886e13..a571a6e72 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -6,13 +6,13 @@ - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/API.Tests/Parsers/ComicVineParserTests.cs b/API.Tests/Parsers/ComicVineParserTests.cs index f01e98afd..2f4fd568e 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, new ComicInfo() + RootDirectory, LibraryType.ComicVine, true, 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, null); + RootDirectory, LibraryType.ComicVine, true, 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, null); + RootDirectory, LibraryType.ComicVine, true, 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, null); + RootDirectory, LibraryType.ComicVine, true, 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 733b55d62..244c08b97 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, null); + var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, true, 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, null); + var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, 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, null); + var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, 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, null); + var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, true, 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, null); + var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, true, 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, null); + actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, true, 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, null); + actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, true, 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, null); + var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, 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, null); + actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, 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, null); + var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, true, null); if (expectedInfo == null) { Assert.Null(actual); diff --git a/API.Tests/Parsers/ImageParserTests.cs b/API.Tests/Parsers/ImageParserTests.cs index f95c98ddf..63df1926e 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, null); + RootDirectory, LibraryType.Image, true, 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, null); + RootDirectory, LibraryType.Image, true, 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, null); + RootDirectory, LibraryType.Image, true, 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 72088526d..08bf9f25d 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, null); + RootDirectory, LibraryType.Book, true, 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 3d78d9372..362b4b08c 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, null); + var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, true, 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, null); + actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, 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, null); + actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index 8b93c5f90..53f2bc4c9 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -68,10 +68,8 @@ 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/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index a80c1ca01..5848c74ba 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, comicInfo); + var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, true, 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 5c1752cd8..caf1ae393 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) + public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) { throw new System.NotImplementedException(); } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) + public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) { throw new System.NotImplementedException(); } diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 833e8fe5f..8278f3b1a 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -42,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 diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index f8714f69a..a732b2526 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) + public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { if (_comicVineParser.IsApplicable(path, type)) { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_imageParser.IsApplicable(path, type)) { - return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_bookParser.IsApplicable(path, type)) { - return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_pdfParser.IsApplicable(path, type)) { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_basicParser.IsApplicable(path, type)) { - return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } return null; } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) + public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { - return Parse(path, rootPath, libraryRoot, type); + return Parse(path, rootPath, libraryRoot, type, enableMetadata); } } diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 2e812647b..acc0345b1 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,4 +938,38 @@ 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"); + } } 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 new file mode 100644 index 000000000..d6e91183b --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json @@ -0,0 +1,5 @@ +[ + "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/API.csproj b/API/API.csproj index 4eed66f22..a7d1177dc 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -50,9 +50,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -62,25 +62,25 @@ - + - + - - - - - + + + + + - - - + + + @@ -89,16 +89,16 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 2f12aa1fe..c09011b77 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -623,6 +623,7 @@ public class LibraryController : BaseApiController library.ManageReadingLists = dto.ManageReadingLists; library.AllowScrobbling = dto.AllowScrobbling; library.AllowMetadataMatching = dto.AllowMetadataMatching; + library.EnableMetadata = dto.EnableMetadata; library.LibraryFileTypes = dto.FileGroupTypes .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) .Distinct() diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index 2b7dea8e6..c05ff0567 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 /// -internal sealed record ExternalMetadataIdsDto +public 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 fae674ded..a7359d69b 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.KavitaPlus.ExternalMetadata; /// /// Represents a request to match some series from Kavita to an external id which K+ uses. /// -internal sealed record MatchSeriesRequestDto +public sealed record MatchSeriesRequestDto { public required string SeriesName { get; set; } public ICollection AlternativeNames { get; set; } = []; diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs index d0cbb7bd3..84e9bbf3e 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; -internal sealed record SeriesDetailPlusApiDto +public sealed record SeriesDetailPlusApiDto { public IEnumerable Recommendations { get; set; } public IEnumerable Reviews { get; set; } diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs index 1dcd8494c..add9ca723 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs @@ -3,6 +3,7 @@ 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/LibraryDto.cs b/API/DTOs/LibraryDto.cs index 8ba687346..7b38379c9 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -66,4 +66,8 @@ 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; } diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index 9bd47fd39..68d2417ec 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -28,6 +28,8 @@ public sealed record UpdateLibraryDto public bool AllowScrobbling { get; init; } [Required] public bool AllowMetadataMatching { get; init; } + [Required] + public bool EnableMetadata { get; init; } /// /// What types of files to allow the scanner to pickup /// diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 3bbf45e23..aa8b67283 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -147,6 +147,9 @@ 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) diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs new file mode 100644 index 000000000..c15f9f77b --- /dev/null +++ b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs @@ -0,0 +1,3709 @@ +// +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 new file mode 100644 index 000000000..f9e38c01d --- /dev/null +++ b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs @@ -0,0 +1,29 @@ +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/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index c9fb953df..eb786865b 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -1296,6 +1296,11 @@ namespace API.Data.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); + b.Property("EnableMetadata") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + b.Property("FolderWatching") .HasColumnType("INTEGER"); diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index abab81378..8dc386298 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -48,6 +48,10 @@ 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; public DateTime Created { get; set; } diff --git a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs b/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs new file mode 100644 index 000000000..e69de29bb diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index 30e6136a5..950c5d3d2 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -110,6 +110,12 @@ 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/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 435727bda..1db334b91 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -67,6 +67,7 @@ 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]; @@ -82,7 +83,8 @@ 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) + ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService, + IKavitaPlusApiService kavitaPlusApiService) { _unitOfWork = unitOfWork; _logger = logger; @@ -91,6 +93,7 @@ public class ExternalMetadataService : IExternalMetadataService _scrobblingService = scrobblingService; _eventHub = eventHub; _coverDbService = coverDbService; + _kavitaPlusApiService = kavitaPlusApiService; FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } @@ -179,9 +182,7 @@ 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 ($"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={user.MalUserName}") - .WithKavitaPlusHeaders(license) - .GetJsonAsync>(); + var result = await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license); if (result == null) { @@ -207,7 +208,7 @@ public class ExternalMetadataService : IExternalMetadataService /// 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 []; @@ -239,14 +240,9 @@ public class ExternalMetadataService : IExternalMetadataService MalId = potentialMalId ?? ScrobblingService.GetMalId(series) }; - var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - try { - var results = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(matchRequest) - .ReceiveJson>(); + var results = await _kavitaPlusApiService.MatchSeries(matchRequest); // Some summaries can contain multiple
s, we need to ensure it's only 1 foreach (var result in results) @@ -287,9 +283,7 @@ 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 license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var details = await GetSeriesDetail(license, aniListId, malId, seriesId); + var details = await GetSeriesDetail(aniListId, malId, seriesId); return details; @@ -392,6 +386,9 @@ 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)); } } @@ -442,16 +439,12 @@ 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 { - result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(data) - .ReceiveJson(); // This returns an AniListSeries and Match returns ExternalSeriesDto + // This returns an AniListSeries and Match returns ExternalSeriesDto + result = await _kavitaPlusApiService.GetSeriesDetail(data); } catch (FlurlHttpException ex) { @@ -466,11 +459,7 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogDebug("Hit rate limit, will retry in 3 seconds"); await Task.Delay(3000); - result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(data) - .ReceiveJson< - SeriesDetailPlusApiDto>(); + result = await _kavitaPlusApiService.GetSeriesDetail(data); } else if (errorMessage.Contains("Unknown Series")) { @@ -1777,7 +1766,7 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// - private async Task GetSeriesDetail(string license, int? aniListId, long? malId, int? seriesId) + private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId) { var payload = new ExternalMetadataIdsDto() { @@ -1809,11 +1798,7 @@ public class ExternalMetadataService : IExternalMetadataService } try { - var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - var ret = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(payload) - .ReceiveJson(); + var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload); ret.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(ret.Summary)); diff --git a/API/Services/Plus/KavitaPlusApiService.cs b/API/Services/Plus/KavitaPlusApiService.cs index cdf9471f8..ec4f414c3 100644 --- a/API/Services/Plus/KavitaPlusApiService.cs +++ b/API/Services/Plus/KavitaPlusApiService.cs @@ -1,6 +1,13 @@ #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; @@ -17,9 +24,13 @@ 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): IKavitaPlusApiService +public class KavitaPlusApiService(ILogger logger, IUnitOfWork unitOfWork): IKavitaPlusApiService { private const string ScrobblingPath = "/api/scrobbling/"; @@ -42,6 +53,46 @@ public class KavitaPlusApiService(ILogger logger): IKavita 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+ /// diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index efdaec8ff..6ff8d19de 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); + ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); } public class ReadingItemService : IReadingItemService @@ -71,11 +71,12 @@ public class ReadingItemService : IReadingItemService /// Path of a file /// /// Library type to determine parsing to perform - public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) + /// Enable Metadata parsing overriding filename parsing + public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { try { - var info = Parse(path, rootPath, libraryRoot, type); + var info = Parse(path, rootPath, libraryRoot, type, enableMetadata); if (info == null) { _logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path); @@ -174,28 +175,29 @@ public class ReadingItemService : IReadingItemService /// /// /// + /// /// - private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type) + private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { if (_comicVineParser.IsApplicable(path, type)) { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_imageParser.IsApplicable(path, type)) { - return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_bookParser.IsApplicable(path, type)) { - return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_pdfParser.IsApplicable(path, type)) { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_basicParser.IsApplicable(path, type)) { - return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } return null; diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index c3f36ef2e..83558eaa0 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)) + .Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata)) .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))); + _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata))); 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 1462ab3d3..168ca7f01 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, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, 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, comicInfo); + return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo); } var ret = new ParserInfo() @@ -101,7 +101,12 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag } // Patch in other information from ComicInfo - UpdateFromComicInfo(ret); + if (enableMetadata) + { + 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 499e554ef..14f42c989 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, ComicInfo comicInfo = null) + public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, 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, comicInfo); + var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, enableMetadata, 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 b68596245..b60f28aee 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, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) { if (type != LibraryType.ComicVine) return null; @@ -81,7 +81,10 @@ 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 - UpdateFromComicInfo(info); + if (enableMetadata) + { + 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 679d6a031..687617fd7 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, ComicInfo? comicInfo = null); + ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null); void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret); bool IsApplicable(string filePath, LibraryType type); } @@ -26,8 +26,9 @@ 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, ComicInfo? comicInfo = null); + public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, 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 415533631..12f9f4d50 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, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) { if (!IsApplicable(filePath, type)) return null; diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index c8eb010b3..c0b130f91 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -165,9 +165,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( diff --git a/API/Services/Tasks/Scanner/Parser/PdfParser.cs b/API/Services/Tasks/Scanner/Parser/PdfParser.cs index bc12e2c77..80bfa9a48 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, ComicInfo comicInfo = null) + public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null) { var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); var ret = new ParserInfo @@ -68,14 +68,18 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); } - // Patch in other information from ComicInfo - UpdateFromComicInfo(ret); - - if (comicInfo != null && !string.IsNullOrEmpty(comicInfo.Title)) + if (enableMetadata) { - ret.Title = comicInfo.Title.Trim(); + // Patch in other information from ComicInfo + UpdateFromComicInfo(ret); + + if (comicInfo != null && !string.IsNullOrEmpty(comicInfo.Title)) + { + 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/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index e22ee4bb6..cb5f4302f 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -521,6 +521,11 @@ 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/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index ba967d8a6..87a464e6a 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -152,6 +152,10 @@ public static class MessageFactory /// 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) { @@ -679,4 +683,16 @@ public static class MessageFactory }, }; } + public static SignalRMessage ExternalMatchRateLimitErrorEvent(int seriesId, string seriesName) + { + return new SignalRMessage() + { + Name = ExternalMatchRateLimitError, + Body = new + { + seriesId = seriesId, + seriesName = seriesName, + }, + }; + } } diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index f2d64cde6..ba4fd09b7 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 - ? "http://localhost:5020" : "https://plus.kavitareader.com"; + ? "https://plus.kavitareader.com" : "https://plus.kavitareader.com"; // http://localhost:5020 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 081ab80ca..b920416bb 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -9,12 +9,12 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/_tag-card-common.scss b/UI/Web/src/_tag-card-common.scss index 07f37c2a0..39a1e87fd 100644 --- a/UI/Web/src/_tag-card-common.scss +++ b/UI/Web/src/_tag-card-common.scss @@ -5,6 +5,11 @@ 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 { diff --git a/UI/Web/src/app/_helpers/form-debug.ts b/UI/Web/src/app/_helpers/form-debug.ts new file mode 100644 index 000000000..4ad70ac87 --- /dev/null +++ b/UI/Web/src/app/_helpers/form-debug.ts @@ -0,0 +1,120 @@ +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 new file mode 100644 index 000000000..3695651d6 --- /dev/null +++ b/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts @@ -0,0 +1,4 @@ +export interface ExternalMatchRateLimitErrorEvent { + seriesId: number; + seriesName: string; +} diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index bad83f54b..0e7d90ee2 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -31,6 +31,7 @@ export interface Library { manageReadingLists: boolean; allowScrobbling: boolean; allowMetadataMatching: boolean; + enableMetadata: boolean; collapseSeriesRelationships: boolean; libraryFileTypes: Array; excludePatterns: Array; diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index 67f07f32e..f870d1449 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -1,15 +1,16 @@ -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', @@ -114,6 +115,10 @@ export enum EVENTS { * A Person merged has been merged into another */ PersonMerged = 'PersonMerged', + /** + * A Rate limit error was hit when matching a series with Kavita+ + */ + ExternalMatchRateLimitError = 'ExternalMatchRateLimitError' } export interface Message { @@ -236,6 +241,13 @@ 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, diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 05958ee61..52aef2a4a 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -266,13 +266,13 @@ export class ReaderService { getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) { - let params: {[key: string]: any} = {}; - if (incognitoMode) { - params['incognitoMode'] = true; - } + const params: {[key: string]: any} = {}; + params['incognitoMode'] = incognitoMode; + if (readingListMode) { params['readingListId'] = readingListId; } + return params; } diff --git a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts index 223b309da..2a5582145 100644 --- a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts +++ b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {LicenseService} from "../../_services/license.service"; import {Router} from "@angular/router"; -import {TranslocoDirective} from "@jsverse/transloco"; +import {translate, TranslocoDirective} from "@jsverse/transloco"; import {ImageComponent} from "../../shared/image/image.component"; import {ImageService} from "../../_services/image.service"; import {Series} from "../../_models/series"; @@ -23,6 +23,8 @@ import {EVENTS, MessageHubService} from "../../_services/message-hub.service"; import {ScanSeriesEvent} from "../../_models/events/scan-series-event"; import {LibraryTypePipe} from "../../_pipes/library-type.pipe"; import {allKavitaPlusMetadataApplicableTypes} from "../../_models/library/library"; +import {ExternalMatchRateLimitErrorEvent} from "../../_models/events/external-match-rate-limit-error-event"; +import {ToastrService} from "ngx-toastr"; @Component({ selector: 'app-manage-matched-metadata', @@ -55,6 +57,7 @@ export class ManageMatchedMetadataComponent implements OnInit { private readonly manageService = inject(ManageService); private readonly messageHub = inject(MessageHubService); private readonly cdRef = inject(ChangeDetectorRef); + private readonly toastr = inject(ToastrService); protected readonly imageService = inject(ImageService); @@ -74,12 +77,19 @@ export class ManageMatchedMetadataComponent implements OnInit { } this.messageHub.messages$.subscribe(message => { - if (message.event !== EVENTS.ScanSeries) return; - - const evt = message.payload as ScanSeriesEvent; - if (this.data.filter(d => d.series.id === evt.seriesId).length > 0) { - this.loadData(); + if (message.event == EVENTS.ScanSeries) { + const evt = message.payload as ScanSeriesEvent; + if (this.data.filter(d => d.series.id === evt.seriesId).length > 0) { + this.loadData(); + } } + + if (message.event == EVENTS.ExternalMatchRateLimitError) { + const evt = message.payload as ExternalMatchRateLimitErrorEvent; + this.toastr.error(translate('toasts.external-match-rate-error', {seriesName: evt.seriesName})) + } + + }); this.filterGroup.valueChanges.pipe( diff --git a/UI/Web/src/app/browse/browse-genres/browse-genres.component.html b/UI/Web/src/app/browse/browse-genres/browse-genres.component.html index 5eef2c91f..8166ef12c 100644 --- a/UI/Web/src/app/browse/browse-genres/browse-genres.component.html +++ b/UI/Web/src/app/browse/browse-genres/browse-genres.component.html @@ -19,7 +19,7 @@ > -
+
{{ item.title }}
{{t('series-count', {num: item.seriesCount | compactNumber})}} diff --git a/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts b/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts index 02c2a8ead..c46795e25 100644 --- a/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts +++ b/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, inject, OnInit} from '@angular/core'; import {CardDetailLayoutComponent} from "../../cards/card-detail-layout/card-detail-layout.component"; -import {DecimalPipe} from "@angular/common"; +import {DecimalPipe, NgClass} from "@angular/common"; import { SideNavCompanionBarComponent } from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component"; @@ -24,7 +24,8 @@ import {Title} from "@angular/platform-browser"; DecimalPipe, SideNavCompanionBarComponent, TranslocoDirective, - CompactNumberPipe + CompactNumberPipe, + NgClass ], templateUrl: './browse-genres.component.html', styleUrl: './browse-genres.component.scss', @@ -62,7 +63,8 @@ export class BrowseGenresComponent implements OnInit { }); } - openFilter(field: FilterField, value: string | number) { - this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe(); + openFilter(field: FilterField, genre: BrowseGenre) { + if (genre.seriesCount === 0) return; // We don't yet have an issue page + this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${genre.id}`).subscribe(); } } diff --git a/UI/Web/src/app/browse/browse-tags/browse-tags.component.html b/UI/Web/src/app/browse/browse-tags/browse-tags.component.html index dcd59bb1f..627e05584 100644 --- a/UI/Web/src/app/browse/browse-tags/browse-tags.component.html +++ b/UI/Web/src/app/browse/browse-tags/browse-tags.component.html @@ -19,7 +19,7 @@ > -
+
{{ item.title }}
{{t('series-count', {num: item.seriesCount | compactNumber})}} diff --git a/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts b/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts index 92910b0b9..05abb6300 100644 --- a/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts +++ b/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, inject, OnInit} from '@angular/core'; import {CardDetailLayoutComponent} from "../../cards/card-detail-layout/card-detail-layout.component"; -import {DecimalPipe} from "@angular/common"; +import {DecimalPipe, NgClass} from "@angular/common"; import { SideNavCompanionBarComponent } from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component"; @@ -25,7 +25,8 @@ import {Title} from "@angular/platform-browser"; DecimalPipe, SideNavCompanionBarComponent, TranslocoDirective, - CompactNumberPipe + CompactNumberPipe, + NgClass ], templateUrl: './browse-tags.component.html', styleUrl: './browse-tags.component.scss', @@ -61,7 +62,8 @@ export class BrowseTagsComponent implements OnInit { }); } - openFilter(field: FilterField, value: string | number) { - this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe(); + openFilter(field: FilterField, tag: BrowseTag) { + if (tag.seriesCount === 0) return; // We don't yet have an issue page + this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${tag.id}`).subscribe(); } } diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html index 1d1ce4c7e..7433c26c3 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html @@ -229,14 +229,17 @@
} - + diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index 6e8e3b22a..3056d7eb5 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -25,7 +25,8 @@ import {ImageService} from 'src/app/_services/image.service'; import {ReadingListService} from 'src/app/_services/reading-list.service'; import { DraggableOrderedListComponent, - IndexUpdateEvent + IndexUpdateEvent, + ItemRemoveEvent } from '../draggable-ordered-list/draggable-ordered-list.component'; import {forkJoin, startWith, tap} from 'rxjs'; import {ReaderService} from 'src/app/_services/reader.service'; @@ -321,6 +322,7 @@ export class ReadingListDetailComponent implements OnInit { } editReadingList(readingList: ReadingList) { + if (!readingList) return; this.actionService.editReadingList(readingList, (readingList: ReadingList) => { // Reload information around list this.readingListService.getReadingList(this.listId).subscribe(rl => { @@ -347,10 +349,10 @@ export class ReadingListDetailComponent implements OnInit { }); } - itemRemoved(item: ReadingListItem, position: number) { + removeItem(removeEvent: ItemRemoveEvent) { if (!this.readingList) return; - this.readingListService.deleteItem(this.readingList.id, item.id).subscribe(() => { - this.items.splice(position, 1); + this.readingListService.deleteItem(this.readingList.id, removeEvent.item.id).subscribe(() => { + this.items.splice(removeEvent.position, 1); this.items = [...this.items]; this.cdRef.markForCheck(); this.toastr.success(translate('toasts.item-removed')); diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html index 901ee270a..6421205ab 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html @@ -18,10 +18,10 @@ {{item.title}}
@if (showRemove) { - } diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts index acde50022..7ce6f6790 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts @@ -9,6 +9,7 @@ import {ImageComponent} from '../../../shared/image/image.component'; import {TranslocoDirective} from "@jsverse/transloco"; import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component"; import {ReadMoreComponent} from "../../../shared/read-more/read-more.component"; +import {ItemRemoveEvent} from "../draggable-ordered-list/draggable-ordered-list.component"; @Component({ selector: 'app-reading-list-item', @@ -33,9 +34,16 @@ export class ReadingListItemComponent { @Input() promoted: boolean = false; @Output() read: EventEmitter = new EventEmitter(); - @Output() remove: EventEmitter = new EventEmitter(); + @Output() remove: EventEmitter = new EventEmitter(); readChapter(item: ReadingListItem) { this.read.emit(item); } + + removeItem(item: ReadingListItem) { + this.remove.emit({ + item: item, + position: item.order + }); + } } diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts index 8685adc48..d18939c4e 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts @@ -61,7 +61,8 @@ export class ExternalRatingComponent implements OnInit { ngOnInit() { this.reviewService.overallRating(this.seriesId, this.chapterId).subscribe(r => { this.overallRating = r.averageScore; - }); + this.cdRef.markForCheck(); + }); } updateRating(rating: number) { @@ -92,6 +93,4 @@ export class ExternalRatingComponent implements OnInit { return ''; } - - protected readonly RatingAuthority = RatingAuthority; } diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 8cbac271a..ff97fcbb0 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -127,6 +127,16 @@
+
+ + +
+ +
+
+
+
+
diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index 797124c4f..d0fed5c81 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -105,15 +105,16 @@ export class LibrarySettingsModalComponent implements OnInit { libraryForm: FormGroup = new FormGroup({ name: new FormControl('', { nonNullable: true, validators: [Validators.required] }), type: new FormControl(LibraryType.Manga, { nonNullable: true, validators: [Validators.required] }), - folderWatching: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - includeInDashboard: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - includeInRecommended: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - includeInSearch: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - manageCollections: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), - manageReadingLists: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), - allowScrobbling: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - allowMetadataMatching: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - collapseSeriesRelationships: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), + folderWatching: new FormControl(true, { nonNullable: true, validators: [] }), + includeInDashboard: new FormControl(true, { nonNullable: true, validators: [] }), + includeInRecommended: new FormControl(true, { nonNullable: true, validators: [] }), + includeInSearch: new FormControl(true, { nonNullable: true, validators: [] }), + manageCollections: new FormControl(false, { nonNullable: true, validators: [] }), + manageReadingLists: new FormControl(false, { nonNullable: true, validators: [] }), + allowScrobbling: new FormControl(true, { nonNullable: true, validators: [] }), + allowMetadataMatching: new FormControl(true, { nonNullable: true, validators: [] }), + collapseSeriesRelationships: new FormControl(false, { nonNullable: true, validators: [] }), + enableMetadata: new FormControl(true, { nonNullable: true, validators: [] }), // required validator doesn't check value, just if true }); selectedFolders: string[] = []; @@ -155,7 +156,7 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('allowScrobbling')?.disable(); if (this.IsMetadataDownloadEligible) { - this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching); + this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching ?? true); this.libraryForm.get('allowMetadataMatching')?.enable(); } else { this.libraryForm.get('allowMetadataMatching')?.setValue(false); @@ -184,6 +185,20 @@ export class LibrarySettingsModalComponent implements OnInit { this.setValues(); + // Turn on/off manage collections/rl + this.libraryForm.get('enableMetadata')?.valueChanges.pipe( + tap(enabled => { + const manageCollectionsFc = this.libraryForm.get('manageCollections'); + const manageReadingListsFc = this.libraryForm.get('manageReadingLists'); + + manageCollectionsFc?.setValue(enabled); + manageReadingListsFc?.setValue(enabled); + + this.cdRef.markForCheck(); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + // This needs to only apply after first render this.libraryForm.get('type')?.valueChanges.pipe( tap((type: LibraryType) => { @@ -257,6 +272,8 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships); this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false); this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false); + this.libraryForm.get('excludePatterns')?.setValue(this.excludePatterns ? this.library.excludePatterns : false); + this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata, true); this.selectedFolders = this.library.folders; this.madeChanges = false; diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 91a3dac9e..c6b8c823f 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1129,6 +1129,8 @@ "include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.", "include-in-search-label": "Include in Search", "include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.", + "enable-metadata-label": "Enable Metadata (ComicInfo/Epub/PDF)", + "enable-metadata-tooltip": "Allow Kavita to read metadata files which override filename parsing.", "force-scan": "Force Scan", "force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan", "reset": "{{common.reset}}", @@ -2743,7 +2745,8 @@ "webtoon-override": "Switching to Webtoon mode due to images representing a webtoon.", "scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services.", "series-bound-to-reading-profile": "Series bound to Reading Profile {{name}}", - "library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}" + "library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}", + "external-match-rate-error": "Kavita ran out of rate looking up {{seriesName}}. Try again in 5 minutes." }, "read-time-pipe": {